Skip to content

Add support for jsonapi top level member #1147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
* remove root key option and split JSON adapter [@joaomdmoura]
* 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 extended format for `include` option to JsonApi adapter [@beauby]
* adds support for top level jsonapi member support [@beauby]
2 changes: 2 additions & 0 deletions docs/general/configuration_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ 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'`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍

3 changes: 2 additions & 1 deletion lib/active_model/serializable_resource.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
require 'set'
module ActiveModel
class SerializableResource
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter])
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter,
:jsonapi_toplevel_meta])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be a global config. I can't see any reason someone would want to configure any of this from the controller. Maybe if you're serving different versions of the JSON API standard, but that hasn't happened yet. There's a PR in the JSON API to add revision at various levels.. But I think that would still be handled via a global display resource revision display document revisions etc. and the Serializer knows how to get the revision for a given resource.

A JSON API document MAY include information about its implementation under a top level jsonapi member.
If present, the value of the jsonapi member MUST be an object (a "jsonapi object").
The jsonapi object MAY contain a version member whose value is a string indicating the highest JSON API version supported.
This object MAY also contain a meta member, whose value is a meta object that contains non-standard meta-information.

Ref: json-api/json-api#824

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was that people might (although ti wouldn't be great) want to handle two APIs with one app, maybe for migrating to a newer version of the standard or something.


def initialize(resource, options = {})
@resource = resource
Expand Down
17 changes: 13 additions & 4 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ def initialize(serializer, options = {})

def serializable_hash(options = nil)
options ||= {}
if serializer.respond_to?(:each)
serializable_hash_for_collection(serializer, options)
else
serializable_hash_for_single_resource(serializer, 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]
end

hash
end

def fragment_cache(cached_hash, non_cached_hash)
Expand Down
2 changes: 2 additions & 0 deletions lib/active_model/serializer/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ module Configuration
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'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be in the JsonApi adapter IMHO

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that makes sense.

end
end
end
Expand Down
86 changes: 86 additions & 0 deletions test/adapter/json_api/toplevel_jsonapi_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require 'test_helper'

module ActiveModel
class Serializer
class Adapter
class JsonApi
class TopLevelJsonApiTest < Minitest::Test
def setup
@author = Author.new(id: 1, name: 'Steve K.')
@author.bio = nil
@author.roles = []
@blog = Blog.new(id: 23, name: 'AMS Blog')
@post = Post.new(id: 42, title: 'New Post', body: 'Body')
@anonymous_post = Post.new(id: 43, title: 'Hello!!', body: 'Hello, world!!')
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
@post.comments = [@comment]
@post.blog = @blog
@anonymous_post.comments = []
@anonymous_post.blog = nil
@comment.post = @post
@comment.author = nil
@post.author = @author
@anonymous_post.author = nil
@blog = Blog.new(id: 1, name: 'My Blog!!')
@blog.writer = @author
@blog.articles = [@post, @anonymous_post]
@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
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
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
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
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
end
end

def test_toplevel_jsonapi_meta
with_adapter :json_api do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for with_adapter. Just pass in adapter: :json_api

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
end
end
end
end
end
end
end