Skip to content

Commit 7789371

Browse files
committed
Add basic JsonApiObject behavior
Finish #1147
1 parent d42c8c8 commit 7789371

File tree

7 files changed

+109
-65
lines changed

7 files changed

+109
-65
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
* adds FlattenJSON as default adapter [@joaomdmoura]
1313
* adds support for `pagination links` at top level of JsonApi adapter [@bacarini]
1414
* adds extended format for `include` option to JsonApi adapter [@beauby]
15-
* adds support for top level jsonapi member support [@beauby]
15+
* adds support for top level `jsonapi` member [@beauby, @bf4]

docs/general/configuration_options.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,11 @@ The following configuration options can be set on `ActiveModel::Serializer.confi
99
## JSON API
1010

1111
- `jsonapi_resource_type`: Whether the `type` attributes of resources should be singular or plural. Possible values: `:singular, :plural`. Default: `:plural`.
12-
- `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`.
13-
- `jsonapi_version`: The latest version of the spec the API conforms to. Used when `jsonapi_toplevel_member` is `true`. Default: `'1.0'`.
12+
- `jsonapi_include_toplevel_member`: Whether to include a [top level JSON API member](http://jsonapi.org/format/#document-jsonapi-object)
13+
in the response document.
14+
Default: `false`.
15+
- Used when `jsonapi_include_toplevel_member` is `true`:
16+
- `jsonapi_version`: The latest version of the spec the API conforms to.
17+
Default: `'1.0'`.
18+
- `jsonapi_toplevel_meta`: Optional metadata. Not included if empty.
19+
Default: `{}`.

lib/active_model/serializable_resource.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
require 'set'
22
module ActiveModel
33
class SerializableResource
4-
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter,
5-
:jsonapi_toplevel_meta])
4+
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter])
65

76
# Primary interface to composing a resource with a serializer and adapter.
87
# @return the serializable_resource, ready for #as_json/#to_json/#serializable_hash.

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,48 +3,68 @@ class ActiveModel::Serializer::Adapter::JsonApi < ActiveModel::Serializer::Adapt
33
autoload :PaginationLinks
44
autoload :FragmentCache
55

6-
def initialize(serializer, options = {})
7-
super
8-
@included = ActiveModel::Serializer::Utils.include_args_to_hash(instance_options[:include])
9-
fields = options.delete(:fields)
10-
if fields
11-
@fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
12-
else
13-
@fieldset = options[:fieldset]
6+
module ApiObjects
7+
# Make JSON API top-level jsonapi member opt-in
8+
# ref: http://jsonapi.org/format/#document-top-level
9+
ActiveModel::Serializer.config.jsonapi_include_toplevel_member = false
10+
ActiveModel::Serializer.config.jsonapi_version = '1.0'
11+
ActiveModel::Serializer.config.jsonapi_toplevel_meta = {}
12+
module JsonApi
13+
extend self
14+
15+
def include_member?
16+
ActiveModel::Serializer.config.jsonapi_include_toplevel_member
17+
end
18+
19+
def add!(document)
20+
object = {
21+
jsonapi: {
22+
version: ActiveModel::Serializer.config.jsonapi_version,
23+
meta: ActiveModel::Serializer.config.jsonapi_toplevel_meta
24+
}
25+
}
26+
object[:jsonapi].reject! { |_, v| v.blank? }
27+
document.merge!(object)
28+
end
1429
end
1530
end
1631

1732
def serializable_hash(options = nil)
1833
options ||= {}
19-
hash =
20-
if serializer.respond_to?(:each)
21-
serializable_hash_for_collection(serializer, options)
22-
else
23-
serializable_hash_for_single_resource(serializer, options)
24-
end
25-
26-
if ActiveModel::Serializer.config.jsonapi_toplevel_member
27-
hash[:jsonapi] = {}
28-
hash[:jsonapi][:version] = ActiveModel::Serializer.config.jsonapi_version
29-
hash[:jsonapi][:meta] = @options[:jsonapi_toplevel_meta] if @options[:jsonapi_toplevel_meta]
34+
self.hash = {}
35+
ApiObjects::JsonApi.include_member? && ApiObjects::JsonApi.add!(hash)
36+
if serializer.respond_to?(:each)
37+
serializable_hash_for_collection(serializer, options)
38+
else
39+
serializable_hash_for_single_resource(serializer, options)
3040
end
31-
32-
hash
3341
end
3442

3543
def fragment_cache(cached_hash, non_cached_hash)
3644
root = false if instance_options.include?(:include)
3745
ActiveModel::Serializer::Adapter::JsonApi::FragmentCache.new.fragment_cache(root, cached_hash, non_cached_hash)
3846
end
3947

48+
def fieldset
49+
@fieldset ||=
50+
begin
51+
fields = instance_options.delete(:fields)
52+
if fields
53+
ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
54+
else
55+
instance_options[:fieldset]
56+
end
57+
end
58+
end
59+
4060
private
4161

4262
ActiveModel.silence_warnings do
43-
attr_reader :included, :fieldset
63+
attr_accessor :hash
4464
end
4565

4666
def serializable_hash_for_collection(serializer, options)
47-
hash = { data: [] }
67+
hash[:data] = []
4868
serializer.each do |s|
4969
result = self.class.new(s, instance_options.merge(fieldset: fieldset)).serializable_hash(options)
5070
hash[:data] << result[:data]
@@ -64,11 +84,13 @@ def serializable_hash_for_collection(serializer, options)
6484
end
6585

6686
def serializable_hash_for_single_resource(serializer, options)
67-
primary_data = primary_data_for(serializer, options)
87+
hash[:data] = {}
88+
hash[:data].update primary_data_for(serializer, options)
89+
6890
relationships = relationships_for(serializer)
69-
included = included_for(serializer)
70-
hash = { data: primary_data }
7191
hash[:data][:relationships] = relationships if relationships.any?
92+
93+
included = included_for(serializer)
7294
hash[:included] = included if included.any?
7395

7496
hash
@@ -129,10 +151,13 @@ def relationship_value_for(serializer, options = {})
129151
end
130152

131153
def relationships_for(serializer)
132-
Hash[serializer.associations.map { |association| [association.key, { data: relationship_value_for(association.serializer, association.options) }] }]
154+
Hash[serializer.associations.map { |association|
155+
[association.key, { data: relationship_value_for(association.serializer, association.options) }]
156+
}]
133157
end
134158

135159
def included_for(serializer)
160+
included = ActiveModel::Serializer::Utils.include_args_to_hash(instance_options[:include])
136161
included.flat_map { |inc|
137162
association = serializer.associations.find { |assoc| assoc.key == inc.first }
138163
_included_for(association.serializer, inc.second) if association

lib/active_model/serializer/configuration.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ module Configuration
44
include ActiveSupport::Configurable
55
extend ActiveSupport::Concern
66

7+
# Configuration options may also be set in
8+
# Serializers and Adapters
79
included do |base|
810
base.config.array_serializer = ActiveModel::Serializer::ArraySerializer
911
base.config.adapter = :flatten_json
1012
base.config.jsonapi_resource_type = :plural
11-
base.config.jsonapi_toplevel_member = false
12-
base.config.jsonapi_version = '1.0'
1313
end
1414
end
1515
end

test/adapter/json_api/toplevel_jsonapi_test.rb

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,58 +27,56 @@ def setup
2727
@author.posts = []
2828
end
2929

30-
def with_config(option, value)
31-
old_value = ActiveModel::Serializer.config[option]
32-
ActiveModel::Serializer.config[option] = value
33-
yield
34-
ensure
35-
ActiveModel::Serializer.config[option] = old_value
30+
def test_toplevel_jsonapi_defaults_to_false
31+
assert_equal config.fetch(:jsonapi_include_toplevel_member), false
3632
end
3733

3834
def test_disable_toplevel_jsonapi
39-
with_adapter :json_api do
40-
with_config(:jsonapi_toplevel_member, false) do
41-
hash = ActiveModel::SerializableResource.new(@post).serializable_hash
42-
assert_nil(hash[:jsonapi])
43-
end
35+
with_config(jsonapi_include_toplevel_member: false) do
36+
hash = serialize(@post)
37+
assert_nil(hash[:jsonapi])
4438
end
4539
end
4640

4741
def test_enable_toplevel_jsonapi
48-
with_adapter :json_api do
49-
with_config(:jsonapi_toplevel_member, true) do
50-
hash = ActiveModel::SerializableResource.new(@post).serializable_hash
51-
refute_nil(hash[:jsonapi])
52-
end
42+
with_config(jsonapi_include_toplevel_member: true) do
43+
hash = serialize(@post)
44+
refute_nil(hash[:jsonapi])
5345
end
5446
end
5547

5648
def test_default_toplevel_jsonapi_version
57-
with_adapter :json_api do
58-
with_config(:jsonapi_toplevel_member, true) do
59-
hash = ActiveModel::SerializableResource.new(@post).serializable_hash
60-
assert_equal('1.0', hash[:jsonapi][:version])
61-
end
49+
with_config(jsonapi_include_toplevel_member: true) do
50+
hash = serialize(@post)
51+
assert_equal('1.0', hash[:jsonapi][:version])
6252
end
6353
end
6454

6555
def test_toplevel_jsonapi_no_meta
66-
with_adapter :json_api do
67-
with_config(:jsonapi_toplevel_member, true) do
68-
hash = ActiveModel::SerializableResource.new(@post).serializable_hash
69-
assert_nil(hash[:jsonapi][:meta])
70-
end
56+
with_config(jsonapi_include_toplevel_member: true) do
57+
hash = serialize(@post)
58+
assert_nil(hash[:jsonapi][:meta])
7159
end
7260
end
7361

7462
def test_toplevel_jsonapi_meta
75-
with_adapter :json_api do
76-
with_config(:jsonapi_toplevel_member, true) do
77-
hash = ActiveModel::SerializableResource.new(@post, jsonapi_toplevel_meta: 'custom').serializable_hash
78-
assert_equal('custom', hash[:jsonapi][:meta])
79-
end
63+
new_config = {
64+
jsonapi_include_toplevel_member: true,
65+
jsonapi_toplevel_meta: {
66+
'copyright' => 'Copyright 2015 Example Corp.'
67+
}
68+
}
69+
with_config(new_config) do
70+
hash = serialize(@post)
71+
assert_equal(new_config[:jsonapi_toplevel_meta], hash.fetch(:jsonapi).fetch(:meta))
8072
end
8173
end
74+
75+
private
76+
77+
def serialize(resource, options = {})
78+
serializable(resource, { adapter: :json_api }.merge!(options)).serializable_hash
79+
end
8280
end
8381
end
8482
end

test/support/serialization_testing.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
module SerializationTesting
2+
def config
3+
ActiveModel::Serializer.config
4+
end
5+
26
private
37

48
def generate_cached_serializer(obj)
@@ -18,6 +22,18 @@ def with_adapter(adapter)
1822
ActiveModel::Serializer.config.adapter = old_adapter
1923
end
2024
alias_method :with_configured_adapter, :with_adapter
25+
26+
def with_config(hash)
27+
old_config = config.dup
28+
ActiveModel::Serializer.config.update(hash)
29+
yield
30+
ensure
31+
ActiveModel::Serializer.config.replace(old_config)
32+
end
33+
34+
def serializable(resource, options = {})
35+
ActiveModel::SerializableResource.new(resource, options)
36+
end
2137
end
2238

2339
class Minitest::Test

0 commit comments

Comments
 (0)