Skip to content

Commit

Permalink
Add support for wildcard includes.
Browse files Browse the repository at this point in the history
  • Loading branch information
beauby committed Sep 21, 2015
1 parent ab1e2af commit 52e5433
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 141 deletions.
2 changes: 1 addition & 1 deletion lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require 'active_model/serializer/configuration'
require 'active_model/serializer/fieldset'
require 'active_model/serializer/lint'
require 'active_model/serializer/utils'
require 'active_model/serializer/include_tree'

module ActiveModel
class Serializer
Expand Down
7 changes: 6 additions & 1 deletion lib/active_model/serializer/adapter/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ module ActiveModel
class Serializer
module Adapter
class Attributes < Base
def initialize(serializer, options = {})
super
@include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include] || '*')
end

def serializable_hash(options = nil)
options ||= {}
if serializer.respond_to?(:each)
Expand All @@ -13,7 +18,7 @@ def serializable_hash(options = nil)
serializer.attributes(options)
end

serializer.associations.each do |association|
serializer.associations(@include_tree).each do |association|
serializer = association.serializer
association_options = association.options

Expand Down
2 changes: 1 addition & 1 deletion lib/active_model/serializer/adapter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Json < Base

def serializable_hash(options = nil)
options ||= {}
{ root => Attributes.new(serializer).serializable_hash(options) }
{ root => Attributes.new(serializer, instance_options).serializable_hash(options) }
end

private
Expand Down
38 changes: 19 additions & 19 deletions lib/active_model/serializer/adapter/json_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class JsonApi < Base

def initialize(serializer, options = {})
super
@included = ActiveModel::Serializer::Utils.include_args_to_hash(instance_options[:include])
@include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include])

fields = options.delete(:fields)
if fields
@fieldset = ActiveModel::Serializer::Fieldset.new(fields, serializer.json_key)
Expand All @@ -19,10 +20,11 @@ def initialize(serializer, options = {})

def serializable_hash(options = nil)
options ||= {}

if serializer.respond_to?(:each)
serializable_hash_for_collection(serializer, options)
serializable_hash_for_collection(options)
else
serializable_hash_for_single_resource(serializer, options)
serializable_hash_for_single_resource(options)
end
end

Expand All @@ -37,7 +39,7 @@ def fragment_cache(cached_hash, non_cached_hash)
attr_reader :included, :fieldset
end

def serializable_hash_for_collection(serializer, options)
def serializable_hash_for_collection(options)
hash = { data: [] }
serializer.each do |s|
result = self.class.new(s, instance_options.merge(fieldset: fieldset)).serializable_hash(options)
Expand All @@ -57,10 +59,10 @@ def serializable_hash_for_collection(serializer, options)
hash
end

def serializable_hash_for_single_resource(serializer, options)
def serializable_hash_for_single_resource(options)
primary_data = primary_data_for(serializer, options)
relationships = relationships_for(serializer)
included = included_for(serializer)
included = included_for
hash = { data: primary_data }
hash[:data][:relationships] = relationships if relationships.any?
hash[:included] = included if included.any?
Expand Down Expand Up @@ -123,19 +125,20 @@ def relationship_value_for(serializer, options = {})
end

def relationships_for(serializer)
Hash[serializer.associations.map { |association| [association.key, { data: relationship_value_for(association.serializer, association.options) }] }]
serializer.associations.each_with_object({}) do |association, hash|
hash[association.key] = { data: relationship_value_for(association.serializer, association.options) }
end
end

