mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-22 22:06:50 +00:00
Extended format for JSONAPI include option.
This commit is contained in:
parent
b594d1487b
commit
ce7a839f3d
@ -11,3 +11,4 @@
|
|||||||
* remove root key option and split JSON adapter [@joaomdmoura]
|
* remove root key option and split JSON adapter [@joaomdmoura]
|
||||||
* adds FlattenJSON as default adapter [@joaomdmoura]
|
* adds FlattenJSON as default adapter [@joaomdmoura]
|
||||||
* adds support for `pagination links` at top level of JsonApi adapter [@bacarini]
|
* adds support for `pagination links` at top level of JsonApi adapter [@bacarini]
|
||||||
|
* adds extended format for `include` option to JSONAPI adapter [@beauby]
|
||||||
|
|||||||
@ -30,6 +30,8 @@ resources in the `"included"` member when the resource names are included in the
|
|||||||
render @posts, include: 'authors,comments'
|
render @posts, include: 'authors,comments'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The format of the `include` option can be either a String composed of a comma-separated list of [relationship paths](http://jsonapi.org/format/#fetching-includes), an Array of Symbols and Hashes, or a mix of both.
|
||||||
|
|
||||||
## Choosing an adapter
|
## Choosing an adapter
|
||||||
|
|
||||||
If you want to use a specify a default adapter, such as JsonApi, you can change this in an initializer:
|
If you want to use a specify a default adapter, such as JsonApi, you can change this in an initializer:
|
||||||
|
|||||||
@ -10,6 +10,7 @@ module ActiveModel
|
|||||||
autoload :Lint
|
autoload :Lint
|
||||||
autoload :Associations
|
autoload :Associations
|
||||||
autoload :Fieldset
|
autoload :Fieldset
|
||||||
|
autoload :Utils
|
||||||
include Configuration
|
include Configuration
|
||||||
include Associations
|
include Associations
|
||||||
|
|
||||||
|
|||||||
@ -7,11 +7,7 @@ class ActiveModel::Serializer::Adapter::JsonApi < ActiveModel::Serializer::Adapt
|
|||||||
super
|
super
|
||||||
@hash = { data: [] }
|
@hash = { data: [] }
|
||||||
|
|
||||||
@options[:include] ||= []
|
@included = ActiveModel::Serializer::Utils.include_args_to_hash(@options[:include])
|
||||||
if @options[:include].is_a?(String)
|
|
||||||
@options[:include] = @options[:include].split(',')
|
|
||||||
end
|
|
||||||
|
|
||||||
fields = options.delete(:fields)
|
fields = options.delete(:fields)
|
||||||
if fields
|
if fields
|
||||||
@fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
|
@fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
|
||||||
@ -117,48 +113,38 @@ class ActiveModel::Serializer::Adapter::JsonApi < ActiveModel::Serializer::Adapt
|
|||||||
end
|
end
|
||||||
|
|
||||||
def included_for(serializer)
|
def included_for(serializer)
|
||||||
serializer.associations.flat_map { |assoc| _included_for(assoc.key, assoc.serializer) }.uniq
|
included = @included.flat_map do |inc|
|
||||||
|
association = serializer.associations.find { |assoc| assoc.key == inc.first }
|
||||||
|
_included_for(association.serializer, inc.second) if association
|
||||||
|
end
|
||||||
|
|
||||||
|
included.uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def _included_for(resource_name, serializer, parent = nil)
|
def _included_for(serializer, includes)
|
||||||
if serializer.respond_to?(:each)
|
if serializer.respond_to?(:each)
|
||||||
serializer.flat_map { |s| _included_for(resource_name, s, parent) }.uniq
|
serializer.flat_map { |s| _included_for(s, includes) }.uniq
|
||||||
else
|
else
|
||||||
return [] unless serializer && serializer.object
|
return [] unless serializer && serializer.object
|
||||||
result = []
|
|
||||||
resource_path = [parent, resource_name].compact.join('.')
|
|
||||||
|
|
||||||
if include_assoc?(resource_path)
|
primary_data = primary_data_for(serializer, @options)
|
||||||
primary_data = primary_data_for(serializer, @options)
|
relationships = relationships_for(serializer)
|
||||||
relationships = relationships_for(serializer)
|
primary_data[:relationships] = relationships if relationships.any?
|
||||||
primary_data[:relationships] = relationships if relationships.any?
|
|
||||||
result.push(primary_data)
|
|
||||||
end
|
|
||||||
|
|
||||||
if include_nested_assoc?(resource_path)
|
included = [primary_data]
|
||||||
non_empty_associations = serializer.associations.select(&:serializer)
|
|
||||||
|
|
||||||
non_empty_associations.each do |association|
|
includes.each do |inc|
|
||||||
result.concat(_included_for(association.key, association.serializer, resource_path))
|
association = serializer.associations.find { |assoc| assoc.key == inc.first }
|
||||||
result.uniq!
|
if association
|
||||||
|
included.concat(_included_for(association.serializer, inc.second))
|
||||||
|
included.uniq!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
result
|
|
||||||
|
included
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def include_assoc?(assoc)
|
|
||||||
check_assoc("#{assoc}$")
|
|
||||||
end
|
|
||||||
|
|
||||||
def include_nested_assoc?(assoc)
|
|
||||||
check_assoc("#{assoc}.")
|
|
||||||
end
|
|
||||||
|
|
||||||
def check_assoc(assoc)
|
|
||||||
@options[:include].any? { |s| s.match(/^#{assoc.gsub('.', '\.')}/) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_links(options)
|
def add_links(options)
|
||||||
links = @hash.fetch(:links) { {} }
|
links = @hash.fetch(:links) { {} }
|
||||||
collection = serializer.object
|
collection = serializer.object
|
||||||
|
|||||||
35
lib/active_model/serializer/utils.rb
Normal file
35
lib/active_model/serializer/utils.rb
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
module ActiveModel::Serializer::Utils
|
||||||
|
module_function
|
||||||
|
|
||||||
|
# Translates a comma separated list of dot separated paths (JSONAPI format) into a Hash.
|
||||||
|
# Example: `'posts.author, posts.comments.upvotes, posts.comments.author'` would become `{ posts: { author: {}, comments: { author: {}, upvotes: {} } } }`.
|
||||||
|
#
|
||||||
|
# @param [String] included
|
||||||
|
# @return [Hash] a Hash representing the same tree structure
|
||||||
|
def include_string_to_hash(included)
|
||||||
|
included.delete(' ').split(',').inject({}) do |hash, path|
|
||||||
|
hash.deep_merge!(path.split('.').reverse_each.inject({}) { |a, e| { e.to_sym => a } })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Translates the arguments passed to the include option into a Hash. The format can be either
|
||||||
|
# a String (see #include_string_to_hash), an Array of Symbols and Hashes, or a mix of both.
|
||||||
|
# Example: `posts: [:author, comments: [:author, :upvotes]]` would become `{ posts: { author: {}, comments: { author: {}, upvotes: {} } } }`.
|
||||||
|
#
|
||||||
|
# @param [Symbol, Hash, Array, String] included
|
||||||
|
# @return [Hash] a Hash representing the same tree structure
|
||||||
|
def include_args_to_hash(included)
|
||||||
|
case included
|
||||||
|
when Symbol
|
||||||
|
{ included => {} }
|
||||||
|
when Hash
|
||||||
|
included.each_with_object({}) { |(key, value), hash| hash[key] = include_args_to_hash(value) }
|
||||||
|
when Array
|
||||||
|
included.inject({}) { |a, e| a.merge!(include_args_to_hash(e)) }
|
||||||
|
when String
|
||||||
|
include_string_to_hash(included)
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -43,29 +43,29 @@ module ActionController
|
|||||||
|
|
||||||
def render_resource_with_include
|
def render_resource_with_include
|
||||||
setup_post
|
setup_post
|
||||||
render json: @post, include: 'author', adapter: :json_api
|
render json: @post, include: [:author], adapter: :json_api
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_resource_with_nested_include
|
def render_resource_with_nested_include
|
||||||
setup_post
|
setup_post
|
||||||
render json: @post, include: 'comments.author', adapter: :json_api
|
render json: @post, include: [comments: [:author]], adapter: :json_api
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_resource_with_nested_has_many_include
|
def render_resource_with_nested_has_many_include
|
||||||
setup_post
|
setup_post
|
||||||
render json: @post, include: ['author', 'author.roles'], adapter: :json_api
|
render json: @post, include: 'author.roles', adapter: :json_api
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_resource_with_missing_nested_has_many_include
|
def render_resource_with_missing_nested_has_many_include
|
||||||
setup_post
|
setup_post
|
||||||
@post.author = @author2 # author2 has no roles.
|
@post.author = @author2 # author2 has no roles.
|
||||||
render json: @post, include: 'author,author.roles', adapter: :json_api
|
render json: @post, include: [author: [:roles]], adapter: :json_api
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_collection_with_missing_nested_has_many_include
|
def render_collection_with_missing_nested_has_many_include
|
||||||
setup_post
|
setup_post
|
||||||
@post.author = @author2
|
@post.author = @author2
|
||||||
render json: [@post, @post2], include: 'author,author.roles', adapter: :json_api
|
render json: [@post, @post2], include: [author: [:roles]], adapter: :json_api
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_collection_without_include
|
def render_collection_without_include
|
||||||
@ -75,7 +75,7 @@ module ActionController
|
|||||||
|
|
||||||
def render_collection_with_include
|
def render_collection_with_include
|
||||||
setup_post
|
setup_post
|
||||||
render json: [@post], include: %w(author comments), adapter: :json_api
|
render json: [@post], include: 'author, comments', adapter: :json_api
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -141,8 +141,7 @@ module ActionController
|
|||||||
get :render_resource_with_nested_include
|
get :render_resource_with_nested_include
|
||||||
response = JSON.parse(@response.body)
|
response = JSON.parse(@response.body)
|
||||||
assert response.key? 'included'
|
assert response.key? 'included'
|
||||||
assert_equal 1, response['included'].size
|
assert_equal 3, response['included'].size
|
||||||
assert_equal 'Anonymous', response['included'].first['attributes']['name']
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_render_collection_without_include
|
def test_render_collection_without_include
|
||||||
|
|||||||
@ -38,7 +38,7 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_includes_linked_post
|
def test_includes_linked_post
|
||||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'post')
|
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:post])
|
||||||
expected = [{
|
expected = [{
|
||||||
id: '42',
|
id: '42',
|
||||||
type: 'posts',
|
type: 'posts',
|
||||||
@ -56,7 +56,7 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_limiting_linked_post_fields
|
def test_limiting_linked_post_fields
|
||||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'post', fields: { post: [:title] })
|
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:post], fields: { post: [:title] })
|
||||||
expected = [{
|
expected = [{
|
||||||
id: '42',
|
id: '42',
|
||||||
type: 'posts',
|
type: 'posts',
|
||||||
@ -108,7 +108,7 @@ module ActiveModel
|
|||||||
|
|
||||||
def test_include_linked_resources_with_type_name
|
def test_include_linked_resources_with_type_name
|
||||||
serializer = BlogSerializer.new(@blog)
|
serializer = BlogSerializer.new(@blog)
|
||||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, include: %w(writer articles))
|
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(serializer, include: [:writer, :articles])
|
||||||
linked = adapter.serializable_hash[:included]
|
linked = adapter.serializable_hash[:included]
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -24,7 +24,7 @@ module ActiveModel
|
|||||||
@serializer = PostPreviewSerializer.new(@post)
|
@serializer = PostPreviewSerializer.new(@post)
|
||||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||||
@serializer,
|
@serializer,
|
||||||
include: %w(comments author)
|
include: [:comments, :author]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_includes_linked_comments
|
def test_includes_linked_comments
|
||||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments')
|
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:comments])
|
||||||
expected = [{
|
expected = [{
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'comments',
|
type: 'comments',
|
||||||
@ -68,7 +68,7 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_limit_fields_of_linked_comments
|
def test_limit_fields_of_linked_comments
|
||||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments', fields: { comment: [:id] })
|
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:comments], fields: { comment: [:id] })
|
||||||
expected = [{
|
expected = [{
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'comments',
|
type: 'comments',
|
||||||
|
|||||||
@ -28,7 +28,7 @@ module ActiveModel
|
|||||||
@virtual_value = VirtualValue.new(id: 1)
|
@virtual_value = VirtualValue.new(id: 1)
|
||||||
|
|
||||||
@serializer = AuthorSerializer.new(@author)
|
@serializer = AuthorSerializer.new(@author)
|
||||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'bio,posts')
|
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:bio, :posts])
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_includes_bio_id
|
def test_includes_bio_id
|
||||||
@ -38,7 +38,7 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
|
|
||||||
def test_includes_linked_bio
|
def test_includes_linked_bio
|
||||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'bio')
|
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:bio])
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -43,11 +43,11 @@ module ActiveModel
|
|||||||
serializer = ArraySerializer.new([@first_post, @second_post])
|
serializer = ArraySerializer.new([@first_post, @second_post])
|
||||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||||
serializer,
|
serializer,
|
||||||
include: ['author', 'author.bio', 'comments']
|
include: [:comments, author: [:bio]]
|
||||||
)
|
)
|
||||||
alt_adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
alt_adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||||
serializer,
|
serializer,
|
||||||
include: 'author,author.bio,comments'
|
include: [:comments, author: [:bio]]
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
@ -153,11 +153,11 @@ module ActiveModel
|
|||||||
serializer = BioSerializer.new @bio1
|
serializer = BioSerializer.new @bio1
|
||||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||||
serializer,
|
serializer,
|
||||||
include: ['author', 'author.posts']
|
include: [author: [:posts]]
|
||||||
)
|
)
|
||||||
alt_adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
alt_adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||||
serializer,
|
serializer,
|
||||||
include: 'author,author.posts'
|
include: [author: [:posts]]
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@ -224,7 +224,7 @@ module ActiveModel
|
|||||||
serializer = ArraySerializer.new([@first_comment, @second_comment])
|
serializer = ArraySerializer.new([@first_comment, @second_comment])
|
||||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||||
serializer,
|
serializer,
|
||||||
include: ['post']
|
include: [:post]
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
@ -257,7 +257,7 @@ module ActiveModel
|
|||||||
serializer = PostPreviewSerializer.new(@first_post)
|
serializer = PostPreviewSerializer.new(@first_post)
|
||||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||||
serializer,
|
serializer,
|
||||||
include: ['author']
|
include: [:author]
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
|
|||||||
79
test/utils/include_args_to_hash_test.rb
Normal file
79
test/utils/include_args_to_hash_test.rb
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
module ActiveModel
|
||||||
|
class Serializer
|
||||||
|
module Utils
|
||||||
|
class IncludeArgsToHashTest < Minitest::Test
|
||||||
|
def test_nil
|
||||||
|
input = nil
|
||||||
|
expected = {}
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_empty_string
|
||||||
|
input = ''
|
||||||
|
expected = {}
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_single_string
|
||||||
|
input = 'author'
|
||||||
|
expected = { author: {} }
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_multiple_strings
|
||||||
|
input = 'author,comments'
|
||||||
|
expected = { author: {}, comments: {} }
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_multiple_strings_with_space
|
||||||
|
input = 'author, comments'
|
||||||
|
expected = { author: {}, comments: {} }
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_nested_string
|
||||||
|
input = 'posts.author'
|
||||||
|
expected = { posts: { author: {} } }
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_multiple_nested_string
|
||||||
|
input = 'posts.author,posts.comments.author,comments'
|
||||||
|
expected = { posts: { author: {}, comments: { author: {} } }, comments: {} }
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_empty_array
|
||||||
|
input = []
|
||||||
|
expected = {}
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_simple_array
|
||||||
|
input = [:comments, :author]
|
||||||
|
expected = { author: {}, comments: {} }
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_nested_array
|
||||||
|
input = [:comments, posts: [:author, comments: [:author]]]
|
||||||
|
expected = { posts: { author: {}, comments: { author: {} } }, comments: {} }
|
||||||
|
actual = ActiveModel::Serializer::Utils.include_args_to_hash(input)
|
||||||
|
assert_equal(expected, actual)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user