From 77893710408780874f712676da197ead74268012 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Wed, 16 Sep 2015 09:21:28 -0500 Subject: [PATCH] Add basic JsonApiObject behavior Finish #1147 --- CHANGELOG.md | 2 +- docs/general/configuration_options.md | 10 ++- lib/active_model/serializable_resource.rb | 3 +- .../serializer/adapter/json_api.rb | 79 ++++++++++++------- lib/active_model/serializer/configuration.rb | 4 +- .../adapter/json_api/toplevel_jsonapi_test.rb | 60 +++++++------- test/support/serialization_testing.rb | 16 ++++ 7 files changed, 109 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b44127ba..c783ef116 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,4 +12,4 @@ * adds FlattenJSON as default adapter [@joaomdmoura] * adds support for `pagination links` at top level of JsonApi adapter [@bacarini] * adds extended format for `include` option to JsonApi adapter [@beauby] - * adds support for top level jsonapi member support [@beauby] + * adds support for top level `jsonapi` member [@beauby, @bf4] diff --git a/docs/general/configuration_options.md b/docs/general/configuration_options.md index 5981d5f00..25b08b12d 100644 --- a/docs/general/configuration_options.md +++ b/docs/general/configuration_options.md @@ -9,5 +9,11 @@ The following configuration options can be set on `ActiveModel::Serializer.confi ## JSON API - `jsonapi_resource_type`: Whether the `type` attributes of resources should be singular or plural. Possible values: `:singular, :plural`. Default: `:plural`. -- `jsonapi_toplevel_member`: Whether to include a [top level JSON API member](http://jsonapi.org/format/#document-jsonapi-object) in the response document. Default: `false`. -- `jsonapi_version`: The latest version of the spec the API conforms to. Used when `jsonapi_toplevel_member` is `true`. Default: `'1.0'`. +- `jsonapi_include_toplevel_member`: Whether to include a [top level JSON API member](http://jsonapi.org/format/#document-jsonapi-object) + in the response document. + Default: `false`. +- Used when `jsonapi_include_toplevel_member` is `true`: + - `jsonapi_version`: The latest version of the spec the API conforms to. + Default: `'1.0'`. + - `jsonapi_toplevel_meta`: Optional metadata. Not included if empty. + Default: `{}`. diff --git a/lib/active_model/serializable_resource.rb b/lib/active_model/serializable_resource.rb index 99820664f..0791385bc 100644 --- a/lib/active_model/serializable_resource.rb +++ b/lib/active_model/serializable_resource.rb @@ -1,8 +1,7 @@ require 'set' module ActiveModel class SerializableResource - ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, - :jsonapi_toplevel_meta]) + ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter]) # Primary interface to composing a resource with a serializer and adapter. # @return the serializable_resource, ready for #as_json/#to_json/#serializable_hash. diff --git a/lib/active_model/serializer/adapter/json_api.rb b/lib/active_model/serializer/adapter/json_api.rb index 24cfea974..b36d4bae5 100644 --- a/lib/active_model/serializer/adapter/json_api.rb +++ b/lib/active_model/serializer/adapter/json_api.rb @@ -3,33 +3,41 @@ class ActiveModel::Serializer::Adapter::JsonApi < ActiveModel::Serializer::Adapt autoload :PaginationLinks autoload :FragmentCache - def initialize(serializer, options = {}) - super - @included = ActiveModel::Serializer::Utils.include_args_to_hash(instance_options[:include]) - fields = options.delete(:fields) - if fields - @fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key) - else - @fieldset = options[:fieldset] + module ApiObjects + # Make JSON API top-level jsonapi member opt-in + # ref: http://jsonapi.org/format/#document-top-level + ActiveModel::Serializer.config.jsonapi_include_toplevel_member = false + ActiveModel::Serializer.config.jsonapi_version = '1.0' + ActiveModel::Serializer.config.jsonapi_toplevel_meta = {} + module JsonApi + extend self + + def include_member? + ActiveModel::Serializer.config.jsonapi_include_toplevel_member + end + + def add!(document) + object = { + jsonapi: { + version: ActiveModel::Serializer.config.jsonapi_version, + meta: ActiveModel::Serializer.config.jsonapi_toplevel_meta + } + } + object[:jsonapi].reject! { |_, v| v.blank? } + document.merge!(object) + end end end def serializable_hash(options = nil) options ||= {} - hash = - if serializer.respond_to?(:each) - serializable_hash_for_collection(serializer, options) - else - serializable_hash_for_single_resource(serializer, options) - end - - if ActiveModel::Serializer.config.jsonapi_toplevel_member - hash[:jsonapi] = {} - hash[:jsonapi][:version] = ActiveModel::Serializer.config.jsonapi_version - hash[:jsonapi][:meta] = @options[:jsonapi_toplevel_meta] if @options[:jsonapi_toplevel_meta] + self.hash = {} + ApiObjects::JsonApi.include_member? && ApiObjects::JsonApi.add!(hash) + if serializer.respond_to?(:each) + serializable_hash_for_collection(serializer, options) + else + serializable_hash_for_single_resource(serializer, options) end - - hash end def fragment_cache(cached_hash, non_cached_hash) @@ -37,14 +45,26 @@ def fragment_cache(cached_hash, non_cached_hash) ActiveModel::Serializer::Adapter::JsonApi::FragmentCache.new.fragment_cache(root, cached_hash, non_cached_hash) end + def fieldset + @fieldset ||= + begin + fields = instance_options.delete(:fields) + if fields + ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key) + else + instance_options[:fieldset] + end + end + end + private ActiveModel.silence_warnings do - attr_reader :included, :fieldset + attr_accessor :hash end def serializable_hash_for_collection(serializer, options) - hash = { data: [] } + hash[:data] = [] serializer.each do |s| result = self.class.new(s, instance_options.merge(fieldset: fieldset)).serializable_hash(options) hash[:data] << result[:data] @@ -64,11 +84,13 @@ def serializable_hash_for_collection(serializer, options) end def serializable_hash_for_single_resource(serializer, options) - primary_data = primary_data_for(serializer, options) + hash[:data] = {} + hash[:data].update primary_data_for(serializer, options) + relationships = relationships_for(serializer) - included = included_for(serializer) - hash = { data: primary_data } hash[:data][:relationships] = relationships if relationships.any? + + included = included_for(serializer) hash[:included] = included if included.any? hash @@ -129,10 +151,13 @@ def relationship_value_for(serializer, options = {}) end def relationships_for(serializer) - Hash[serializer.associations.map { |association| [association.key, { data: relationship_value_for(association.serializer, association.options) }] }] + Hash[serializer.associations.map { |association| + [association.key, { data: relationship_value_for(association.serializer, association.options) }] + }] end def included_for(serializer) + included = ActiveModel::Serializer::Utils.include_args_to_hash(instance_options[:include]) included.flat_map { |inc| association = serializer.associations.find { |assoc| assoc.key == inc.first } _included_for(association.serializer, inc.second) if association diff --git a/lib/active_model/serializer/configuration.rb b/lib/active_model/serializer/configuration.rb index d696ad2c1..997f301d2 100644 --- a/lib/active_model/serializer/configuration.rb +++ b/lib/active_model/serializer/configuration.rb @@ -4,12 +4,12 @@ module Configuration include ActiveSupport::Configurable extend ActiveSupport::Concern + # Configuration options may also be set in + # Serializers and Adapters included do |base| base.config.array_serializer = ActiveModel::Serializer::ArraySerializer base.config.adapter = :flatten_json base.config.jsonapi_resource_type = :plural - base.config.jsonapi_toplevel_member = false - base.config.jsonapi_version = '1.0' end end end diff --git a/test/adapter/json_api/toplevel_jsonapi_test.rb b/test/adapter/json_api/toplevel_jsonapi_test.rb index a95da1aea..d0fbf3a92 100644 --- a/test/adapter/json_api/toplevel_jsonapi_test.rb +++ b/test/adapter/json_api/toplevel_jsonapi_test.rb @@ -27,58 +27,56 @@ def setup @author.posts = [] end - def with_config(option, value) - old_value = ActiveModel::Serializer.config[option] - ActiveModel::Serializer.config[option] = value - yield - ensure - ActiveModel::Serializer.config[option] = old_value + def test_toplevel_jsonapi_defaults_to_false + assert_equal config.fetch(:jsonapi_include_toplevel_member), false end def test_disable_toplevel_jsonapi - with_adapter :json_api do - with_config(:jsonapi_toplevel_member, false) do - hash = ActiveModel::SerializableResource.new(@post).serializable_hash - assert_nil(hash[:jsonapi]) - end + with_config(jsonapi_include_toplevel_member: false) do + hash = serialize(@post) + assert_nil(hash[:jsonapi]) end end def test_enable_toplevel_jsonapi - with_adapter :json_api do - with_config(:jsonapi_toplevel_member, true) do - hash = ActiveModel::SerializableResource.new(@post).serializable_hash - refute_nil(hash[:jsonapi]) - end + with_config(jsonapi_include_toplevel_member: true) do + hash = serialize(@post) + refute_nil(hash[:jsonapi]) end end def test_default_toplevel_jsonapi_version - with_adapter :json_api do - with_config(:jsonapi_toplevel_member, true) do - hash = ActiveModel::SerializableResource.new(@post).serializable_hash - assert_equal('1.0', hash[:jsonapi][:version]) - end + with_config(jsonapi_include_toplevel_member: true) do + hash = serialize(@post) + assert_equal('1.0', hash[:jsonapi][:version]) end end def test_toplevel_jsonapi_no_meta - with_adapter :json_api do - with_config(:jsonapi_toplevel_member, true) do - hash = ActiveModel::SerializableResource.new(@post).serializable_hash - assert_nil(hash[:jsonapi][:meta]) - end + with_config(jsonapi_include_toplevel_member: true) do + hash = serialize(@post) + assert_nil(hash[:jsonapi][:meta]) end end def test_toplevel_jsonapi_meta - with_adapter :json_api do - with_config(:jsonapi_toplevel_member, true) do - hash = ActiveModel::SerializableResource.new(@post, jsonapi_toplevel_meta: 'custom').serializable_hash - assert_equal('custom', hash[:jsonapi][:meta]) - end + new_config = { + jsonapi_include_toplevel_member: true, + jsonapi_toplevel_meta: { + 'copyright' => 'Copyright 2015 Example Corp.' + } + } + with_config(new_config) do + hash = serialize(@post) + assert_equal(new_config[:jsonapi_toplevel_meta], hash.fetch(:jsonapi).fetch(:meta)) end end + + private + + def serialize(resource, options = {}) + serializable(resource, { adapter: :json_api }.merge!(options)).serializable_hash + end end end end diff --git a/test/support/serialization_testing.rb b/test/support/serialization_testing.rb index e990ec27b..e12720974 100644 --- a/test/support/serialization_testing.rb +++ b/test/support/serialization_testing.rb @@ -1,4 +1,8 @@ module SerializationTesting + def config + ActiveModel::Serializer.config + end + private def generate_cached_serializer(obj) @@ -18,6 +22,18 @@ def with_adapter(adapter) ActiveModel::Serializer.config.adapter = old_adapter end alias_method :with_configured_adapter, :with_adapter + + def with_config(hash) + old_config = config.dup + ActiveModel::Serializer.config.update(hash) + yield + ensure + ActiveModel::Serializer.config.replace(old_config) + end + + def serializable(resource, options = {}) + ActiveModel::SerializableResource.new(resource, options) + end end class Minitest::Test