Skip to content

Commit 06ae191

Browse files
bf4beauby
authored andcommitted
Add support for top level jsonapi member.
1 parent 1f08865 commit 06ae191

File tree

7 files changed

+156
-6
lines changed

7 files changed

+156
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Features:
1717
- [#1158](https://github.com/rails-api/active_model_serializers/pull/1158) Add support for wildcards in `include` option (@beauby)
1818
- [#1127](https://github.com/rails-api/active_model_serializers/pull/1127) Add support for nested
1919
associations for JSON and Attributes adapters via the `include` option (@NullVoxPopuli, @beauby).
20+
- [#1050](https://github.com/rails-api/active_model_serializers/pull/1050) Add support for toplevel jsonapi member (@beauby, @bf4)
2021

2122
Fixes:
2223

docs/general/configuration_options.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +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_include_toplevel_object`: 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_object` 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/serializer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
require 'thread_safe'
2-
require 'active_model/serializer/adapter'
32
require 'active_model/serializer/array_serializer'
43
require 'active_model/serializer/include_tree'
54
require 'active_model/serializer/associations'
@@ -11,6 +10,7 @@ module ActiveModel
1110
class Serializer
1211
include Configuration
1312
include Associations
13+
require 'active_model/serializer/adapter'
1414

1515
# Matches
1616
# "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,40 @@ class JsonApi < Base
66
autoload :PaginationLinks
77
autoload :FragmentCache
88

9+
# TODO: if we like this abstraction and other API objects to it,
10+
# then extract to its own file and require it.
11+
module ApiObjects
12+
module JsonApi
13+
ActiveModel::Serializer.config.jsonapi_version = '1.0'
14+
ActiveModel::Serializer.config.jsonapi_toplevel_meta = {}
15+
# Make JSON API top-level jsonapi member opt-in
16+
# ref: http://jsonapi.org/format/#document-top-level
17+
ActiveModel::Serializer.config.jsonapi_include_toplevel_object = false
18+
module_function
19+
20+
def add!(hash)
21+
hash.merge!(object) if include_object?
22+
end
23+
24+
def include_object?
25+
ActiveModel::Serializer.config.jsonapi_include_toplevel_object
26+
end
27+
28+
# TODO: see if we can cache this
29+
def object
30+
object = {
31+
jsonapi: {
32+
version: ActiveModel::Serializer.config.jsonapi_version,
33+
meta: ActiveModel::Serializer.config.jsonapi_toplevel_meta
34+
}
35+
}
36+
object[:jsonapi].reject! { |_, v| v.blank? }
37+
38+
object
39+
end
40+
end
41+
end
42+
943
def initialize(serializer, options = {})
1044
super
1145
@include_tree = IncludeTree.from_include_args(options[:include])
@@ -21,11 +55,16 @@ def initialize(serializer, options = {})
2155
def serializable_hash(options = nil)
2256
options ||= {}
2357

24-
if serializer.respond_to?(:each)
25-
serializable_hash_for_collection(options)
26-
else
27-
serializable_hash_for_single_resource(options)
28-
end
58+
hash =
59+
if serializer.respond_to?(:each)
60+
serializable_hash_for_collection(options)
61+
else
62+
serializable_hash_for_single_resource(options)
63+
end
64+
65+
ApiObjects::JsonApi.add!(hash)
66+
67+
hash
2968
end
3069

3170
def fragment_cache(cached_hash, non_cached_hash)

lib/active_model/serializer/configuration.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ 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 = :attributes
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
require 'test_helper'
2+
3+
module ActiveModel
4+
class Serializer
5+
module Adapter
6+
class JsonApi
7+
class TopLevelJsonApiTest < Minitest::Test
8+
def setup
9+
@author = Author.new(id: 1, name: 'Steve K.')
10+
@author.bio = nil
11+
@author.roles = []
12+
@blog = Blog.new(id: 23, name: 'AMS Blog')
13+
@post = Post.new(id: 42, title: 'New Post', body: 'Body')
14+
@anonymous_post = Post.new(id: 43, title: 'Hello!!', body: 'Hello, world!!')
15+
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
16+
@post.comments = [@comment]
17+
@post.blog = @blog
18+
@anonymous_post.comments = []
19+
@anonymous_post.blog = nil
20+
@comment.post = @post
21+
@comment.author = nil
22+
@post.author = @author
23+
@anonymous_post.author = nil
24+
@blog = Blog.new(id: 1, name: 'My Blog!!')
25+
@blog.writer = @author
26+
@blog.articles = [@post, @anonymous_post]
27+
@author.posts = []
28+
end
29+
30+
def test_toplevel_jsonapi_defaults_to_false
31+
assert_equal config.fetch(:jsonapi_include_toplevel_object), false
32+
end
33+
34+
def test_disable_toplevel_jsonapi
35+
with_config(jsonapi_include_toplevel_object: false) do
36+
hash = serialize(@post)
37+
assert_nil(hash[:jsonapi])
38+
end
39+
end
40+
41+
def test_enable_toplevel_jsonapi
42+
with_config(jsonapi_include_toplevel_object: true) do
43+
hash = serialize(@post)
44+
refute_nil(hash[:jsonapi])
45+
end
46+
end
47+
48+
def test_default_toplevel_jsonapi_version
49+
with_config(jsonapi_include_toplevel_object: true) do
50+
hash = serialize(@post)
51+
assert_equal('1.0', hash[:jsonapi][:version])
52+
end
53+
end
54+
55+
def test_toplevel_jsonapi_no_meta
56+
with_config(jsonapi_include_toplevel_object: true) do
57+
hash = serialize(@post)
58+
assert_nil(hash[:jsonapi][:meta])
59+
end
60+
end
61+
62+
def test_toplevel_jsonapi_meta
63+
new_config = {
64+
jsonapi_include_toplevel_object: 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))
72+
end
73+
end
74+
75+
private
76+
77+
def serialize(resource, options = {})
78+
serializable(resource, { adapter: :json_api }.merge!(options)).serializable_hash
79+
end
80+
end
81+
end
82+
end
83+
end
84+
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)