Extract IncludeTree. (#1685)

This commit is contained in:
Lucas Hosseini 2016-05-28 16:07:11 +02:00 committed by L. Preston Sego III
parent f2cb497fe3
commit f48fd2a327
14 changed files with 49 additions and 227 deletions

View File

@ -14,6 +14,7 @@ Fixes:
Misc: Misc:
- [#1734](https://github.com/rails-api/active_model_serializers/pull/1734) Adds documentation for conditional attribute (@lambda2) - [#1734](https://github.com/rails-api/active_model_serializers/pull/1734) Adds documentation for conditional attribute (@lambda2)
- [#1685](https://github.com/rails-api/active_model_serializers/pull/1685) Replace `IncludeTree` with `IncludeDirective` from the jsonapi gem.
### [v0.10.0 (2016-05-17)](https://github.com/rails-api/active_model_serializers/compare/4a2d9853ba7...v0.10.0) ### [v0.10.0 (2016-05-17)](https://github.com/rails-api/active_model_serializers/compare/4a2d9853ba7...v0.10.0)

View File

@ -42,6 +42,8 @@ Gem::Specification.new do |spec|
# 'minitest' # 'minitest'
# 'thread_safe' # 'thread_safe'
spec.add_runtime_dependency 'jsonapi', '~> 0.1.1.beta2'
spec.add_development_dependency 'activerecord', rails_versions spec.add_development_dependency 'activerecord', rails_versions
# arel # arel
# activesupport # activesupport

View File

@ -28,7 +28,7 @@ Example supported requests
- Relationships - Relationships
- GET /articles/1/relationships/comments - GET /articles/1/relationships/comments
- GET /articles/1/relationships/author - GET /articles/1/relationships/author
- Optional: [Inclusion of related resources](http://jsonapi.org/format/#fetching-includes) `ActiveModel::Serializer::IncludeTree` - Optional: [Inclusion of related resources](http://jsonapi.org/format/#fetching-includes) `JSONAPI::IncludeDirective`
- GET /articles/1?`include`=comments - GET /articles/1?`include`=comments
- GET /articles/1?`include`=comments.author - GET /articles/1?`include`=comments.author
- GET /articles/1?`include`=author,comments.author - GET /articles/1?`include`=author,comments.author

View File

@ -1,9 +1,9 @@
require 'thread_safe' require 'thread_safe'
require 'jsonapi/include_directive'
require 'active_model/serializer/collection_serializer' require 'active_model/serializer/collection_serializer'
require 'active_model/serializer/array_serializer' require 'active_model/serializer/array_serializer'
require 'active_model/serializer/error_serializer' require 'active_model/serializer/error_serializer'
require 'active_model/serializer/errors_serializer' require 'active_model/serializer/errors_serializer'
require 'active_model/serializer/include_tree'
require 'active_model/serializer/associations' require 'active_model/serializer/associations'
require 'active_model/serializer/attributes' require 'active_model/serializer/attributes'
require 'active_model/serializer/caching' require 'active_model/serializer/caching'

View File

@ -78,18 +78,18 @@ module ActiveModel
end end
end end
# @param [IncludeTree] include_tree (defaults to the # @param [JSONAPI::IncludeDirective] include_directive (defaults to the
# default_includes config value when not provided) # +default_include_directive+ config value when not provided)
# @return [Enumerator<Association>] # @return [Enumerator<Association>]
# #
def associations(include_tree = ActiveModelSerializers.default_include_tree) def associations(include_directive = ActiveModelSerializers.default_include_directive)
return unless object return unless object
Enumerator.new do |y| Enumerator.new do |y|
self.class._reflections.each do |reflection| self.class._reflections.each do |reflection|
next if reflection.excluded?(self) next if reflection.excluded?(self)
key = reflection.options.fetch(:key, reflection.name) key = reflection.options.fetch(:key, reflection.name)
next unless include_tree.key?(key) next unless include_directive.key?(key)
y.yield reflection.build_association(self, instance_options) y.yield reflection.build_association(self, instance_options)
end end
end end

View File

@ -163,10 +163,10 @@ module ActiveModel
# Read cache from cache_store # Read cache from cache_store
# @return [Hash] # @return [Hash]
def cache_read_multi(collection_serializer, adapter_instance, include_tree) def cache_read_multi(collection_serializer, adapter_instance, include_directive)
return {} if ActiveModelSerializers.config.cache_store.blank? return {} if ActiveModelSerializers.config.cache_store.blank?
keys = object_cache_keys(collection_serializer, adapter_instance, include_tree) keys = object_cache_keys(collection_serializer, adapter_instance, include_directive)
return {} if keys.blank? return {} if keys.blank?
@ -176,15 +176,15 @@ module ActiveModel
# Find all cache_key for the collection_serializer # Find all cache_key for the collection_serializer
# @param serializers [ActiveModel::Serializer::CollectionSerializer] # @param serializers [ActiveModel::Serializer::CollectionSerializer]
# @param adapter_instance [ActiveModelSerializers::Adapter::Base] # @param adapter_instance [ActiveModelSerializers::Adapter::Base]
# @param include_tree [ActiveModel::Serializer::IncludeTree] # @param include_directive [JSONAPI::IncludeDirective]
# @return [Array] all cache_key of collection_serializer # @return [Array] all cache_key of collection_serializer
def object_cache_keys(collection_serializer, adapter_instance, include_tree) def object_cache_keys(collection_serializer, adapter_instance, include_directive)
cache_keys = [] cache_keys = []
collection_serializer.each do |serializer| collection_serializer.each do |serializer|
cache_keys << object_cache_key(serializer, adapter_instance) cache_keys << object_cache_key(serializer, adapter_instance)
serializer.associations(include_tree).each do |association| serializer.associations(include_directive).each do |association|
if association.serializer.respond_to?(:each) if association.serializer.respond_to?(:each)
association.serializer.each do |sub_serializer| association.serializer.each do |sub_serializer|
cache_keys << object_cache_key(sub_serializer, adapter_instance) cache_keys << object_cache_key(sub_serializer, adapter_instance)

View File

@ -31,11 +31,10 @@ module ActiveModelSerializers
[file, lineno] [file, lineno]
end end
# Memoized default include tree # Memoized default include directive
# @return [ActiveModel::Serializer::IncludeTree] # @return [JSONAPI::IncludeDirective]
def self.default_include_tree def self.default_include_directive
@default_include_tree ||= ActiveModel::Serializer::IncludeTree @default_include_directive ||= JSONAPI::IncludeDirective.new(config.default_includes, allow_wildcard: true)
.from_include_args(config.default_includes)
end end
require 'active_model/serializer/version' require 'active_model/serializer/version'

View File

@ -4,11 +4,13 @@ module ActiveModelSerializers
def initialize(serializer, options = {}) def initialize(serializer, options = {})
super super
@cached_attributes = options[:cache_attributes] || {} @cached_attributes = options[:cache_attributes] || {}
@include_tree = @include_directive =
if options[:include] if options[:include_directive]
ActiveModel::Serializer::IncludeTree.from_include_args(options[:include]) options[:include_directive]
elsif options[:include]
JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
else else
ActiveModelSerializers.default_include_tree ActiveModelSerializers.default_include_directive
end end
end end
@ -26,8 +28,8 @@ module ActiveModelSerializers
def serializable_hash_for_collection(options) def serializable_hash_for_collection(options)
cache_attributes cache_attributes
opts = instance_options.merge(include_directive: @include_directive)
serializer.map { |s| Attributes.new(s, instance_options).serializable_hash(options) } serializer.map { |s| Attributes.new(s, opts).serializable_hash(options) }
end end
def serializable_hash_for_single_resource(options) def serializable_hash_for_single_resource(options)
@ -38,7 +40,7 @@ module ActiveModelSerializers
def resource_relationships(options) def resource_relationships(options)
relationships = {} relationships = {}
serializer.associations(@include_tree).each do |association| serializer.associations(@include_directive).each do |association|
relationships[association.key] ||= relationship_value_for(association, options) relationships[association.key] ||= relationship_value_for(association, options)
end end
@ -49,7 +51,7 @@ module ActiveModelSerializers
return association.options[:virtual_value] if association.options[:virtual_value] return association.options[:virtual_value] if association.options[:virtual_value]
return unless association.serializer && association.serializer.object return unless association.serializer && association.serializer.object
opts = instance_options.merge(include: @include_tree[association.key]) opts = instance_options.merge(include_directive: @include_directive[association.key])
relationship_value = Attributes.new(association.serializer, opts).serializable_hash(options) relationship_value = Attributes.new(association.serializer, opts).serializable_hash(options)
if association.options[:polymorphic] && relationship_value if association.options[:polymorphic] && relationship_value
@ -64,7 +66,7 @@ module ActiveModelSerializers
def cache_attributes def cache_attributes
return if @cached_attributes.present? return if @cached_attributes.present?
@cached_attributes = ActiveModel::Serializer.cache_read_multi(serializer, self, @include_tree) @cached_attributes = ActiveModel::Serializer.cache_read_multi(serializer, self, @include_directive)
end end
def resource_object_for(options) def resource_object_for(options)

View File

@ -33,7 +33,7 @@ module ActiveModelSerializers
def initialize(serializer, options = {}) def initialize(serializer, options = {})
super super
@include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include]) @include_directive = JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields)) @fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
end end
@ -232,7 +232,7 @@ module ActiveModelSerializers
@included = [] @included = []
@resource_identifiers = Set.new @resource_identifiers = Set.new
serializers.each { |serializer| process_resource(serializer, true) } serializers.each { |serializer| process_resource(serializer, true) }
serializers.each { |serializer| process_relationships(serializer, @include_tree) } serializers.each { |serializer| process_relationships(serializer, @include_directive) }
[@primary, @included] [@primary, @included]
end end
@ -251,21 +251,21 @@ module ActiveModelSerializers
true true
end end
def process_relationships(serializer, include_tree) def process_relationships(serializer, include_directive)
serializer.associations(include_tree).each do |association| serializer.associations(include_directive).each do |association|
process_relationship(association.serializer, include_tree[association.key]) process_relationship(association.serializer, include_directive[association.key])
end end
end end
def process_relationship(serializer, include_tree) def process_relationship(serializer, include_directive)
if serializer.respond_to?(:each) if serializer.respond_to?(:each)
serializer.each { |s| process_relationship(s, include_tree) } serializer.each { |s| process_relationship(s, include_directive) }
return return
end end
return unless serializer && serializer.object return unless serializer && serializer.object
return unless process_resource(serializer, false) return unless process_resource(serializer, false)
process_relationships(serializer, include_tree) process_relationships(serializer, include_directive)
end end
# {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes} # {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
@ -429,8 +429,10 @@ module ActiveModelSerializers
# meta: meta # meta: meta
# }.reject! {|_,v| v.nil? } # }.reject! {|_,v| v.nil? }
def relationships_for(serializer, requested_associations) def relationships_for(serializer, requested_associations)
include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(requested_associations) include_directive = JSONAPI::IncludeDirective.new(
serializer.associations(include_tree).each_with_object({}) do |association, hash| requested_associations,
allow_wildcard: true)
serializer.associations(include_directive).each_with_object({}) do |association, hash|
hash[association.key] = Relationship.new( hash[association.key] = Relationship.new(
serializer, serializer,
association.serializer, association.serializer,

View File

@ -226,19 +226,19 @@ module ActionController
} }
end end
def with_default_includes(include_tree) def with_default_includes(include_directive)
original = ActiveModelSerializers.config.default_includes original = ActiveModelSerializers.config.default_includes
ActiveModelSerializers.config.default_includes = include_tree ActiveModelSerializers.config.default_includes = include_directive
clear_include_tree_cache clear_include_directive_cache
yield yield
ensure ensure
ActiveModelSerializers.config.default_includes = original ActiveModelSerializers.config.default_includes = original
clear_include_tree_cache clear_include_directive_cache
end end
def clear_include_tree_cache def clear_include_directive_cache
ActiveModelSerializers ActiveModelSerializers
.instance_variable_set(:@default_include_tree, nil) .instance_variable_set(:@default_include_directive, nil)
end end
end end
end end

