diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 2f55d450b..b513216fa 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -50,6 +50,9 @@ def self.digest_caller_file(caller_line) self._attributes ||= [] class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute self._attributes_keys ||= {} + class_attribute :_links # @api private : links definitions, @see Serializer#link + self._links ||= {} + serializer.class_attribute :_cache # @api private : the cache object serializer.class_attribute :_fragmented # @api private : @see ::fragmented serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key @@ -72,6 +75,7 @@ def self.inherited(base) caller_line = caller.first base._attributes = _attributes.dup base._attributes_keys = _attributes_keys.dup + base._links = _links.dup base._cache_digest = digest_caller_file(caller_line) super end @@ -83,6 +87,10 @@ def self.type(type) self._type = type end + def self.link(name, value = nil, &block) + _links[name] = block || value + end + # @example # class AdminAuthorSerializer < ActiveModel::Serializer # attributes :id, :name, :recent_edits @@ -249,6 +257,12 @@ def attributes(requested_attrs = nil) end end + # @api private + # Used by JsonApi adapter to build resource links. + def links + self.class._links + end + protected attr_accessor :instance_options diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 962c95be5..8eafe166f 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -5,6 +5,7 @@ class JsonApi < Base extend ActiveSupport::Autoload autoload :PaginationLinks autoload :FragmentCache + autoload :Link # TODO: if we like this abstraction and other API objects to it, # then extract to its own file and require it. @@ -94,16 +95,15 @@ def serializable_hash_for_collection(options) if serializer.paginated? hash[:links] ||= {} - hash[:links].update(links_for(serializer, options)) + hash[:links].update(pagination_links_for(serializer, options)) end hash end def serializable_hash_for_single_resource - primary_data = primary_data_for(serializer) - relationships = relationships_for(serializer) - primary_data[:relationships] = relationships if relationships.any? + primary_data = resource_object_for(serializer) + hash = { data: primary_data } included = included_resources(@include_tree, [primary_data]) @@ -136,22 +136,27 @@ def resource_identifier_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) - cache_check(serializer) do + resource_object = cache_check(serializer) do resource_object = resource_identifier_for(serializer) + requested_fields = fieldset && fieldset.fields_for(resource_object[:type]) - attributes = serializer.attributes(requested_fields).except(:id) + attributes = attributes_for(serializer, requested_fields) resource_object[:attributes] = attributes if attributes.any? resource_object end - end - def primary_data_for(serializer) - if serializer.respond_to?(:each) - serializer.map { |s| resource_object_for(s) } - else - resource_object_for(serializer) - end + relationships = relationships_for(serializer) + resource_object[:relationships] = relationships if relationships.any? + + links = links_for(serializer) + resource_object[:links] = links if links.any? + + resource_object end def relationship_value_for(serializer, options = {}) @@ -191,9 +196,7 @@ def add_included_resources_for(serializer, include_tree, primary_data, included) else return unless serializer && serializer.object - resource_object = primary_data_for(serializer) - relationships = relationships_for(serializer) - resource_object[:relationships] = relationships if relationships.any? + resource_object = resource_object_for(serializer) return if included.include?(resource_object) || primary_data.include?(resource_object) included.push(resource_object) @@ -204,7 +207,21 @@ def add_included_resources_for(serializer, include_tree, primary_data, included) end end - def links_for(serializer, options) + def links_for(serializer) + serializer.links.each_with_object({}) do |(name, value), hash| + hash[name] = + if value.respond_to?(:call) + link = Link.new(serializer) + link.instance_eval(&value) + + link.to_hash + else + value + end + end + end + + def pagination_links_for(serializer, options) JsonApi::PaginationLinks.new(serializer.object, options[:context]).serializable_hash(options) end end diff --git a/lib/active_model/serializer/adapter/json_api/link.rb b/lib/active_model/serializer/adapter/json_api/link.rb new file mode 100644 index 000000000..45ce89609 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/link.rb @@ -0,0 +1,34 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + class Link + def initialize(serializer) + @object = serializer.object + @scope = serializer.scope + end + + def href(value) + self._href = value + end + + def meta(value) + self._meta = value + end + + def to_hash + hash = { href: _href } + hash.merge!(meta: _meta) if _meta + + hash + end + + protected + + attr_accessor :_href, :_meta + attr_reader :object, :scope + end + end + end + end +end diff --git a/test/adapter/json_api/links_test.rb b/test/adapter/json_api/links_test.rb index d3b56ee9a..85e62e92e 100644 --- a/test/adapter/json_api/links_test.rb +++ b/test/adapter/json_api/links_test.rb @@ -5,8 +5,19 @@ class Serializer module Adapter class JsonApi class LinksTest < Minitest::Test + LinkAuthor = Class.new(::Model) + class LinkAuthorSerializer < ActiveModel::Serializer + link :self do + href "//example.com/link_author/#{object.id}" + meta stuff: 'value' + end + + link :other, '//example.com/resource' + end + def setup @post = Post.new(id: 1337, comments: [], author: nil) + @author = LinkAuthor.new(id: 1337) end def test_toplevel_links @@ -15,16 +26,36 @@ def test_toplevel_links adapter: :json_api, links: { self: { - href: '//posts' + href: '//example.com/posts', + meta: { + stuff: 'value' + } } }).serializable_hash expected = { self: { - href: '//posts' + href: '//example.com/posts', + meta: { + stuff: 'value' + } } } assert_equal(expected, hash[:links]) end + + def test_resource_links + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + self: { + href: '//example.com/link_author/1337', + meta: { + stuff: 'value' + } + }, + other: '//example.com/resource' + } + assert_equal(expected, hash[:data][:links]) + end end end end