Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
jsonapi-resources / lib / jsonapi / resource.rb
Size: Mime:
require 'jsonapi/callbacks'

module JSONAPI
  class Resource
    include Callbacks

    attr_reader :context

    define_jsonapi_resources_callbacks :create,
                                       :update,
                                       :remove,
                                       :save,
                                       :create_to_many_link,
                                       :replace_to_many_links,
                                       :create_to_one_link,
                                       :replace_to_one_link,
                                       :replace_polymorphic_to_one_link,
                                       :remove_to_many_link,
                                       :remove_to_one_link,
                                       :replace_fields

    def initialize(model, context)
      @model = model
      @context = context
    end

    def _model
      @model
    end

    def id
      _model.public_send(self.class._primary_key)
    end

    def is_new?
      id.nil?
    end

    def change(callback)
      completed = false

      if @changing
        run_callbacks callback do
          completed = (yield == :completed)
        end
      else
        run_callbacks is_new? ? :create : :update do
          @changing = true
          run_callbacks callback do
            completed = (yield == :completed)
          end

          completed = (save == :completed) if @save_needed || is_new?
        end
      end

      return completed ? :completed : :accepted
    end

    def remove
      run_callbacks :remove do
        _remove
      end
    end

    def create_to_many_links(relationship_type, relationship_key_values)
      change :create_to_many_link do
        _create_to_many_links(relationship_type, relationship_key_values)
      end
    end

    def replace_to_many_links(relationship_type, relationship_key_values)
      change :replace_to_many_links do
        _replace_to_many_links(relationship_type, relationship_key_values)
      end
    end

    def replace_to_one_link(relationship_type, relationship_key_value)
      change :replace_to_one_link do
        _replace_to_one_link(relationship_type, relationship_key_value)
      end
    end

    def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
      change :replace_polymorphic_to_one_link do
        _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type)
      end
    end

    def remove_to_many_link(relationship_type, key)
      change :remove_to_many_link do
        _remove_to_many_link(relationship_type, key)
      end
    end

    def remove_to_one_link(relationship_type)
      change :remove_to_one_link do
        _remove_to_one_link(relationship_type)
      end
    end

    def replace_fields(field_data)
      change :replace_fields do
        _replace_fields(field_data)
      end
    end

    # Override this on a resource instance to override the fetchable keys
    def fetchable_fields
      self.class.fields
    end

    # Override this on a resource to customize how the associated records
    # are fetched for a model. Particularly helpful for authorization.
    def records_for(relation_name)
      _model.public_send relation_name
    end

    def model_error_messages
      _model.errors.messages
    end

    # Add metadata to validation error objects.
    #
    # Suppose `model_error_messages` returned the following error messages
    # hash:
    #
    #   {password: ["too_short", "format"]}
    #
    # Then to add data to the validation error `validation_error_metadata`
    # could return:
    #
    #   {
    #     password: {
    #       "too_short": {"minimum_length" => 6},
    #       "format": {"requirement" => "must contain letters and numbers"}
    #     }
    #   }
    #
    # The specified metadata is then be merged into the validation error
    # object.
    def validation_error_metadata
      {}
    end

    # Override this to return resource level meta data
    # must return a hash, and if the hash is empty the meta section will not be serialized with the resource
    # meta keys will be not be formatted with the key formatter for the serializer by default. They can however use the
    # serializer's format_key and format_value methods if desired
    # the _options hash will contain the serializer and the serialization_options
    def meta(_options)
      {}
    end

    # Override this to return custom links
    # must return a hash, which will be merged with the default { self: 'self-url' } links hash
    # links keys will be not be formatted with the key formatter for the serializer by default.
    # They can however use the serializer's format_key and format_value methods if desired
    # the _options hash will contain the serializer and the serialization_options
    def custom_links(_options)
      {}
    end

    private

    def save
      run_callbacks :save do
        _save
      end
    end

    # Override this on a resource to return a different result code. Any
    # value other than :completed will result in operations returning
    # `:accepted`
    #
    # For example to return `:accepted` if your model does not immediately
    # save resources to the database you could override `_save` as follows:
    #
    # ```
    # def _save
    #   super
    #   return :accepted
    # end
    # ```
    def _save
      unless @model.valid?
        fail JSONAPI::Exceptions::ValidationErrors.new(self)
      end

      if defined? @model.save
        saved = @model.save(validate: false)
        unless saved
          if @model.errors.present?
            fail JSONAPI::Exceptions::ValidationErrors.new(self)
          else
            fail JSONAPI::Exceptions::SaveFailed.new
          end
        end
      else
        saved = true
      end

      @save_needed = !saved

      :completed
    end

    def _remove
      unless @model.destroy
        fail JSONAPI::Exceptions::ValidationErrors.new(self)
      end
      :completed
    end

    def _create_to_many_links(relationship_type, relationship_key_values)
      relationship = self.class._relationships[relationship_type]

      relationship_key_values.each do |relationship_key_value|
        related_resource = relationship.resource_klass.find_by_key(relationship_key_value, context: @context)

        relation_name = relationship.relation_name(context: @context)
        # TODO: Add option to skip relations that already exist instead of returning an error?
        relation = @model.public_send(relation_name).where(relationship.primary_key => relationship_key_value).first
        if relation.nil?
          @model.public_send(relation_name) << related_resource._model
        else
          fail JSONAPI::Exceptions::HasManyRelationExists.new(relationship_key_value)
        end
      end

      :completed
    end

    def _replace_to_many_links(relationship_type, relationship_key_values)
      relationship = self.class._relationships[relationship_type]
      send("#{relationship.foreign_key}=", relationship_key_values)
      @save_needed = true

      :completed
    end

    def _replace_to_one_link(relationship_type, relationship_key_value)
      relationship = self.class._relationships[relationship_type]

      send("#{relationship.foreign_key}=", relationship_key_value)
      @save_needed = true

      :completed
    end

    def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type)
      relationship = self.class._relationships[relationship_type.to_sym]

      _model.public_send("#{relationship.foreign_key}=", key_value)
      _model.public_send("#{relationship.polymorphic_type}=", key_type.to_s.classify)

      @save_needed = true

      :completed
    end

    def _remove_to_many_link(relationship_type, key)
      relation_name = self.class._relationships[relationship_type].relation_name(context: @context)

      @model.public_send(relation_name).delete(key)

      :completed
    end

    def _remove_to_one_link(relationship_type)
      relationship = self.class._relationships[relationship_type]

      send("#{relationship.foreign_key}=", nil)
      @save_needed = true

      :completed
    end

    def _replace_fields(field_data)
      field_data[:attributes].each do |attribute, value|
        begin
          send "#{attribute}=", value
          @save_needed = true
        rescue ArgumentError
          # :nocov: Will be thrown if an enum value isn't allowed for an enum. Currently not tested as enums are a rails 4.1 and higher feature
          raise JSONAPI::Exceptions::InvalidFieldValue.new(attribute, value)
          # :nocov:
        end
      end

      field_data[:to_one].each do |relationship_type, value|
        if value.nil?
          remove_to_one_link(relationship_type)
        else
          case value
          when Hash
            replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type))
          else
            replace_to_one_link(relationship_type, value)
          end
        end
      end if field_data[:to_one]

      field_data[:to_many].each do |relationship_type, values|
        replace_to_many_links(relationship_type, values)
      end if field_data[:to_many]

      :completed
    end

    class << self
      def inherited(subclass)
        subclass.abstract(false)
        subclass.immutable(false)
        subclass._attributes = (_attributes || {}).dup
        subclass._model_hints = (_model_hints || {}).dup

        subclass._relationships = {}
        # Add the relationships from the base class to the subclass using the original options
        if _relationships.is_a?(Hash)
          _relationships.each_value do |relationship|
            options = relationship.options.dup
            options[:parent_resource] = subclass
            subclass._add_relationship(relationship.class, relationship.name, options)
          end
        end

        subclass._allowed_filters = (_allowed_filters || Set.new).dup

        type = subclass.name.demodulize.sub(/Resource$/, '').underscore
        subclass._type = type.pluralize.to_sym

        subclass.attribute :id, format: :id

        check_reserved_resource_name(subclass._type, subclass.name)
      end

      def resource_for(type)
        type_with_module = type.include?('/') ? type : module_path + type

        resource_name = _resource_name_from_type(type_with_module)
        resource = resource_name.safe_constantize if resource_name
        if resource.nil?
          fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
        end
        resource
      end

      def resource_for_model(model)
        resource_for(resource_type_for(model))
      end

      def _resource_name_from_type(type)
        "#{type.to_s.underscore.singularize}_resource".camelize
      end

      def resource_type_for(model)
        model_name = model.class.to_s.underscore
        if _model_hints[model_name]
          _model_hints[model_name]
        else
          model_name.rpartition('/').last
        end
      end

      attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints

      def create(context)
        new(create_model, context)
      end

      def create_model
        _model_class.new
      end

      def routing_options(options)
        @_routing_resource_options = options
      end

      def routing_resource_options
        @_routing_resource_options ||= {}
      end

      # Methods used in defining a resource class
      def attributes(*attrs)
        options = attrs.extract_options!.dup
        attrs.each do |attr|
          attribute(attr, options)
        end
      end

      def attribute(attr, options = {})
        check_reserved_attribute_name(attr)

        if (attr.to_sym == :id) && (options[:format].nil?)
          ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.')
        end

        @_attributes ||= {}
        @_attributes[attr] = options
        define_method attr do
          @model.public_send(attr)
        end unless method_defined?(attr)

        define_method "#{attr}=" do |value|
          @model.public_send "#{attr}=", value
        end unless method_defined?("#{attr}=")
      end

      def default_attribute_options
        { format: :default }
      end

      def relationship(*attrs)
        options = attrs.extract_options!
        klass = case options[:to]
                  when :one
                    Relationship::ToOne
                  when :many
                    Relationship::ToMany
                  else
                    #:nocov:#
                    fail ArgumentError.new('to: must be either :one or :many')
                    #:nocov:#
                end
        _add_relationship(klass, *attrs, options.except(:to))
      end

      def has_one(*attrs)
        _add_relationship(Relationship::ToOne, *attrs)
      end

      def has_many(*attrs)
        _add_relationship(Relationship::ToMany, *attrs)
      end

      def model_name(model, options = {})
        @_model_name = model.to_sym

        model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
      end

      def model_hint(model: _model_name, resource: _type)
        model_name = ((model.is_a?(Class)) && (model < ActiveRecord::Base)) ? model.name : model
        resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s

        _model_hints[model_name.to_s.gsub('::', '/').underscore] = resource_type.to_s
      end

      def filters(*attrs)
        @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h })
      end

      def filter(attr, *args)
        @_allowed_filters[attr.to_sym] = args.extract_options!
      end

      def primary_key(key)
        @_primary_key = key.to_sym
      end

      # TODO: remove this after the createable_fields and updateable_fields are phased out
      # :nocov:
      def method_missing(method, *args)
        if method.to_s.match /createable_fields/
          ActiveSupport::Deprecation.warn('`createable_fields` is deprecated, please use `creatable_fields` instead')
          creatable_fields(*args)
        elsif method.to_s.match /updateable_fields/
          ActiveSupport::Deprecation.warn('`updateable_fields` is deprecated, please use `updatable_fields` instead')
          updatable_fields(*args)
        else
          super
        end
      end
      # :nocov:

      # Override in your resource to filter the updatable keys
      def updatable_fields(_context = nil)
        _updatable_relationships | _attributes.keys - [:id]
      end

      # Override in your resource to filter the creatable keys
      def creatable_fields(_context = nil)
        _updatable_relationships | _attributes.keys
      end

      # Override in your resource to filter the sortable keys
      def sortable_fields(_context = nil)
        _attributes.keys
      end

      def fields
        _relationships.keys | _attributes.keys
      end

      def resolve_relationship_names_to_relations(resource_klass, model_includes, options = {})
        case model_includes
          when Array
            return model_includes.map do |value|
              resolve_relationship_names_to_relations(resource_klass, value, options)
            end
          when Hash
            model_includes.keys.each do |key|
              relationship = resource_klass._relationships[key]
              value = model_includes[key]
              model_includes.delete(key)
              model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options)
            end
            return model_includes
          when Symbol
            relationship = resource_klass._relationships[model_includes]
            return relationship.relation_name(options)
        end
      end

      def apply_includes(records, options = {})
        include_directives = options[:include_directives]
        if include_directives
          model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options)
          records = records.includes(model_includes)
        end

        records
      end

      def apply_pagination(records, paginator, order_options)
        records = paginator.apply(records, order_options) if paginator
        records
      end

      def apply_sort(records, order_options, _context = {})
        if order_options.any?
          records.order(order_options)
        else
          records
        end
      end

      def apply_filter(records, filter, value, options = {})
        strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]

        if strategy
          if strategy.is_a?(Symbol) || strategy.is_a?(String)
            send(strategy, records, value, options)
          else
            strategy.call(records, value, options)
          end
        else
          records.where(filter => value)
        end
      end

      def apply_filters(records, filters, options = {})
        required_includes = []

        if filters
          filters.each do |filter, value|
            if _relationships.include?(filter)
              if _relationships[filter].belongs_to?
                records = apply_filter(records, _relationships[filter].foreign_key, value, options)
              else
                required_includes.push(filter.to_s)
                records = apply_filter(records, "#{_relationships[filter].table_name}.#{_relationships[filter].primary_key}", value, options)
              end
            else
              records = apply_filter(records, filter, value, options)
            end
          end
        end

        if required_includes.any?
          records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes)))
        end

        records
      end

      def filter_records(filters, options, records = records(options))
        records = apply_filters(records, filters, options)
        apply_includes(records, options)
      end

      def sort_records(records, order_options, context = {})
        apply_sort(records, order_options, context)
      end

      def find_count(filters, options = {})
        filter_records(filters, options).count(:all)
      end

      # Override this method if you have more complex requirements than this basic find method provides
      def find(filters, options = {})
        context = options[:context]

        records = filter_records(filters, options)

        sort_criteria = options.fetch(:sort_criteria) { [] }
        order_options = construct_order_options(sort_criteria)
        records = sort_records(records, order_options, context)

        records = apply_pagination(records, options[:paginator], order_options)

        resources = []
        records.each do |model|
          resources.push self.resource_for_model(model).new(model, context)
        end

        resources
      end

      def find_by_key(key, options = {})
        context = options[:context]
        records = records(options)
        records = apply_includes(records, options)
        model = records.where({_primary_key => key}).first
        fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
        self.resource_for_model(model).new(model, context)
      end

      # Override this method if you want to customize the relation for
      # finder methods (find, find_by_key)
      def records(_options = {})
        _model_class.all
      end

      def verify_filters(filters, context = nil)
        verified_filters = {}
        filters.each do |filter, raw_value|
          verified_filter = verify_filter(filter, raw_value, context)
          verified_filters[verified_filter[0]] = verified_filter[1]
        end
        verified_filters
      end

      def is_filter_relationship?(filter)
        filter == _type || _relationships.include?(filter)
      end

      def verify_filter(filter, raw, context = nil)
        filter_values = []
        if raw.present?
          filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
        end

        strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]

        if strategy
          if strategy.is_a?(Symbol) || strategy.is_a?(String)
            values = send(strategy, filter_values, context)
          else
            values = strategy.call(filter_values, context)
          end
          [filter, values]
        else
          if is_filter_relationship?(filter)
            verify_relationship_filter(filter, filter_values, context)
          else
            verify_custom_filter(filter, filter_values, context)
          end
        end
      end

      def key_type(key_type)
        @_resource_key_type = key_type
      end

      def resource_key_type
        @_resource_key_type || JSONAPI.configuration.resource_key_type
      end

      def verify_key(key, context = nil)
        key_type = resource_key_type

        case key_type
        when :integer
          return if key.nil?
          Integer(key)
        when :string
          return if key.nil?
          if key.to_s.include?(',')
            raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
          else
            key
          end
        when :uuid
          return if key.nil?
          if key.to_s.match(/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/)
            key
          else
            raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
          end
        else
          key_type.call(key, context)
        end
      rescue
        raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key)
      end

      # override to allow for key processing and checking
      def verify_keys(keys, context = nil)
        return keys.collect do |key|
          verify_key(key, context)
        end
      end

      # Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
      def verify_custom_filter(filter, value, _context = nil)
        [filter, value]
      end

      # Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
      # relationship logic, such as uuids, multiple keys or permission checks on keys
      def verify_relationship_filter(filter, raw, _context = nil)
        [filter, raw]
      end

      # quasi private class methods
      def _attribute_options(attr)
        default_attribute_options.merge(@_attributes[attr])
      end

      def _updatable_relationships
        @_relationships.map { |key, _relationship| key }
      end

      def _relationship(type)
        type = type.to_sym
        @_relationships[type]
      end

      def _model_name
        _abstract ? '' : @_model_name ||= name.demodulize.sub(/Resource$/, '')
      end

      def _primary_key
        @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
      end

      def _table_name
        @_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
      end

      def _as_parent_key
        @_as_parent_key ||= "#{_type.to_s.singularize}_id"
      end

      def _allowed_filters
        !@_allowed_filters.nil? ? @_allowed_filters : { id: {} }
      end

      def _paginator
        @_paginator ||= JSONAPI.configuration.default_paginator
      end

      def paginator(paginator)
        @_paginator = paginator
      end

      def abstract(val = true)
        @abstract = val
      end

      def _abstract
        @abstract
      end

      def immutable(val = true)
        @immutable = val
      end

      def _immutable
        @immutable
      end

      def mutable?
        !@immutable
      end

      def _model_class
        return nil if _abstract

        return @model if @model
        @model = _model_name.to_s.safe_constantize
        warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this a base Resource declare it as abstract." if @model.nil?
        @model
      end

      def _allowed_filter?(filter)
        !_allowed_filters[filter].nil?
      end

      def module_path
        if name == 'JSONAPI::Resource'
          ''
        else
          name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
        end
      end

      def construct_order_options(sort_params)
        return {} unless sort_params

        sort_params.each_with_object({}) do |sort, order_hash|
          field = sort[:field] == 'id' ? _primary_key : sort[:field]
          order_hash[field] = sort[:direction]
        end
      end

      def _add_relationship(klass, *attrs)
        options = attrs.extract_options!
        options[:parent_resource] = self

        attrs.each do |attr|
          relationship_name = attr.to_sym

          check_reserved_relationship_name(relationship_name)

          # Initialize from an ActiveRecord model's properties
          if _model_class && _model_class.ancestors.collect{|ancestor| ancestor.name}.include?('ActiveRecord::Base')
            model_association = _model_class.reflect_on_association(relationship_name)
            if model_association
              options[:class_name] ||= model_association.class_name
            end
          end

          @_relationships[relationship_name] = relationship = klass.new(relationship_name, options)

          associated_records_method_name = case relationship
                                           when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}"
                                           when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}"
                                           end

          foreign_key = relationship.foreign_key

          define_method "#{foreign_key}=" do |value|
            @model.method("#{foreign_key}=").call(value)
          end unless method_defined?("#{foreign_key}=")

          define_method associated_records_method_name do
            relationship = self.class._relationships[relationship_name]
            relation_name = relationship.relation_name(context: @context)
            records_for(relation_name)
          end unless method_defined?(associated_records_method_name)

          if relationship.is_a?(JSONAPI::Relationship::ToOne)
            if relationship.belongs_to?
              define_method foreign_key do
                @model.method(foreign_key).call
              end unless method_defined?(foreign_key)

              define_method relationship_name do |options = {}|
                relationship = self.class._relationships[relationship_name]

                if relationship.polymorphic?
                  associated_model = public_send(associated_records_method_name)
                  resource_klass = self.class.resource_for_model(associated_model) if associated_model
                  return resource_klass.new(associated_model, @context) if resource_klass
                else
                  resource_klass = relationship.resource_klass
                  if resource_klass
                    associated_model = public_send(associated_records_method_name)
                    return associated_model ? resource_klass.new(associated_model, @context) : nil
                  end
                end
              end unless method_defined?(relationship_name)
            else
              define_method foreign_key do
                relationship = self.class._relationships[relationship_name]

                record = public_send(associated_records_method_name)
                return nil if record.nil?
                record.public_send(relationship.resource_klass._primary_key)
              end unless method_defined?(foreign_key)

              define_method relationship_name do |options = {}|
                relationship = self.class._relationships[relationship_name]

                resource_klass = relationship.resource_klass
                if resource_klass
                  associated_model = public_send(associated_records_method_name)
                  return associated_model ? resource_klass.new(associated_model, @context) : nil
                end
              end unless method_defined?(relationship_name)
            end
          elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
            define_method foreign_key do
              records = public_send(associated_records_method_name)
              return records.collect do |record|
                record.public_send(relationship.resource_klass._primary_key)
              end
            end unless method_defined?(foreign_key)

            define_method relationship_name do |options = {}|
              relationship = self.class._relationships[relationship_name]

              resource_klass = relationship.resource_klass
              records = public_send(associated_records_method_name)

              filters = options.fetch(:filters, {})
              unless filters.nil? || filters.empty?
                records = resource_klass.apply_filters(records, filters, options)
              end

              sort_criteria =  options.fetch(:sort_criteria, {})
              unless sort_criteria.nil? || sort_criteria.empty?
                order_options = relationship.resource_klass.construct_order_options(sort_criteria)
                records = resource_klass.apply_sort(records, order_options, @context)
              end

              paginator = options[:paginator]
              if paginator
                records = resource_klass.apply_pagination(records, paginator, order_options)
              end

              return records.collect do |record|
                if relationship.polymorphic?
                  resource_klass = self.class.resource_for_model(record)
                end
                resource_klass.new(record, @context)
              end
            end unless method_defined?(relationship_name)
          end
        end
      end

      private

      def check_reserved_resource_name(type, name)
        if [:ids, :types, :hrefs, :links].include?(type)
          warn "[NAME COLLISION] `#{name}` is a reserved resource name."
          return
        end
      end

      def check_reserved_attribute_name(name)
        # Allow :id since it can be used to specify the format. Since it is a method on the base Resource
        # an attribute method won't be created for it.
        if [:type].include?(name.to_sym)
          warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
        end
      end

      def check_reserved_relationship_name(name)
        if [:id, :ids, :type, :types].include?(name.to_sym)
          warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
        end
      end
    end
  end
end