View File

@ -286,9 +286,9 @@ module ActiveModelSerializers
def test_object_cache_keys def test_object_cache_keys
serializable = ActiveModelSerializers::SerializableResource.new([@comment, @comment]) serializable = ActiveModelSerializers::SerializableResource.new([@comment, @comment])
include_tree = ActiveModel::Serializer::IncludeTree.from_include_args('*') include_directive = JSONAPI::IncludeDirective.new('*', allow_wildcard: true)
actual = ActiveModel::Serializer.object_cache_keys(serializable.adapter.serializer, serializable.adapter, include_tree) actual = ActiveModel::Serializer.object_cache_keys(serializable.adapter.serializer, serializable.adapter, include_directive)
assert_equal 3, actual.size assert_equal 3, actual.size
assert actual.any? { |key| key == "comment/1/#{serializable.adapter.cached_name}" } assert actual.any? { |key| key == "comment/1/#{serializable.adapter.cached_name}" }

View File

@ -1,26 +0,0 @@
require 'test_helper'
module ActiveModel
class Serializer
class IncludeTree
class FromStringTest < ActiveSupport::TestCase
def test_simple_array
input = [:comments, :author]
actual = ActiveModel::Serializer::IncludeTree.from_include_args(input)
assert(actual.key?(:author))
assert(actual.key?(:comments))
end
def test_nested_array
input = [:comments, posts: [:author, comments: [:author]]]
actual = ActiveModel::Serializer::IncludeTree.from_include_args(input)
assert(actual.key?(:posts))
assert(actual[:posts].key?(:author))
assert(actual[:posts].key?(:comments))
assert(actual[:posts][:comments].key?(:author))
assert(actual.key?(:comments))
end
end
end
end
end

