diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 1343e1239..e00a46eac 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -6,6 +6,8 @@ class JsonApi < Base autoload :PaginationLinks autoload :FragmentCache autoload :Link + autoload :Association + autoload :ResourceIdentifier autoload :Deserialization # TODO: if we like this abstraction and other API objects to it, @@ -97,7 +99,7 @@ def resource_objects_for(serializers) end def process_resource(serializer, primary) - resource_identifier = resource_identifier_for(serializer) + resource_identifier = JsonApi::ResourceIdentifier.new(serializer).as_json return false unless @resource_identifiers.add?(resource_identifier) resource_object = resource_object_for(serializer) @@ -127,37 +129,13 @@ def process_relationship(serializer, include_tree) process_relationships(serializer, include_tree) end - def resource_identifier_type_for(serializer) - return serializer._type if serializer._type - if ActiveModelSerializers.config.jsonapi_resource_type == :singular - serializer.object.class.model_name.singular - else - serializer.object.class.model_name.plural - end - end - - def resource_identifier_id_for(serializer) - if serializer.respond_to?(:id) - serializer.id - else - serializer.object.id - end - end - - def resource_identifier_for(serializer) - type = resource_identifier_type_for(serializer) - id = resource_identifier_id_for(serializer) - - { id: id.to_s, type: type } - end - def attributes_for(serializer, fields) serializer.attributes(fields).except(:id) end def resource_object_for(serializer) resource_object = cache_check(serializer) do - resource_object = resource_identifier_for(serializer) + resource_object = JsonApi::ResourceIdentifier.new(serializer).as_json requested_fields = fieldset && fieldset.fields_for(resource_object[:type]) attributes = attributes_for(serializer, requested_fields) @@ -165,7 +143,8 @@ def resource_object_for(serializer) resource_object end - relationships = relationships_for(serializer) + requested_associations = fieldset.fields_for(resource_object[:type]) || '*' + relationships = relationships_for(serializer, requested_associations) resource_object[:relationships] = relationships if relationships.any? links = links_for(serializer) @@ -174,24 +153,15 @@ def resource_object_for(serializer) resource_object end - def relationship_value_for(serializer, options = {}) - if serializer.respond_to?(:each) - serializer.map { |s| resource_identifier_for(s) } - else - if options[:virtual_value] - options[:virtual_value] - elsif serializer && serializer.object - resource_identifier_for(serializer) - end - end - end - - def relationships_for(serializer) - resource_type = resource_identifier_type_for(serializer) - requested_associations = fieldset.fields_for(resource_type) || '*' + def relationships_for(serializer, requested_associations) include_tree = IncludeTree.from_include_args(requested_associations) serializer.associations(include_tree).each_with_object({}) do |association, hash| - hash[association.key] = { data: relationship_value_for(association.serializer, association.options) } + hash[association.key] = JsonApi::Association.new(serializer, + association.serializer, + association.options, + association.links, + association.meta) + .as_json end end diff --git a/lib/active_model/serializer/adapter/json_api/association.rb b/lib/active_model/serializer/adapter/json_api/association.rb new file mode 100644 index 000000000..b6cfc70dd --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/association.rb @@ -0,0 +1,48 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + class Association + def initialize(parent_serializer, serializer, options, links, meta) + @object = parent_serializer.object + @scope = parent_serializer.scope + + @options = options + @data = data_for(serializer, options) + @links = links + .map { |key, value| { key => Link.new(parent_serializer, value).as_json } } + .reduce({}, :merge) + @meta = meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta + end + + def as_json + hash = {} + hash[:data] = @data if @options[:include_data] + hash[:links] = @links if @links.any? + hash[:meta] = @meta if @meta + + hash + end + + protected + + attr_reader :object, :scope + + private + + def data_for(serializer, options) + if serializer.respond_to?(:each) + serializer.map { |s| ResourceIdentifier.new(s).as_json } + else + if options[:virtual_value] + options[:virtual_value] + elsif serializer && serializer.object + ResourceIdentifier.new(serializer).as_json + end + end + end + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json_api/resource_identifier.rb b/lib/active_model/serializer/adapter/json_api/resource_identifier.rb new file mode 100644 index 000000000..99bff2981 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/resource_identifier.rb @@ -0,0 +1,41 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + class ResourceIdentifier + def initialize(serializer) + @id = id_for(serializer) + @type = type_for(serializer) + end + + def as_json + { id: @id.to_s, type: @type } + end + + protected + + attr_reader :object, :scope + + private + + def type_for(serializer) + return serializer._type if serializer._type + if ActiveModelSerializers.config.jsonapi_resource_type == :singular + serializer.object.class.model_name.singular + else + serializer.object.class.model_name.plural + end + end + + def id_for(serializer) + if serializer.respond_to?(:id) + serializer.id + else + serializer.object.id + end + end + end + end + end + end +end diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb index 1003f0a6f..cbe167527 100644 --- a/lib/active_model/serializer/association.rb +++ b/lib/active_model/serializer/association.rb @@ -9,7 +9,7 @@ class Serializer # @example # Association.new(:comments, CommentSummarySerializer) # - Association = Struct.new(:name, :serializer, :options) do + Association = Struct.new(:name, :serializer, :options, :links, :meta) do # @return [Symbol] # def key diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index c0287b646..9e520c07e 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -34,6 +34,38 @@ class Serializer # So you can inspect reflections in your Adapters. # class Reflection < Field + def initialize(*) + super + @_links = {} + @_include_data = true + end + + def link(name, value = nil, &block) + @_links[name] = block || value + nil + end + + def meta(value = nil, &block) + @_meta = block || value + nil + end + + def include_data(value = true) + @_include_data = value + nil + end + + def value(serializer) + @object = serializer.object + @scope = serializer.scope + + if block + instance_eval(&block) + else + serializer.read_attribute_for_serialization(name) + end + end + # Build association. This method is used internally to # build serializer's association by its reflection. # @@ -59,6 +91,7 @@ def build_association(subject, parent_serializer_options) association_value = value(subject) reflection_options = options.dup serializer_class = subject.class.serializer_for(association_value, reflection_options) + reflection_options[:include_data] = _include_data if serializer_class begin @@ -73,9 +106,13 @@ def build_association(subject, parent_serializer_options) reflection_options[:virtual_value] = association_value end - Association.new(name, serializer, reflection_options) + Association.new(name, serializer, reflection_options, _links, _meta) end + protected + + attr_accessor :object, :scope, :_links, :_meta, :_include_data + private def serializer_options(subject, parent_serializer_options, reflection_options) diff --git a/test/adapter/json_api/links_test.rb b/test/adapter/json_api/links_test.rb index dbda88ea0..11873ac44 100644 --- a/test/adapter/json_api/links_test.rb +++ b/test/adapter/json_api/links_test.rb @@ -7,6 +7,8 @@ class JsonApi class LinksTest < ActiveSupport::TestCase LinkAuthor = Class.new(::Model) class LinkAuthorSerializer < ActiveModel::Serializer + type 'author' + link :self do href "//example.com/link_author/#{object.id}" meta stuff: 'value' @@ -17,11 +19,23 @@ class LinkAuthorSerializer < ActiveModel::Serializer link :yet_another do "//example.com/resource/#{object.id}" end + + has_many :posts do + link :self do + href '//example.com/link_author/relationships/posts' + meta stuff: 'value' + end + link :related do + href '//example.com/link_author/posts' + meta count: object.posts.count + end + include_data false + end end def setup @post = Post.new(id: 1337, comments: [], author: nil) - @author = LinkAuthor.new(id: 1337) + @author = LinkAuthor.new(id: 1337, posts: [@post]) end def test_toplevel_links @@ -61,6 +75,23 @@ def test_resource_links } assert_equal(expected, hash[:data][:links]) end + + def test_relationship_links + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + links: { + self: { + href: '//example.com/link_author/relationships/posts', + meta: { stuff: 'value' } + }, + related: { + href: '//example.com/link_author/posts', + meta: { count: 1 } + } + } + } + assert_equal(expected, hash[:data][:relationships][:posts]) + end end end end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index aa0cae085..f62da8b81 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -32,13 +32,13 @@ def test_has_many_and_has_one case key when :posts - assert_equal({}, options) + assert_equal({ include_data: true }, options) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) when :bio - assert_equal({}, options) + assert_equal({ include_data: true }, options) assert_nil serializer when :roles - assert_equal({}, options) + assert_equal({ include_data: true }, options) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) else flunk "Unknown association: #{key}" @@ -80,7 +80,7 @@ def test_belongs_to flunk "Unknown association: #{key}" end - assert_equal({}, association.options) + assert_equal({ include_data: true }, association.options) end end