lib/jsonapi/resource.rb in jsonapi-resources-0.4.4 vs lib/jsonapi/resource.rb in jsonapi-resources-0.5.0

- old
+ new

@@ -11,26 +11,26 @@ 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, + :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 = nil) @model = model @context = context end def id - model.send(self.class._primary_key) + model.public_send(self.class._primary_key) end def is_new? id.nil? end @@ -60,43 +60,43 @@ run_callbacks :remove do _remove end end - def create_has_many_links(association_type, association_key_values) - change :create_has_many_link do - _create_has_many_links(association_type, association_key_values) + 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_has_many_links(association_type, association_key_values) - change :replace_has_many_links do - _replace_has_many_links(association_type, association_key_values) + 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_has_one_link(association_type, association_key_value) - change :replace_has_one_link do - _replace_has_one_link(association_type, association_key_value) + 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_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) + 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_has_many_link(association_type, key) - change :remove_has_many_link do - _remove_has_many_link(association_type, key) + 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_has_one_link(association_type) - change :remove_has_one_link do - _remove_has_one_link(association_type) + 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 @@ -109,12 +109,12 @@ 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 = {}) - model.send association_name + def records_for(relationship_name, _options = {}) + model.public_send relationship_name end private def save @@ -136,11 +136,11 @@ # return :accepted # end # ``` def _save unless @model.valid? - fail JSONAPI::Exceptions::ValidationErrors.new(@model.errors.messages) + fail JSONAPI::Exceptions::ValidationErrors.new(self) end if defined? @model.save saved = @model.save fail JSONAPI::Exceptions::SaveFailed.new unless saved @@ -157,69 +157,69 @@ @model.destroy :completed end - def _create_has_many_links(association_type, association_key_values) - association = self.class._associations[association_type] + def _create_to_many_links(relationship_type, relationship_key_values) + relationship = self.class._relationships[relationship_type] - association_key_values.each do |association_key_value| - related_resource = association.resource_klass.find_by_key(association_key_value, context: @context) + relationship_key_values.each do |relationship_key_value| + related_resource = relationship.resource_klass.find_by_key(relationship_key_value, context: @context) # 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 + relation = @model.public_send(relationship.type).where(relationship.primary_key => relationship_key_value).first if relation.nil? - @model.send(association.type) << related_resource.model + @model.public_send(relationship.type) << related_resource.model else - fail JSONAPI::Exceptions::HasManyRelationExists.new(association_key_value) + fail JSONAPI::Exceptions::HasManyRelationExists.new(relationship_key_value) end end :completed end - def _replace_has_many_links(association_type, association_key_values) - association = self.class._associations[association_type] + def _replace_to_many_links(relationship_type, relationship_key_values) + relationship = self.class._relationships[relationship_type] - send("#{association.foreign_key}=", association_key_values) + send("#{relationship.foreign_key}=", relationship_key_values) @save_needed = true :completed end - def _replace_has_one_link(association_type, association_key_value) - association = self.class._associations[association_type] + def _replace_to_one_link(relationship_type, relationship_key_value) + relationship = self.class._relationships[relationship_type] - send("#{association.foreign_key}=", association_key_value) + send("#{relationship.foreign_key}=", relationship_key_value) @save_needed = true :completed end - def _replace_polymorphic_has_one_link(association_type, key_value, key_type) - association = self.class._associations[association_type.to_sym] + def _replace_polymorphic_to_one_link(relationship_type, key_value, key_type) + relationship = self.class._relationships[relationship_type.to_sym] - model.send("#{association.foreign_key}=", key_value) - model.send("#{association.polymorphic_type}=", key_type.to_s.classify) + 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_has_many_link(association_type, key) - association = self.class._associations[association_type] + def _remove_to_many_link(relationship_type, key) + relationship = self.class._relationships[relationship_type] - @model.send(association.type).delete(key) + @model.public_send(relationship.type).delete(key) :completed end - def _remove_has_one_link(association_type) - association = self.class._associations[association_type] + def _remove_to_one_link(relationship_type) + relationship = self.class._relationships[relationship_type] - send("#{association.foreign_key}=", nil) + send("#{relationship.foreign_key}=", nil) @save_needed = true :completed end @@ -233,34 +233,34 @@ raise JSONAPI::Exceptions::InvalidFieldValue.new(attribute, value) # :nocov: end end - field_data[:has_one].each do |association_type, value| + field_data[:to_one].each do |relationship_type, value| if value.nil? - remove_has_one_link(association_type) + remove_to_one_link(relationship_type) else case value when Hash - replace_polymorphic_has_one_link(association_type.to_s, value.fetch(:id), value.fetch(:type)) + replace_polymorphic_to_one_link(relationship_type.to_s, value.fetch(:id), value.fetch(:type)) else - replace_has_one_link(association_type, value) + replace_to_one_link(relationship_type, value) end end - end if field_data[:has_one] + end if field_data[:to_one] - field_data[:has_many].each do |association_type, values| - replace_has_many_links(association_type, values) - end if field_data[:has_many] + 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(base) base._attributes = (_attributes || {}).dup - base._associations = (_associations || {}).dup + base._relationships = (_relationships || {}).dup base._allowed_filters = (_allowed_filters || Set.new).dup type = base.name.demodulize.sub(/Resource$/, '').underscore base._type = type.pluralize.to_sym @@ -276,11 +276,11 @@ 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, :_relationships, :_allowed_filters, :_type, :_paginator def create(context) new(create_model, context) end @@ -312,28 +312,43 @@ end @_attributes ||= {} @_attributes[attr] = options define_method attr do - @model.send(attr) + @model.public_send(attr) end unless method_defined?(attr) define_method "#{attr}=" do |value| - @model.send "#{attr}=", 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) - _associate(Association::HasOne, *attrs) + _add_relationship(Relationship::ToOne, *attrs) end def has_many(*attrs) - _associate(Association::HasMany, *attrs) + _add_relationship(Relationship::ToMany, *attrs) end def model_name(model) @_model_name = model.to_sym end @@ -365,51 +380,51 @@ end # :nocov: # Override in your resource to filter the updatable keys def updatable_fields(_context = nil) - _updatable_associations | _attributes.keys - [:id] + _updatable_relationships | _attributes.keys - [:id] end # Override in your resource to filter the creatable keys def creatable_fields(_context = nil) - _updatable_associations | _attributes.keys + _updatable_relationships | _attributes.keys end # Override in your resource to filter the sortable keys def sortable_fields(_context = nil) _attributes.keys end def fields - _associations.keys | _attributes.keys + _relationships.keys | _attributes.keys end - def resolve_association_names_to_relations(resource_klass, model_includes, options = {}) + def resolve_relationship_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) + resolve_relationship_names_to_relations(resource_klass, value, options) end when Hash model_includes.keys.each do |key| - association = resource_klass._associations[key] + relationship = resource_klass._relationships[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) + model_includes[relationship.relation_name(options)] = resolve_relationship_names_to_relations(relationship.resource_klass, value, options) end return model_includes when Symbol - association = resource_klass._associations[model_includes] - return association.relation_name(options) + 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_association_names_to_relations(self, include_directives.model_includes, options) + model_includes = resolve_relationship_names_to_relations(self, include_directives.model_includes, options) records = records.includes(model_includes) end records end @@ -434,16 +449,16 @@ 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) + if _relationships.include?(filter) + if _relationships[filter].is_a?(JSONAPI::Relationship::ToMany) required_includes.push(filter.to_s) - records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options) + records = apply_filter(records, "#{filter}.#{_relationships[filter].primary_key}", value, options) else - records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options) + records = apply_filter(records, "#{_relationships[filter].foreign_key}", value, options) end else records = apply_filter(records, filter, value, options) end end @@ -512,20 +527,20 @@ verified_filters[verified_filter[0]] = verified_filter[1] end verified_filters end - def is_filter_association?(filter) - filter == _type || _associations.include?(filter) + def is_filter_relationship?(filter) + filter == _type || _relationships.include?(filter) end def verify_filter(filter, raw, context = nil) filter_values = [] filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty? - if is_filter_association?(filter) - verify_association_filter(filter, filter_values, context) + if is_filter_relationship?(filter) + verify_relationship_filter(filter, filter_values, context) else verify_custom_filter(filter, filter_values, context) end end @@ -546,40 +561,40 @@ # override to allow for custom filters 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) + # override 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_associations - @_associations.map { |key, _association| key } + def _updatable_relationships + @_relationships.map { |key, _relationship| key } end - def _has_association?(type) + def _has_relationship?(type) type = type.to_s - @_associations.key?(type.singularize.to_sym) || @_associations.key?(type.pluralize.to_sym) + @_relationships.key?(type.singularize.to_sym) || @_relationships.key?(type.pluralize.to_sym) end - def _association(type) + def _relationship(type) type = type.to_sym - @_associations[type] + @_relationships[type] end def _model_name @_model_name ||= name.demodulize.sub(/Resource$/, '') end def _primary_key - @_primary_key ||= :id + @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id end def _as_parent_key @_as_parent_key ||= "#{_type.to_s.singularize}_#{_primary_key}" end @@ -636,72 +651,89 @@ 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, :href, :links].include?(name.to_sym) + if [:type, :href, :links, :model].include?(name.to_sym) warn "[NAME COLLISION] `#{name}` is a reserved key in #{@@resource_types[_type]}." end end - def check_reserved_association_name(name) + def check_reserved_relationship_name(name) if [:id, :ids, :type, :types, :href, :hrefs, :link, :links].include?(name.to_sym) - warn "[NAME COLLISION] `#{name}` is a reserved association name in #{@@resource_types[_type]}." + warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{@@resource_types[_type]}." end end - def _associate(klass, *attrs) + def _add_relationship(klass, *attrs) 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) + check_reserved_relationship_name(attr) + @_relationships[attr] = relationship = 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}" + associated_records_method_name = case relationship + when JSONAPI::Relationship::ToOne then "record_for_#{attr}" + when JSONAPI::Relationship::ToMany then "records_for_#{attr}" end - foreign_key = association.foreign_key + 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 |options = {}| - relation_name = association.relation_name(options.merge({context: @context})) + options = options.merge({context: @context}) + relation_name = relationship.relation_name(options) records_for(relation_name, options) end unless method_defined?(associated_records_method_name) - if association.is_a?(JSONAPI::Association::HasOne) - define_method foreign_key do - @model.method(foreign_key).call - end unless method_defined?(foreign_key) + 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 attr do |options = {}| - if association.polymorphic? - associated_model = public_send(associated_records_method_name) - 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 + define_method attr do |options = {}| + if relationship.polymorphic? + associated_model = public_send(associated_records_method_name) + 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 = 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?(attr) + else + define_method foreign_key do + 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 attr do |options = {}| + 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?(attr) - elsif association.is_a?(JSONAPI::Association::HasMany) + end unless method_defined?(attr) + 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.send(association.resource_klass._primary_key) + record.public_send(relationship.resource_klass._primary_key) end end unless method_defined?(foreign_key) define_method attr do |options = {}| - resource_klass = association.resource_klass + 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) @@ -717,10 +749,10 @@ if paginator records = resource_klass.apply_pagination(records, paginator, order_options) end return records.collect do |record| - if association.polymorphic? + if relationship.polymorphic? resource_klass = Resource.resource_for(self.class.module_path + record.class.to_s.underscore) end resource_klass.new(record, @context) end end unless method_defined?(attr)