View File

@ -1,94 +0,0 @@
require 'test_helper'
module ActiveModel
class Serializer
class IncludeTree
class FromStringTest < ActiveSupport::TestCase
def test_single_string
input = 'author'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:author))
end
def test_multiple_strings
input = 'author,comments'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:author))
assert(actual.key?(:comments))
end
def test_multiple_strings_with_space
input = 'author, comments'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:author))
assert(actual.key?(:comments))
end
def test_nested_string
input = 'posts.author'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:posts))
assert(actual[:posts].key?(:author))
end
def test_multiple_nested_string
input = 'posts.author,posts.comments.author,comments'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:posts))
assert(actual[:posts].key?(:author))
assert(actual[:posts].key?(:comments))
assert(actual[:posts][:comments].key?(:author))
assert(actual.key?(:comments))
end
def test_toplevel_star_string
input = '*'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:comments))
end
def test_nested_star_string
input = 'posts.*'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:posts))
assert(actual[:posts].key?(:comments))
end
def test_nested_star_middle_string
input = 'posts.*.author'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:posts))
assert(actual[:posts].key?(:comments))
assert(actual[:posts][:comments].key?(:author))
refute(actual[:posts][:comments].key?(:unspecified))
end
def test_nested_star_lower_precedence_string
input = 'posts.comments.author,posts.*'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:posts))
assert(actual[:posts].key?(:comments))
assert(actual[:posts][:comments].key?(:author))
end
def test_toplevel_double_star_string
input = '**'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:posts))
assert(actual[:posts].key?(:comments))
assert(actual[:posts][:comments].key?(:posts))
end
def test_nested_double_star_string
input = 'comments, posts.**'
actual = ActiveModel::Serializer::IncludeTree.from_string(input)
assert(actual.key?(:comments))
refute(actual[:comments].key?(:author))
assert(actual.key?(:posts))
assert(actual[:posts].key?(:comments))
assert(actual[:posts][:comments].key?(:posts))
end
end
end
end
end

