mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-22 22:06:50 +00:00
Merge pull request #1131 from beauby/jsonapi-include-tree
Extended format for JSONAPI `include` option
This commit is contained in:
commit
2789a579e8
@ -11,3 +11,4 @@
|
||||
* 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]
|
||||
|
||||
@ -30,6 +30,8 @@ resources in the `"included"` member when the resource names are included in the
|
||||
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
|
||||
|
||||
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 :Associations
|
||||
autoload :Fieldset
|
||||
autoload :Utils
|
||||
include Configuration
|
||||
include Associations
|
||||
|
||||
|
||||
@ -7,11 +7,7 @@ class ActiveModel::Serializer::Adapter::JsonApi < ActiveModel::Serializer::Adapt
|
||||
super
|
||||
@hash = { data: [] }
|
||||
|
||||
@options[:include] ||= []
|
||||
if @options[:include].is_a?(String)
|
||||
@options[:include] = @options[:include].split(',')
|
||||
end
|
||||
|
||||
@included = ActiveModel::Serializer::Utils.include_args_to_hash(@options[:include])
|
||||
fields = options.delete(:fields)
|
||||
if fields
|
||||
@fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
|
||||
@ -117,48 +113,38 @@ class ActiveModel::Serializer::Adapter::JsonApi < ActiveModel::Serializer::Adapt
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def _included_for(resource_name, serializer, parent = nil)
|
||||
def _included_for(serializer, includes)
|
||||
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
|
||||
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)
|
||||
relationships = relationships_for(serializer)
|
||||
primary_data[:relationships] = relationships if relationships.any?
|
||||
result.push(primary_data)
|
||||
end
|
||||
primary_data = primary_data_for(serializer, @options)
|
||||
relationships = relationships_for(serializer)
|
||||
primary_data[:relationships] = relationships if relationships.any?
|
||||
|
||||
if include_nested_assoc?(resource_path)
|
||||
non_empty_associations = serializer.associations.select(&:serializer)
|
||||
included = [primary_data]
|
||||
|
||||
non_empty_associations.each do |association|
|
||||
result.concat(_included_for(association.key, association.serializer, resource_path))
|
||||
result.uniq!
|
||||
includes.each do |inc|
|
||||
association = serializer.associations.find { |assoc| assoc.key == inc.first }
|
||||
if association
|
||||
included.concat(_included_for(association.serializer, inc.second))
|
||||
included.uniq!
|
||||
end
|
||||
end
|
||||
result
|
||||
|
||||
included
|
||||
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)
|
||||
links = @hash.fetch(:links) { {} }
|
||||
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
|
||||
setup_post
|
||||
render json: @post, include: 'author', adapter: :json_api
|
||||
render json: @post, include: [:author], adapter: :json_api
|
||||
end
|
||||
|
||||
def render_resource_with_nested_include
|
||||
setup_post
|
||||
render json: @post, include: 'comments.author', adapter: :json_api
|
||||
render json: @post, include: [comments: [:author]], adapter: :json_api
|
||||
end
|
||||
|
||||
def render_resource_with_nested_has_many_include
|
||||
setup_post
|
||||
render json: @post, include: ['author', 'author.roles'], adapter: :json_api
|
||||
render json: @post, include: 'author.roles', adapter: :json_api
|
||||
end
|
||||
|
||||
def render_resource_with_missing_nested_has_many_include
|
||||
setup_post
|
||||
@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
|
||||
|
||||
def render_collection_with_missing_nested_has_many_include
|
||||
setup_post
|
||||
@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
|
||||
|
||||
def render_collection_without_include
|
||||
@ -75,7 +75,7 @@ module ActionController
|
||||
|
||||
def render_collection_with_include
|
||||
setup_post
|
||||
render json: [@post], include: %w(author comments), adapter: :json_api
|
||||
render json: [@post], include: 'author, comments', adapter: :json_api
|
||||
end
|
||||
end
|
||||
|
||||
@ -141,8 +141,7 @@ module ActionController
|
||||
get :render_resource_with_nested_include
|
||||
response = JSON.parse(@response.body)
|
||||
assert response.key? 'included'
|
||||
assert_equal 1, response['included'].size
|
||||
assert_equal 'Anonymous', response['included'].first['attributes']['name']
|
||||
assert_equal 3, response['included'].size
|
||||
end
|
||||
|
||||
def test_render_collection_without_include
|
||||
|
||||
@ -38,7 +38,7 @@ module ActiveModel
|
||||
end
|
||||
|
||||
def test_includes_linked_post
|
||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'post')
|
||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:post])
|
||||
expected = [{
|
||||
id: '42',
|
||||
type: 'posts',
|
||||
@ -56,7 +56,7 @@ module ActiveModel
|
||||
end
|
||||
|
||||
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 = [{
|
||||
id: '42',
|
||||
type: 'posts',
|
||||
@ -108,7 +108,7 @@ module ActiveModel
|
||||
|
||||
def test_include_linked_resources_with_type_name
|
||||
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]
|
||||
expected = [
|
||||
{
|
||||
|
||||
@ -24,7 +24,7 @@ module ActiveModel
|
||||
@serializer = PostPreviewSerializer.new(@post)
|
||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||
@serializer,
|
||||
include: %w(comments author)
|
||||
include: [:comments, :author]
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ module ActiveModel
|
||||
end
|
||||
|
||||
def test_includes_linked_comments
|
||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'comments')
|
||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:comments])
|
||||
expected = [{
|
||||
id: '1',
|
||||
type: 'comments',
|
||||
@ -68,7 +68,7 @@ module ActiveModel
|
||||
end
|
||||
|
||||
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 = [{
|
||||
id: '1',
|
||||
type: 'comments',
|
||||
|
||||
@ -28,7 +28,7 @@ module ActiveModel
|
||||
@virtual_value = VirtualValue.new(id: 1)
|
||||
|
||||
@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
|
||||
|
||||
def test_includes_bio_id
|
||||
@ -38,7 +38,7 @@ module ActiveModel
|
||||
end
|
||||
|
||||
def test_includes_linked_bio
|
||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: 'bio')
|
||||
@adapter = ActiveModel::Serializer::Adapter::JsonApi.new(@serializer, include: [:bio])
|
||||
|
||||
expected = [
|
||||
{
|
||||
|
||||
@ -43,11 +43,11 @@ module ActiveModel
|
||||
serializer = ArraySerializer.new([@first_post, @second_post])
|
||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||
serializer,
|
||||
include: ['author', 'author.bio', 'comments']
|
||||
include: [:comments, author: [:bio]]
|
||||
)
|
||||
alt_adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||
serializer,
|
||||
include: 'author,author.bio,comments'
|
||||
include: [:comments, author: [:bio]]
|
||||
)
|
||||
|
||||
expected = {
|
||||
@ -153,11 +153,11 @@ module ActiveModel
|
||||
serializer = BioSerializer.new @bio1
|
||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||
serializer,
|
||||
include: ['author', 'author.posts']
|
||||
include: [author: [:posts]]
|
||||
)
|
||||
alt_adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||
serializer,
|
||||
include: 'author,author.posts'
|
||||
include: [author: [:posts]]
|
||||
)
|
||||
|
||||
expected = [
|
||||
@ -224,7 +224,7 @@ module ActiveModel
|
||||
serializer = ArraySerializer.new([@first_comment, @second_comment])
|
||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||
serializer,
|
||||
include: ['post']
|
||||
include: [:post]
|
||||
)
|
||||
|
||||
expected = [
|
||||
@ -257,7 +257,7 @@ module ActiveModel
|
||||
serializer = PostPreviewSerializer.new(@first_post)
|
||||
adapter = ActiveModel::Serializer::Adapter::JsonApi.new(
|
||||
serializer,
|
||||
include: ['author']
|
||||
include: [:author]
|
||||
)
|
||||
|
||||
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