Skip to content

Commit

Permalink
Add basic JsonApiObject behavior
Browse files Browse the repository at this point in the history
Finish #1147
  • Loading branch information
bf4 committed Sep 18, 2015
1 parent d42c8c8 commit 4775d8f
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 65 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
10 changes: 8 additions & 2 deletions docs/general/configuration_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: `{}`.
3 changes: 1 addition & 2 deletions lib/active_model/serializable_resource.rb
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
79 changes: 52 additions & 27 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,68 @@ 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
module_function

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)
root = false if instance_options.include?(:include)
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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/active_model/serializer/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
60 changes: 29 additions & 31 deletions test/adapter/json_api/toplevel_jsonapi_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions test/support/serialization_testing.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
module SerializationTesting
def config
ActiveModel::Serializer.config
end

private

def generate_cached_serializer(obj)
Expand All @@ -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
Expand Down

0 comments on commit 4775d8f

Please sign in to comment.