def included_for(serializer)
included.flat_map { |inc|
association = serializer.associations.find { |assoc| assoc.key == inc.first }
_included_for(association.serializer, inc.second) if association
def included_for
serializer.associations(@include_tree).flat_map { |association|
_included_for(association.serializer, @include_tree[association.key])
}.uniq
end

def _included_for(serializer, includes)
def _included_for(serializer, include_tree)
if serializer.respond_to?(:each)
serializer.flat_map { |s| _included_for(s, includes) }.uniq
serializer.flat_map { |s| _included_for(s, include_tree) }.uniq
else
return [] unless serializer && serializer.object

Expand All @@ -145,12 +148,9 @@ def _included_for(serializer, includes)

included = [primary_data]

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
serializer.associations(include_tree).each do |association|
included.concat(_included_for(association.serializer, include_tree[association.key]))
included.uniq!
end

included
Expand Down
7 changes: 5 additions & 2 deletions lib/active_model/serializer/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,17 @@ def associate(reflection)
end
end

# @param [IncludeTree] include_tree (optional)
# @return [Enumerator<Association>]
#
def associations
def associations(include_tree = nil)
return unless object

include_tree ||= ActiveModel::Serializer::IncludeTree.from_string('*')

Enumerator.new do |y|
self.class._reflections.each do |reflection|
y.yield reflection.build_association(self, instance_options)
y.yield reflection.build_association(self, instance_options) if include_tree.key?(reflection.name)
end
end
end
Expand Down
77 changes: 77 additions & 0 deletions lib/active_model/serializer/include_tree.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module ActiveModel
class Serializer
# Description

class IncludeTree
class << self
# Builds an IncludeTree from a comma separated list of dot separated paths (JSON API format).
# @example `'posts.author, posts.comments.upvotes, posts.comments.author'`
#
# @param [String] included
# @return [IncludeTree]
#
def from_string(included)
new(include_string_to_hash(included))
end

# Translates the arguments passed to the include option into an IncludeTree.
# The format can be either a String (see #from_string), an Array of Symbols and Hashes, or a mix of both.
# @example `posts: [:author, comments: [:author, :upvotes]]`
#
# @param [Symbol, Hash, Array, String] included
# @return [IncludeTree]
#
def from_include_args(included)
new(include_args_to_hash(included))
end

private

def include_string_to_hash(included)
included.delete(' ').split(',').reduce({}) do |hash, path|
include_tree = path.split('.').reverse_each.reduce({}) { |a, e| { e.to_sym => a } }
hash.deep_merge!(include_tree)
end
end

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.reduce({}) { |a, e| a.merge!(include_args_to_hash(e)) }
when String
include_string_to_hash(included)
else
{}
end
end
end

# @param [Hash{Symbol => IncludeTree}] hash
def initialize(hash = {})
@hash = hash
end

def key?(key)
@hash.key?(key) || @hash.key?(:*) || @hash.key?(:**)
end

def [](key)
# TODO(beauby): Adopt a lazy caching strategy for generating subtrees.
case
when @hash.key?(key)
self.class.new(@hash[key])
when @hash.key?(:*)
self.class.new(@hash[:*])
when @hash.key?(:**)
self.class.new(:** => {})
end
end
end
end
end
35 changes: 0 additions & 35 deletions lib/active_model/serializer/utils.rb

This file was deleted.

6 changes: 3 additions & 3 deletions test/action_controller/json_api/linked_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def render_resource_with_nested_include
render json: @post, include: [comments: [:author]], adapter: :json_api
end

def render_resource_with_nested_has_many_include
def render_resource_with_nested_has_many_include_wildcard
setup_post
render json: @post, include: 'author.roles', adapter: :json_api
render json: @post, include: 'author.*', adapter: :json_api
end

def render_resource_with_missing_nested_has_many_include
Expand Down Expand Up @@ -96,7 +96,7 @@ def test_render_resource_with_include
end

def test_render_resource_with_nested_has_many_include
get :render_resource_with_nested_has_many_include
get :render_resource_with_nested_has_many_include_wildcard
response = JSON.parse(@response.body)
expected_linked = [
{
Expand Down
26 changes: 26 additions & 0 deletions test/include_tree/from_include_args_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'test_helper'

module ActiveModel
class Serializer
class IncludeTree
class FromStringTest < Minitest::Test
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
Loading

0 comments on commit 52e5433

Please sign in to comment.