lib/jsonapi/resource.rb in jsonapi-resources-0.10.0.beta3 vs lib/jsonapi/resource.rb in jsonapi-resources-0.10.0.beta4

- old
+ new

@@ -1,1108 +1,5 @@ -require 'jsonapi/callbacks' -require 'jsonapi/configuration' - 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 - @reload_needed = false - @changing = false - @save_needed = false - end - - def _model - @model - end - - def id - _model.public_send(self.class._primary_key) - end - - def identity - JSONAPI::ResourceIdentity.new(self.class, id) - end - - def cache_id - [id, self.class.hash_cache_field(_model.public_send(self.class._cache_field))] - 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, options = {}) - change :create_to_many_link do - _create_to_many_links(relationship_type, relationship_key_values, options) - end - end - - def replace_to_many_links(relationship_type, relationship_key_values, options = {}) - change :replace_to_many_links do - _replace_to_many_links(relationship_type, relationship_key_values, options) - end - end - - def replace_to_one_link(relationship_type, relationship_key_value, options = {}) - change :replace_to_one_link do - _replace_to_one_link(relationship_type, relationship_key_value, options) - end - end - - def replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options = {}) - change :replace_polymorphic_to_one_link do - _replace_polymorphic_to_one_link(relationship_type, relationship_key_value, relationship_key_type, options) - end - end - - def remove_to_many_link(relationship_type, key, options = {}) - change :remove_to_many_link do - _remove_to_many_link(relationship_type, key, options) - end - end - - def remove_to_one_link(relationship_type, options = {}) - change :remove_to_one_link do - _remove_to_one_link(relationship_type, options) - 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 - - 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(validation_context = nil) - unless @model.valid?(validation_context) - 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 - @model.reload if @reload_needed - @reload_needed = false - - @save_needed = !saved - - :completed - end - - def _remove - unless @model.destroy - fail JSONAPI::Exceptions::ValidationErrors.new(self) - end - :completed - - rescue ActiveRecord::DeleteRestrictionError => e - fail JSONAPI::Exceptions::RecordLocked.new(e.message) - end - - def reflect_relationship?(relationship, options) - return false if !relationship.reflect || - (!JSONAPI.configuration.use_relationship_reflection || options[:reflected_source]) - - inverse_relationship = relationship.resource_klass._relationships[relationship.inverse_relationship] - if inverse_relationship.nil? - warn "Inverse relationship could not be found for #{self.class.name}.#{relationship.name}. Relationship reflection disabled." - return false - end - true - end - - def _create_to_many_links(relationship_type, relationship_key_values, options) - relationship = self.class._relationships[relationship_type] - relation_name = relationship.relation_name(context: @context) - - if options[:reflected_source] - @model.public_send(relation_name) << options[:reflected_source]._model - return :completed - end - - # load requested related resources - # make sure they all exist (also based on context) and add them to relationship - - related_resources = relationship.resource_klass.find_by_keys(relationship_key_values, context: @context) - - if related_resources.count != relationship_key_values.count - # todo: obscure id so not to leak info - fail JSONAPI::Exceptions::RecordNotFound.new('unspecified') - end - - reflect = reflect_relationship?(relationship, options) - - related_resources.each do |related_resource| - if reflect - if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany) - related_resource.create_to_many_links(relationship.inverse_relationship, [id], reflected_source: self) - else - related_resource.replace_to_one_link(relationship.inverse_relationship, id, reflected_source: self) - end - @reload_needed = true - else - unless @model.public_send(relation_name).include?(related_resource._model) - @model.public_send(relation_name) << related_resource._model - end - end - end - - :completed - end - - def _replace_to_many_links(relationship_type, relationship_key_values, options) - relationship = self.class._relationship(relationship_type) - - reflect = reflect_relationship?(relationship, options) - - if reflect - existing_rids = self.class.find_related_fragments([identity], relationship_type, options) - - existing = existing_rids.keys.collect { |rid| rid.id } - - to_delete = existing - (relationship_key_values & existing) - to_delete.each do |key| - _remove_to_many_link(relationship_type, key, reflected_source: self) - end - - to_add = relationship_key_values - (relationship_key_values & existing) - _create_to_many_links(relationship_type, to_add, {}) - - @reload_needed = true - else - send("#{relationship.foreign_key}=", relationship_key_values) - @save_needed = true - end - - :completed - end - - def _replace_to_one_link(relationship_type, relationship_key_value, _options) - 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, _options) - relationship = self.class._relationships[relationship_type.to_sym] - - send("#{relationship.foreign_key}=", {type: key_type, id: key_value}) - @save_needed = true - - :completed - end - - def _remove_to_many_link(relationship_type, key, options) - relationship = self.class._relationships[relationship_type] - - reflect = reflect_relationship?(relationship, options) - - if reflect - - related_resource = relationship.resource_klass.find_by_key(key, context: @context) - - if related_resource.nil? - fail JSONAPI::Exceptions::RecordNotFound.new(key) - else - if related_resource.class._relationships[relationship.inverse_relationship].is_a?(JSONAPI::Relationship::ToMany) - related_resource.remove_to_many_link(relationship.inverse_relationship, id, reflected_source: self) - else - related_resource.remove_to_one_link(relationship.inverse_relationship, reflected_source: self) - end - end - - @reload_needed = true - else - @model.public_send(relationship.relation_name(context: @context)).destroy(key) - end - - :completed - - rescue ActiveRecord::DeleteRestrictionError => e - fail JSONAPI::Exceptions::RecordLocked.new(e.message) - rescue ActiveRecord::RecordNotFound - fail JSONAPI::Exceptions::RecordNotFound.new(key) - end - - def _remove_to_one_link(relationship_type, _options) - 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.caching(_caching) - subclass.paginator(_paginator) - subclass._attributes = (_attributes || {}).dup - subclass.polymorphic(false) - - subclass._model_hints = (_model_hints || {}).dup - - unless _model_name.empty? || _immutable - subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true) - end - - subclass.rebuild_relationships(_relationships || {}) - - subclass._allowed_filters = (_allowed_filters || Set.new).dup - - subclass._allowed_sort = _allowed_sort.dup - - type = subclass.name.demodulize.sub(/Resource$/, '').underscore - subclass._type = type.pluralize.to_sym - - unless subclass._attributes[:id] - subclass.attribute :id, format: :id, readonly: true - end - - check_reserved_resource_name(subclass._type, subclass.name) - - subclass.include JSONAPI.configuration.resource_finder if JSONAPI.configuration.resource_finder - end - - # A ResourceFinder is a mixin that adds functionality to find Resources and Resource Fragments - # to the core Resource class. - # - # Resource fragments are a hash with the following format: - # { - # identity: <required: a ResourceIdentity>, - # cache: <optional: the resource's cache value> - # attributes: <optional: attributes hash for attributes requested - currently unused> - # related: { - # <relationship_name>: <ResourceIdentity of a source resource in find_included_fragments> - # } - # } - # - # begin ResourceFinder Abstract methods - def find(_filters, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - def count(_filters, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - def find_by_keys(_keys, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - def find_by_key(_key, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - def find_fragments(_filters, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - def find_included_fragments(_source_rids, _relationship_name, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - def find_related_fragments(_source_rids, _relationship_name, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - def count_related(_source_rid, _relationship_name, _options = {}) - # :nocov: - raise 'Abstract ResourceFinder method called. Ensure that a ResourceFinder has been set.' - # :nocov: - end - - #end ResourceFinder Abstract methods - - def rebuild_relationships(relationships) - original_relationships = relationships.deep_dup - - @_relationships = {} - - if original_relationships.is_a?(Hash) - original_relationships.each_value do |relationship| - options = relationship.options.dup - options[:parent_resource] = self - options[:inverse_relationship] = relationship.inverse_relationship - _add_relationship(relationship.class, relationship.name, options) - end - end - end - - def resource_klass_for(type) - type = type.underscore - type_with_module = type.start_with?(module_path) ? 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_klass_for_model(model) - resource_klass_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, :_type, :_model_hints - attr_writer :_allowed_filters, :_paginator, :_allowed_sort - - 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(attribute_name, options = {}) - attr = attribute_name.to_sym - - check_reserved_attribute_name(attr) - - if (attr == :id) && (options[:format].nil?) - ActiveSupport::Deprecation.warn('Id without format is no longer supported. Please remove ids from attributes, or specify a format.') - end - - check_duplicate_attribute_name(attr) if options[:format].nil? - - @_attributes ||= {} - @_attributes[attr] = options - define_method attr do - @model.public_send(options[:delegate] ? options[:delegate].to_sym : attr) - end unless method_defined?(attr) - - define_method "#{attr}=" do |value| - @model.public_send("#{options[:delegate] ? options[:delegate].to_sym : attr}=", value) - end unless method_defined?("#{attr}=") - - if options.fetch(:sortable, true) && !_has_sort?(attr) - sort attr - end - end - - def attribute_to_model_field(attribute) - field_name = if attribute == :_cache_field - _cache_field - else - # Note: this will allow the returning of model attributes without a corresponding - # resource attribute, for example a belongs_to id such as `author_id` or bypassing - # the delegate. - attr = @_attributes[attribute] - attr && attr[:delegate] ? attr[:delegate].to_sym : attribute - end - if Rails::VERSION::MAJOR >= 5 - attribute_type = _model_class.attribute_types[field_name.to_s] - else - attribute_type = _model_class.column_types[field_name.to_s] - end - { name: field_name, type: attribute_type} - end - - def cast_to_attribute_type(value, type) - if Rails::VERSION::MAJOR >= 5 - return type.cast(value) - else - return type.type_cast_from_database(value) - end - 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 belongs_to(*attrs) - ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\ - " using the `belongs_to` class method. We think `has_one`" \ - " is more appropriate. If you know what you're doing," \ - " and don't want to see this warning again, override the" \ - " `belongs_to` class method on your resource." - _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 - - rebuild_relationships(_relationships) - end - - def model_hint(model: _model_name, resource: _type) - resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s - - _model_hints[model.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 sort(sorting, options = {}) - self._allowed_sort[sorting.to_sym] = options - end - - def sorts(*args) - options = args.extract_options! - _allowed_sort.merge!(args.inject({}) { |h, sorting| h[sorting.to_sym] = options.dup; h }) - end - - def primary_key(key) - @_primary_key = key.to_sym - end - - def cache_field(field) - @_cache_field = field.to_sym - end - - # Override in your resource to filter the updatable keys - def updatable_fields(_context = nil) - _updatable_relationships | _updatable_attributes - [:id] - end - - # Override in your resource to filter the creatable keys - def creatable_fields(_context = nil) - _updatable_relationships | _updatable_attributes - end - - # Override in your resource to filter the sortable keys - def sortable_fields(_context = nil) - _allowed_sort.keys - end - - def sortable_field?(key, context = nil) - sortable_fields(context).include? key.to_sym - end - - def fields - _relationships.keys | _attributes.keys - end - - def resources_for(records, context) - records.collect do |record| - resource_for(record, context) - end - end - - def resource_for(model_record, context) - resource_klass = self.resource_klass_for_model(model_record) - resource_klass.new(model_record, context) - 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? - begin - filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw] - rescue CSV::MalformedCSVError - filter_values << raw - end - end - - strategy = _allowed_filters.fetch(filter, Hash.new)[:verify] - - if strategy - values = call_method_or_proc(strategy, filter_values, context) - [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 call_method_or_proc(strategy, *args) - if strategy.is_a?(Symbol) || strategy.is_a?(String) - send(strategy, *args) - else - strategy.call(*args) - 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 lambda 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 lambda 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 _attribute_delegated_name(attr) - @_attributes.fetch(attr.to_sym, {}).fetch(:delegate, attr) - end - - def _has_attribute?(attr) - @_attributes.keys.include?(attr.to_sym) - end - - def _updatable_attributes - _attributes.map { |key, options| key unless options[:readonly] }.compact - end - - def _updatable_relationships - @_relationships.map { |key, relationship| key unless relationship.readonly? }.compact - end - - def _relationship(type) - return nil unless type - type = type.to_sym - @_relationships[type] - end - - def _model_name - if _abstract - '' - else - return @_model_name.to_s if defined?(@_model_name) - class_name = self.name - return '' if class_name.nil? - @_model_name = class_name.demodulize.sub(/Resource$/, '') - @_model_name.to_s - end - end - - def _polymorphic_name - if !_polymorphic - '' - else - @_polymorphic_name ||= _model_name.to_s.downcase - end - end - - def _primary_key - @_primary_key ||= _default_primary_key - end - - def _default_primary_key - @_default_primary_key ||=_model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id - end - - def _cache_field - @_cache_field ||= JSONAPI.configuration.default_resource_cache_field - 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 - defined?(@_allowed_filters) ? @_allowed_filters : { id: {} } - end - - def _allowed_sort - @_allowed_sort ||= {} - end - - def _paginator - @_paginator ||= JSONAPI.configuration.default_paginator - end - - def paginator(paginator) - @_paginator = paginator - end - - def _polymorphic - @_polymorphic - end - - def polymorphic(polymorphic = true) - @_polymorphic = polymorphic - end - - def _polymorphic_types - @poly_hash ||= {}.tap do |hash| - ObjectSpace.each_object do |klass| - next unless Module === klass - if klass < ActiveRecord::Base - klass.reflect_on_all_associations(:has_many).select{|r| r.options[:as] }.each do |reflection| - (hash[reflection.options[:as]] ||= []) << klass.name.downcase - end - end - end - end - @poly_hash[_polymorphic_name.to_sym] - end - - def _polymorphic_resource_klasses - @_polymorphic_resource_klasses ||= _polymorphic_types.collect do |type| - resource_klass_for(type) - end - 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 caching(val = true) - @caching = val - end - - def _caching - @caching - end - - def caching? - if @caching.nil? - !JSONAPI.configuration.resource_cache.nil? && JSONAPI.configuration.default_caching - else - @caching && !JSONAPI.configuration.resource_cache.nil? - end - end - - def attribute_caching_context(_context) - nil - end - - # Generate a hashcode from the value to be used as part of the cache lookup - def hash_cache_field(value) - value.hash - end - - def _model_class - return nil if _abstract - - return @model_class if @model_class - - model_name = _model_name - return nil if model_name.to_s.blank? - - @model_class = model_name.to_s.safe_constantize - if @model_class.nil? - warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this is a base Resource declare it as abstract." - end - - @model_class - end - - def _allowed_filter?(filter) - !_allowed_filters[filter].nil? - end - - def _has_sort?(sorting) - !_allowed_sort[sorting.to_sym].nil? - end - - def module_path - if name == 'JSONAPI::Resource' - '' - else - name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : '' - end - end - - def default_sort - [{field: 'id', direction: :asc}] - end - - def construct_order_options(sort_params) - sort_params ||= default_sort - - return {} unless sort_params - - sort_params.each_with_object({}) do |sort, order_hash| - field = sort[:field].to_s == 'id' ? _primary_key : sort[:field].to_s - order_hash[field] = sort[:direction] - end - end - - def _add_relationship(klass, *attrs) - options = attrs.extract_options! - options[:parent_resource] = self - - attrs.each do |name| - relationship_name = name.to_sym - check_reserved_relationship_name(relationship_name) - check_duplicate_relationship_name(relationship_name) - - define_relationship_methods(relationship_name.to_sym, klass, options) - end - end - - # ResourceBuilder methods - def define_relationship_methods(relationship_name, relationship_klass, options) - relationship = register_relationship( - relationship_name, - relationship_klass.new(relationship_name, options) - ) - - define_foreign_key_setter(relationship) - end - - def define_foreign_key_setter(relationship) - if relationship.polymorphic? - define_on_resource "#{relationship.foreign_key}=" do |v| - _model.method("#{relationship.foreign_key}=").call(v[:id]) - _model.public_send("#{relationship.polymorphic_type}=", v[:type]) - end - else - define_on_resource "#{relationship.foreign_key}=" do |value| - _model.method("#{relationship.foreign_key}=").call(value) - end - end - end - - def define_on_resource(method_name, &block) - return if method_defined?(method_name) - define_method(method_name, block) - end - - def register_relationship(name, relationship_object) - @_relationships[name] = relationship_object - 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, :_cache_field, :cache_field].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 - - def check_duplicate_relationship_name(name) - if _relationships.include?(name.to_sym) - warn "[DUPLICATE RELATIONSHIP] `#{name}` has already been defined in #{_resource_name_from_type(_type)}." - end - end - - def check_duplicate_attribute_name(name) - if _attributes.include?(name.to_sym) - warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}." - end - end - end + class Resource < ActiveRelationResource + root_resource end -end +end \ No newline at end of file