Extended format for JSONAPI include option.

This commit is contained in:
Lucas Hosseini 2015-09-10 01:20:39 +02:00
parent b594d1487b
commit ce7a839f3d
12 changed files with 159 additions and 56 deletions

View File

@ -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]

View File

@ -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:

View File

@ -10,6 +10,7 @@ module ActiveModel
autoload :Lint
autoload :Associations
autoload :Fieldset
autoload :Utils
include Configuration
include Associations

View File

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

View 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

View File

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

View File

@ -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 = [
{

View File

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

View File

@ -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',

View File

@ -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 = [
{

View File

@ -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 = {

View 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