View File

@ -1,64 +0,0 @@
require 'test_helper'
module ActiveModel
class Serializer
class IncludeTree
module Parsing
class IncludeArgsToHashTest < MiniTest::Test
def test_include_args_to_hash_from_symbol
expected = { author: {} }
input = :author
actual = Parsing.include_args_to_hash(input)
assert_equal(expected, actual)
end
def test_include_args_to_hash_from_array
expected = { author: {}, comments: {} }
input = [:author, :comments]
actual = Parsing.include_args_to_hash(input)
assert_equal(expected, actual)
end
def test_include_args_to_hash_from_nested_array
expected = { author: {}, comments: { author: {} } }
input = [:author, comments: [:author]]
actual = Parsing.include_args_to_hash(input)
assert_equal(expected, actual)
end
def test_include_args_to_hash_from_array_of_hashes
expected = {
author: {},
blogs: { posts: { contributors: {} } },
comments: { author: { blogs: { posts: {} } } }
}
input = [
:author,
blogs: [posts: :contributors],
comments: { author: { blogs: :posts } }
]
actual = Parsing.include_args_to_hash(input)
assert_equal(expected, actual)
end
def test_array_of_string
expected = {
comments: { author: {}, attachment: {} }
}
input = [
'comments.author',
'comments.attachment'
]
actual = Parsing.include_args_to_hash(input)
assert_equal(expected, actual)
end
end
end
end
end
end