lib/jsonapi/resource.rb in jsonapi-resources-0.4.2 vs lib/jsonapi/resource.rb in jsonapi-resources-0.4.3

- old
+ new

@@ -4,21 +4,22 @@ class Resource include Callbacks @@resource_types = {} - attr :context + attr_reader :context attr_reader :model define_jsonapi_resources_callbacks :create, :update, :remove, :save, :create_has_many_link, :replace_has_many_links, :create_has_one_link, :replace_has_one_link, + :replace_polymorphic_has_one_link, :remove_has_many_link, :remove_has_one_link, :replace_fields def initialize(model, context = nil) @@ -46,13 +47,11 @@ @changing = true run_callbacks callback do completed = (yield == :completed) end - if @save_needed || is_new? - completed = (save == :completed) - end + completed = (save == :completed) if @save_needed || is_new? end end return completed ? :completed : :accepted end @@ -79,10 +78,16 @@ change :replace_has_one_link do _replace_has_one_link(association_type, association_key_value) end end + def replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type) + change :replace_polymorphic_has_one_link do + _replace_polymorphic_has_one_link(association_type, association_key_value, association_key_type) + end + end + def remove_has_many_link(association_type, key) change :remove_has_many_link do _remove_has_many_link(association_type, key) end end @@ -104,15 +109,16 @@ 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(association_name, options = {}) + def records_for(association_name, _options = {}) model.send association_name end private + def save run_callbacks :save do _save end end @@ -130,84 +136,93 @@ # return :accepted # end # ``` def _save unless @model.valid? - raise JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages) + fail JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages) end if defined? @model.save saved = @model.save - unless saved - raise JSONAPI::Exceptions::SaveFailed.new - end + fail JSONAPI::Exceptions::SaveFailed.new unless saved else saved = true end @save_needed = !saved - return :completed + :completed end def _remove @model.destroy - return :completed + :completed end def _create_has_many_links(association_type, association_key_values) association = self.class._associations[association_type] association_key_values.each do |association_key_value| - related_resource = Resource.resource_for(self.class.module_path + association.type.to_s).find_by_key(association_key_value, context: @context) + related_resource = association.resource_klass.find_by_key(association_key_value, context: @context) - # ToDo: Add option to skip relations that already exist instead of returning an error? + # TODO: Add option to skip relations that already exist instead of returning an error? relation = @model.send(association.type).where(association.primary_key => association_key_value).first if relation.nil? @model.send(association.type) << related_resource.model else - raise JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value) + fail JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value) end end - return :completed + :completed end def _replace_has_many_links(association_type, association_key_values) association = self.class._associations[association_type] send("#{association.foreign_key}=", association_key_values) @save_needed = true - return :completed + :completed end def _replace_has_one_link(association_type, association_key_value) association = self.class._associations[association_type] send("#{association.foreign_key}=", association_key_value) @save_needed = true - return :completed + :completed end + def _replace_polymorphic_has_one_link(association_type, key_value, key_type) + association = self.class._associations[association_type.to_sym] + + model.send("#{association.foreign_key}=", key_value) + model.send("#{association.polymorphic_type}=", key_type.to_s.classify) + + @save_needed = true + + :completed + end + def _remove_has_many_link(association_type, key) association = self.class._associations[association_type] @model.send(association.type).delete(key) - return :completed + :completed end def _remove_has_one_link(association_type) association = self.class._associations[association_type] send("#{association.foreign_key}=", nil) @save_needed = true - return :completed + :completed end def _replace_fields(field_data) field_data[:attributes].each do |attribute, value| begin @@ -222,19 +237,24 @@ field_data[:has_one].each do |association_type, value| if value.nil? remove_has_one_link(association_type) else - replace_has_one_link(association_type, value) + case value + when Hash + replace_polymorphic_has_one_link(association_type.to_s, value.fetch(:id), value.fetch(:type)) + else + replace_has_one_link(association_type, value) + end end end if field_data[:has_one] field_data[:has_many].each do |association_type, values| replace_has_many_links(association_type, values) end if field_data[:has_many] - return :completed + :completed end class << self def inherited(base) base._attributes = (_attributes || {}).dup @@ -251,19 +271,19 @@ def resource_for(type) resource_name = JSONAPI::Resource._resource_name_from_type(type) resource = resource_name.safe_constantize if resource_name if resource.nil? - raise NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)" + fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)" end resource end - attr_accessor :_attributes, :_associations, :_allowed_filters , :_type, :_paginator + attr_accessor :_attributes, :_associations, :_allowed_filters, :_type, :_paginator def create(context) - self.new(self.create_model, context) + new(create_model, context) end def create_model _model_class.new end @@ -301,11 +321,11 @@ @model.send "#{attr}=", value end unless method_defined?("#{attr}=") end def default_attribute_options - {format: :default} + { format: :default } end def has_one(*attrs) _associate(Association::HasOne, *attrs) end @@ -317,11 +337,11 @@ def model_name(model) @_model_name = model.to_sym end def filters(*attrs) - @_allowed_filters.merge!(attrs.inject( Hash.new ) { |h, attr| h[attr] = {}; h }) + @_allowed_filters.merge!(attrs.inject({}) { |h, attr| h[attr] = {}; h }) end def filter(attr, *args) @_allowed_filters[attr.to_sym] = args.extract_options! end @@ -332,49 +352,72 @@ # 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") - self.creatable_fields(*args) + 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") - self.updatable_fields(*args) + 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) + def updatable_fields(_context = nil) _updatable_associations | _attributes.keys - [:id] end # Override in your resource to filter the creatable keys - def creatable_fields(context = nil) + def creatable_fields(_context = nil) _updatable_associations | _attributes.keys end # Override in your resource to filter the sortable keys - def sortable_fields(context = nil) + def sortable_fields(_context = nil) _attributes.keys end def fields _associations.keys | _attributes.keys end - def apply_includes(records, directives) - records = records.includes(*directives.model_includes) if directives + def resolve_association_names_to_relations(resource_klass, model_includes, options = {}) + case model_includes + when Array + return model_includes.map do |value| + resolve_association_names_to_relations(resource_klass, value, options) + end + when Hash + model_includes.keys.each do |key| + association = resource_klass._associations[key] + value = model_includes[key] + model_includes.delete(key) + model_includes[association.relation_name(options)] = resolve_association_names_to_relations(association.resource_klass, value, options) + end + return model_includes + when Symbol + association = resource_klass._associations[model_includes] + return association.relation_name(options) + end + end + + def apply_includes(records, options = {}) + include_directives = options[:include_directives] + if include_directives + model_includes = resolve_association_names_to_relations(self, include_directives.model_includes, options) + records = records.includes(model_includes) + end + records end def apply_pagination(records, paginator, order_options) - if paginator - records = paginator.apply(records, order_options) - end + records = paginator.apply(records, order_options) if paginator records end def apply_sort(records, order_options) if order_options.any? @@ -382,22 +425,22 @@ else records end end - def apply_filter(records, filter, value, options = {}) + def apply_filter(records, filter, value, _options = {}) records.where(filter => value) end def apply_filters(records, filters, options = {}) required_includes = [] if filters filters.each do |filter, value| if _associations.include?(filter) if _associations[filter].is_a?(JSONAPI::Association::HasMany) - required_includes.push(filter) + required_includes.push(filter.to_s) records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options) else records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options) end else @@ -405,24 +448,20 @@ end end end if required_includes.any? - records.includes(required_includes) - elsif records.respond_to? :to_ary - records - else - records.all + records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes))) end + + records end def filter_records(filters, options) - include_directives = options[:include_directives] - records = records(options) - records = apply_includes(records, include_directives) - apply_filters(records, filters, options) + records = apply_filters(records, filters, options) + apply_includes(records, options) end def sort_records(records, order_options) apply_sort(records, order_options) end @@ -443,31 +482,28 @@ records = apply_pagination(records, options[:paginator], order_options) resources = [] records.each do |model| - resources.push self.new(model, context) + resources.push new(model, context) end - return resources + resources end def find_by_key(key, options = {}) context = options[:context] - include_directives = options[:include_directives] records = records(options) - records = apply_includes(records, include_directives) + records = apply_includes(records, options) model = records.where({_primary_key => key}).first - if model.nil? - raise JSONAPI::Exceptions::RecordNotFound.new(key) - end - self.new(model, context) + fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil? + new(model, context) end # Override this method if you want to customize the relation for # finder methods (find, find_by_key) - def records(options = {}) + def records(_options = {}) _model_class end def verify_filters(filters, context = nil) verified_filters = {} @@ -492,11 +528,11 @@ verify_custom_filter(filter, filter_values, context) end end # override to allow for key processing and checking - def verify_key(key, context = nil) + def verify_key(key, _context = nil) key && Integer(key) rescue raise JSONAPI::Exceptions::InvalidFieldValue.new(:id, key) end @@ -506,40 +542,40 @@ verify_key(key, context) end end # override to allow for custom filters - def verify_custom_filter(filter, value, context = nil) - return filter, value + def verify_custom_filter(filter, value, _context = nil) + [filter, value] end # override to allow for custom association logic, such as uuids, multiple keys or permission checks on keys - def verify_association_filter(filter, raw, context = nil) - return filter, raw + def verify_association_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_associations - @_associations.map { |key, association| key } + @_associations.map { |key, _association| key } end def _has_association?(type) type = type.to_s - @_associations.has_key?(type.singularize.to_sym) || @_associations.has_key?(type.pluralize.to_sym) + @_associations.key?(type.singularize.to_sym) || @_associations.key?(type.pluralize.to_sym) end def _association(type) type = type.to_sym @_associations[type] end def _model_name - @_model_name ||= self.name.demodulize.sub(/Resource$/, '') + @_model_name ||= name.demodulize.sub(/Resource$/, '') end def _primary_key @_primary_key ||= :id end @@ -547,17 +583,17 @@ def _as_parent_key @_as_parent_key ||= "#{_type.to_s.singularize}_#{_primary_key}" end def _allowed_filters - !@_allowed_filters.nil? ? @_allowed_filters : { :id => {} } + !@_allowed_filters.nil? ? @_allowed_filters : { id: {} } end def _resource_name_from_type(type) class_name = @@resource_types[type] if class_name.nil? - class_name = "#{type.to_s.singularize}_resource".camelize + class_name = "#{type.to_s.underscore.singularize}_resource".camelize @@resource_types[type] = class_name end return class_name end @@ -576,23 +612,24 @@ def _allowed_filter?(filter) !_allowed_filters[filter].nil? end def module_path - @module_path ||= self.name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : '' + @module_path ||= name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').downcase : '' end def construct_order_options(sort_params) return {} unless sort_params - sort_params.each_with_object({}) { |sort, order_hash| + sort_params.each_with_object({}) do |sort, order_hash| field = sort[:field] == 'id' ? _primary_key : sort[:field] order_hash[field] = sort[:direction] - } + 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 @@ -616,65 +653,81 @@ options = attrs.extract_options! options[:module_path] = module_path attrs.each do |attr| check_reserved_association_name(attr) + @_associations[attr] = association = klass.new(attr, options) - @_associations[attr] = klass.new(attr, options) + associated_records_method_name = case association + when JSONAPI::Association::HasOne then "record_for_#{attr}" + when JSONAPI::Association::HasMany then "records_for_#{attr}" + end - foreign_key = @_associations[attr].foreign_key + foreign_key = association.foreign_key - define_method foreign_key do - @model.method(foreign_key).call - end unless method_defined?(foreign_key) - define_method "#{foreign_key}=" do |value| @model.method("#{foreign_key}=").call(value) end unless method_defined?("#{foreign_key}=") - associated_records_method_name = case @_associations[attr] - when JSONAPI::Association::HasOne then "record_for_#{attr}" - when JSONAPI::Association::HasMany then "records_for_#{attr}" - end - - define_method associated_records_method_name do |options={}| - records_for(attr, options) + define_method associated_records_method_name do |options = {}| + relation_name = association.relation_name(options.merge({context: @context})) + records_for(relation_name, options) end unless method_defined?(associated_records_method_name) - if @_associations[attr].is_a?(JSONAPI::Association::HasOne) - define_method attr do - type_name = self.class._associations[attr].type.to_s - resource_class = Resource.resource_for(self.class.module_path + type_name) - if resource_class + if association.is_a?(JSONAPI::Association::HasOne) + define_method foreign_key do + @model.method(foreign_key).call + end unless method_defined?(foreign_key) + + define_method attr do |options = {}| + if association.polymorphic? associated_model = public_send(associated_records_method_name) - return associated_model ? resource_class.new(associated_model, @context) : nil + resource_klass = Resource.resource_for(self.class.module_path + associated_model.class.to_s.underscore) if associated_model + return resource_klass.new(associated_model, @context) if resource_klass + else + resource_klass = association.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?(attr) - elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany) + elsif association.is_a?(JSONAPI::Association::HasMany) + define_method foreign_key do + records = public_send(associated_records_method_name) + return records.collect do |record| + record.send(association.resource_klass._primary_key) + end + end unless method_defined?(foreign_key) define_method attr do |options = {}| - type_name = self.class._associations[attr].type.to_s - resource_class = Resource.resource_for(self.class.module_path + type_name) + resource_klass = association.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 = self.class.construct_order_options(sort_criteria) + records = resource_klass.apply_sort(records, order_options) + end + paginator = options[:paginator] + if paginator + records = resource_klass.apply_pagination(records, paginator, order_options) + end - resources = [] - - if resource_class - records = public_send(associated_records_method_name) - records = resource_class.apply_filters(records, filters, options) - order_options = self.class.construct_order_options(sort_criteria) - records = resource_class.apply_sort(records, order_options) - records = resource_class.apply_pagination(records, paginator, order_options) - records.each do |record| - resources.push resource_class.new(record, @context) + return records.collect do |record| + if association.polymorphic? + resource_klass = Resource.resource_for(self.class.module_path + record.class.to_s.underscore) end + resource_klass.new(record, @context) end - return resources end unless method_defined?(attr) end end end end - end end