diff --git a/CHANGELOG.md b/CHANGELOG.md index b6a852b06..ab451b60c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,19 @@ ## 0.10.x Breaking changes: + Features: +- [#1504](https://github.com/rails-api/active_model_serializers/pull/1504) Adds the changes missing from #1454 + and add more tests for resource identifier and relationship objects. (@groyoh) - [#1018](https://github.com/rails-api/active_model_serializers/pull/1018) Add more tests and docs for top-level links. (@leandrocp) - [#1454](https://github.com/rails-api/active_model_serializers/pull/1454) Add support for relationship-level links and meta attributes. (@beauby) - [#1340](https://github.com/rails-api/active_model_serializers/pull/1340) Add support for resource-level meta. (@beauby) + Fixes: - [#1501](https://github.com/rails-api/active_model_serializers/pull/1501) Adds tests for SerializableResource::use_adapter?,doc typos (@domitian) - [#1488](https://github.com/rails-api/active_model_serializers/pull/1488) Require ActiveSupport's string inflections (@nate00) + Misc: ### v0.10.0.rc4 (2016/01/27 11:00 +00:00) diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index a41571933..cfe4e04dc 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -6,10 +6,9 @@ class JsonApi < Base autoload :PaginationLinks autoload :FragmentCache autoload :Link - autoload :Association - autoload :ResourceIdentifier autoload :Meta autoload :Deserialization + require 'active_model/serializer/adapter/json_api/api_objects' # TODO: if we like this abstraction and other API objects to it, # then extract to its own file and require it. @@ -100,7 +99,7 @@ def resource_objects_for(serializers) end def process_resource(serializer, primary) - resource_identifier = JsonApi::ResourceIdentifier.new(serializer).as_json + resource_identifier = ApiObjects::ResourceIdentifier.new(serializer).as_json return false unless @resource_identifiers.add?(resource_identifier) resource_object = resource_object_for(serializer) @@ -136,7 +135,7 @@ def attributes_for(serializer, fields) def resource_object_for(serializer) resource_object = cache_check(serializer) do - resource_object = JsonApi::ResourceIdentifier.new(serializer).as_json + resource_object = ApiObjects::ResourceIdentifier.new(serializer).as_json requested_fields = fieldset && fieldset.fields_for(resource_object[:type]) attributes = attributes_for(serializer, requested_fields) @@ -160,12 +159,13 @@ def resource_object_for(serializer) 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] = JsonApi::Association.new(serializer, + hash[association.key] = ApiObjects::Relationship.new( + serializer, association.serializer, association.options, association.links, - association.meta) - .as_json + association.meta + ).as_json end end diff --git a/lib/active_model/serializer/adapter/json_api/api_objects.rb b/lib/active_model/serializer/adapter/json_api/api_objects.rb new file mode 100644 index 000000000..bad3173c3 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/api_objects.rb @@ -0,0 +1,13 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + extend ActiveSupport::Autoload + autoload :Relationship + autoload :ResourceIdentifier + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb b/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb new file mode 100644 index 000000000..d1ebc1b96 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb @@ -0,0 +1,52 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class Relationship + def initialize(parent_serializer, serializer, options = {}, links = {}, meta = nil) + @object = parent_serializer.object + @scope = parent_serializer.scope + + @options = options + @data = data_for(serializer, options) + @links = links.each_with_object({}) do |(key, value), hash| + hash[key] = Link.new(parent_serializer, value).as_json + end + @meta = meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta + end + + def as_json + hash = {} + hash[:data] = data if options[:include_data] + links = self.links + hash[:links] = links if links.any? + meta = self.meta + hash[:meta] = meta if meta + + hash + end + + protected + + attr_reader :object, :scope, :data, :options, :links, :meta + + 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 +end diff --git a/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb b/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb new file mode 100644 index 000000000..058f06031 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb @@ -0,0 +1,39 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class ResourceIdentifier + def initialize(serializer) + @id = id_for(serializer) + @type = type_for(serializer) + end + + def as_json + { id: id, type: type } + end + + protected + + attr_reader :id, :type + + 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) + serializer.read_attribute_for_serialization(:id).to_s + end + end + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json_api/association.rb b/lib/active_model/serializer/adapter/json_api/association.rb deleted file mode 100644 index b6cfc70dd..000000000 --- a/lib/active_model/serializer/adapter/json_api/association.rb +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 99bff2981..000000000 --- a/lib/active_model/serializer/adapter/json_api/resource_identifier.rb +++ /dev/null @@ -1,41 +0,0 @@ -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/reflection.rb b/lib/active_model/serializer/reflection.rb index 89fa4074f..d7378e60f 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -42,17 +42,17 @@ def initialize(*) def link(name, value = nil, &block) @_links[name] = block || value - nil + :nil end def meta(value = nil, &block) @_meta = block || value - nil + :nil end def include_data(value = true) @_include_data = value - nil + :nil end def value(serializer) @@ -60,7 +60,12 @@ def value(serializer) @scope = serializer.scope if block - instance_eval(&block) + block_value = instance_eval(&block) + if block_value == :nil + serializer.read_attribute_for_serialization(name) + else + block_value + end else serializer.read_attribute_for_serialization(name) end diff --git a/test/adapter/json_api/api_objects/relationship_test.rb b/test/adapter/json_api/api_objects/relationship_test.rb new file mode 100644 index 000000000..5564400ea --- /dev/null +++ b/test/adapter/json_api/api_objects/relationship_test.rb @@ -0,0 +1,168 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class RelationshipTest < ActiveSupport::TestCase + def setup + @blog = Blog.new(id: 1) + @author = Author.new(id: 1, name: 'Steve K.', blog: @blog) + @serializer = BlogSerializer.new(@blog) + ActionController::Base.cache_store.clear + end + + def test_relationship_with_data + expected = { + data: { + id: '1', + type: 'blogs' + } + } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_with_nil_model + @serializer = BlogSerializer.new(nil) + expected = { data: nil } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_with_nil_serializer + @serializer = nil + expected = { data: nil } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_with_data_array + posts = [Post.new(id: 1), Post.new(id: 2)] + @serializer = ActiveModel::Serializer::ArraySerializer.new(posts) + @author.posts = posts + @author.blog = nil + expected = { + data: [ + { + id: '1', + type: 'posts' + }, + { + id: '2', + type: 'posts' + } + ] + } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_data_not_included + test_relationship({}, options: { include_data: false }) + end + + def test_relationship_simple_link + links = { self: 'a link' } + test_relationship({ links: { self: 'a link' } }, links: links) + end + + def test_relationship_many_links + links = { + self: 'a link', + related: 'another link' + } + expected = { + links: { + self: 'a link', + related: 'another link' + } + } + test_relationship(expected, links: links) + end + + def test_relationship_block_link + links = { self: proc { "#{object.id}" } } + expected = { links: { self: "#{@blog.id}" } } + test_relationship(expected, links: links) + end + + def test_relationship_block_link_with_meta + links = { + self: proc do + href "#{object.id}" + meta(id: object.id) + end + } + expected = { + links: { + self: { + href: "#{@blog.id}", + meta: { id: @blog.id } + } + } + } + test_relationship(expected, links: links) + end + + def test_relationship_simple_meta + meta = { id: '1' } + expected = { meta: meta } + test_relationship(expected, meta: meta) + end + + def test_relationship_block_meta + meta = proc do + { id: object.id } + end + expected = { + meta: { + id: @blog.id + } + } + test_relationship(expected, meta: meta) + end + + def test_relationship_with_everything + links = { + self: 'a link', + related: proc do + href "#{object.id}" + meta object.id + end + + } + meta = proc do + { id: object.id } + end + expected = { + data: { + id: '1', + type: 'blogs' + }, + links: { + self: 'a link', + related: { + href: '1', meta: 1 + } + }, + meta: { + id: @blog.id + } + } + test_relationship(expected, meta: meta, options: { include_data: true }, links: links) + end + + private + + def test_relationship(expected, params = {}) + options = params.fetch(:options, {}) + links = params.fetch(:links, {}) + meta = params[:meta] + parent_serializer = AuthorSerializer.new(@author) + relationship = Relationship.new(parent_serializer, @serializer, options, links, meta) + assert_equal(expected, relationship.as_json) + end + end + end + end + end + end +end diff --git a/test/adapter/json_api/api_objects/resource_identifier_test.rb b/test/adapter/json_api/api_objects/resource_identifier_test.rb new file mode 100644 index 000000000..a40f07071 --- /dev/null +++ b/test/adapter/json_api/api_objects/resource_identifier_test.rb @@ -0,0 +1,88 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class ResourceIdentifierTest < ActiveSupport::TestCase + class WithDefinedTypeSerializer < Serializer + type 'with_defined_type' + end + + class WithDefinedIdSerializer < Serializer + def id + 'special_id' + end + end + + class FragmentedSerializer < Serializer; end + + def setup + @model = Author.new(id: 1, name: 'Steve K.') + ActionController::Base.cache_store.clear + end + + def test_defined_type + test_type(WithDefinedTypeSerializer, 'with_defined_type') + end + + def test_singular_type + test_type_inflection(AuthorSerializer, 'author', :singular) + end + + def test_plural_type + test_type_inflection(AuthorSerializer, 'authors', :plural) + end + + def test_id_defined_on_object + test_id(AuthorSerializer, @model.id.to_s) + end + + def test_id_defined_on_serializer + test_id(WithDefinedIdSerializer, 'special_id') + end + + def test_id_defined_on_fragmented + FragmentedSerializer.fragmented(WithDefinedIdSerializer.new(@author)) + test_id(FragmentedSerializer, 'special_id') + end + + private + + def test_type_inflection(serializer_class, expected_type, inflection) + original_inflection = ActiveModelSerializers.config.jsonapi_resource_type + ActiveModelSerializers.config.jsonapi_resource_type = inflection + test_type(serializer_class, expected_type) + ActiveModelSerializers.config.jsonapi_resource_type = original_inflection + end + + def test_type(serializer_class, expected_type) + serializer = serializer_class.new(@model) + resource_identifier = ResourceIdentifier.new(serializer) + expected = { + id: @model.id.to_s, + type: expected_type + } + + assert_equal(expected, resource_identifier.as_json) + end + + def test_id(serializer_class, id) + serializer = serializer_class.new(@model) + resource_identifier = ResourceIdentifier.new(serializer) + inflection = ActiveModelSerializers.config.jsonapi_resource_type + type = @model.class.model_name.send(inflection) + expected = { + id: id, + type: type + } + + assert_equal(expected, resource_identifier.as_json) + end + end + end + end + end + end +end diff --git a/test/adapter/json_api/links_test.rb b/test/adapter/json_api/links_test.rb index 81dde4a30..43e37dd7e 100644 --- a/test/adapter/json_api/links_test.rb +++ b/test/adapter/json_api/links_test.rb @@ -17,18 +17,6 @@ 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 @@ -91,23 +79,6 @@ 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/adapter/json_api/relationship_test.rb b/test/adapter/json_api/relationship_test.rb new file mode 100644 index 000000000..110fbec4d --- /dev/null +++ b/test/adapter/json_api/relationship_test.rb @@ -0,0 +1,173 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + class RelationshipTest < ActiveSupport::TestCase + RelationshipAuthor = Class.new(::Model) + class RelationshipAuthorSerializer < ActiveModel::Serializer + has_one :bio do + link :self, '//example.com/link_author/relationships/bio' + end + + has_one :profile do + link :related do + "//example.com/profiles/#{object.profile.id}" + end + end + + has_many :locations do + link :related do + ids = object.locations.map!(&:id).join(',') + href "//example.com/locations/#{ids}" + end + end + + has_many :posts do + link :related do + ids = object.posts.map!(&:id).join(',') + href "//example.com/posts/#{ids}" + meta ids: ids + end + end + + has_many :roles do + meta count: object.posts.count + end + + has_one :blog do + link :self, '//example.com/link_author/relationships/blog' + include_data false + end + + belongs_to :reviewer do + meta name: 'Dan Brown' + include_data true + end + + has_many :likes do + link :related do + ids = object.likes.map!(&:id).join(',') + href "//example.com/likes/#{ids}" + meta ids: ids + end + meta liked: object.likes.any? + end + end + + def setup + @post = Post.new(id: 1337, comments: [], author: nil) + @blog = Blog.new(id: 1337, name: 'extra') + @bio = Bio.new(id: 1337) + @like = Like.new(id: 1337) + @role = Role.new(id: 1337) + @profile = Profile.new(id: 1337) + @location = Location.new(id: 1337) + @reviewer = Author.new(id: 1337) + @author = RelationshipAuthor.new( + id: 1337, + posts: [@post], + blog: @blog, + reviewer: @reviewer, + bio: @bio, + likes: [@like], + roles: [@role], + locations: [@location], + profile: @profile + ) + end + + def test_relationship_simple_link + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + data: { + id: '1337', + type: 'bios' + }, + links: { + self: '//example.com/link_author/relationships/bio' + } + } + assert_equal(expected, hash[:data][:relationships][:bio]) + end + + def test_relationship_block_link + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + data: { id: '1337', type: 'profiles' }, + links: { related: '//example.com/profiles/1337' } + } + assert_equal(expected, hash[:data][:relationships][:profile]) + end + + def test_relationship_block_link_href + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + data: [{ id: '1337', type: 'locations' }], + links: { + related: { href: '//example.com/locations/1337' } + } + } + assert_equal(expected, hash[:data][:relationships][:locations]) + end + + def test_relationship_block_link_meta + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + data: [{ id: '1337', type: 'posts' }], + links: { + related: { + href: '//example.com/posts/1337', + meta: { ids: '1337' } + } + } + } + assert_equal(expected, hash[:data][:relationships][:posts]) + end + + def test_relationship_meta + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + data: [{ id: '1337', type: 'roles' }], + meta: { count: 1 } + } + assert_equal(expected, hash[:data][:relationships][:roles]) + end + + def test_relationship_not_including_data + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + links: { self: '//example.com/link_author/relationships/blog' } + } + assert_equal(expected, hash[:data][:relationships][:blog]) + end + + def test_relationship_including_data_explicit + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + data: { id: '1337', type: 'authors' }, + meta: { name: 'Dan Brown' } + } + assert_equal(expected, hash[:data][:relationships][:reviewer]) + end + + def test_relationship_with_everything + hash = serializable(@author, adapter: :json_api).serializable_hash + expected = { + data: [{ id: '1337', type: 'likes' }], + links: { + related: { + href: '//example.com/likes/1337', + meta: { ids: '1337' } + } + }, + meta: { liked: true } + } + assert_equal(expected, hash[:data][:relationships][:likes]) + end + end + end + end + end +end