Skip to content

Commit 22f63f4

Browse files
committed
Initial stab at recursive sideloading
Implementation idea for sideloading to help out JS Frameworks Implementation idea for sideloading to help out JS Frameworks added tests for sideloading and binding.pry for debugging added configuration option for sideloading associations switching computers tests pass revert require of dev-friendly gems - as they aren't as compatible as AMS is aiming to be also fixed a bug introduced by the sideloading - no existing tests caught the bug before initial push added a test for has_one Why does steve show up twice be more explicit about what gets added to the associations all tests passing minor renaming refactor remove gemfile.local
1 parent 8d3a89e commit 22f63f4

File tree

8 files changed

+320
-1
lines changed

8 files changed

+320
-1
lines changed

Gemfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ end
3333

3434
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
3535
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
36+
37+
38+
# Add a Gemfile.local to locally bundle gems outside of version control
39+
local_gemfile = File.join(File.expand_path("..", __FILE__), "Gemfile.local")
40+
if File.readable?(local_gemfile)
41+
eval_gemfile local_gemfile
42+
end

active_model_serializers.gemspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,5 @@ Gem::Specification.new do |spec|
4747

4848
spec.add_development_dependency 'bundler', '~> 1.6'
4949
spec.add_development_dependency 'timecop', '>= 0.7'
50+
5051
end

lib/active_model/serializer/adapter.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class Adapter
44
extend ActiveSupport::Autoload
55
require 'active_model/serializer/adapter/json'
66
require 'active_model/serializer/adapter/json_api'
7+
require 'active_model/serializer/adapter/json_sideload'
78
autoload :FlattenJson
89
autoload :Null
910
autoload :FragmentCache
@@ -86,7 +87,11 @@ def meta_key
8687
end
8788

8889
def root
89-
serializer.json_key.to_sym if serializer.json_key
90+
if serializer.json_key
91+
serializer.json_key.to_sym
92+
else
93+
serializer.root
94+
end
9095
end
9196

9297
def include_meta(json)
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
module ActiveModel
2+
class Serializer
3+
class Adapter
4+
class JsonSideload < Json
5+
6+
# the list of what has already been serialized will be kept here to
7+
# help avoid infinite recursion
8+
#
9+
# this should be a list of association_name to a list of objects
10+
attr_accessor :serialized
11+
12+
# When we are sideloading associations, we can more easily track what
13+
# has been serialized, so that we avoid infinite
14+
# recursion / serialization.
15+
def initialize(*args)
16+
@serialized = {}
17+
18+
super(*args)
19+
end
20+
21+
def serializable_hash(options = nil)
22+
options ||= {}
23+
24+
serialize_hash(options)
25+
26+
singularize_lone_objects
27+
28+
@serialized
29+
end
30+
31+
32+
def serialize_hash(options)
33+
result = nil
34+
35+
if serializer.respond_to?(:each)
36+
#TODO: Is this ever hit?
37+
# binding.pry
38+
ap "wwwwwwwwwwwwwwwwwwwwww"
39+
result = serialize_array(serializer, options)
40+
else
41+
# skip if we are already serialized
42+
key_name = serializer.object.class.name.tableize
43+
existing_of_kind = @serialized[key_name]
44+
exists = existing_of_kind ? existing_of_kind.select{|a| a[:id] == s.object.id } : false
45+
return if exists
46+
47+
# we aren't an array! woo!
48+
49+
result = serialized_attributes_of(serializer, options)
50+
# add to the list of the serialized
51+
52+
53+
# now, go over our associations, and add them to the master
54+
# serialized hash
55+
serializer.associations.each do |association|
56+
serializer = association.serializer
57+
opts = association.options
58+
59+
# make sure the association key exists in the master
60+
# serialized hash
61+
@serialized[association.key] ||= []
62+
63+
if serializer.respond_to?(:each)
64+
array = serialize_array(serializer, opts)
65+
association_list = @serialized[association.key]
66+
67+
68+
array.each do |item|
69+
if not association_list.include?(item)
70+
association_list << item
71+
end
72+
end
73+
@serialized[association.key] = association_list
74+
75+
# add the ids to the result
76+
result[ids_name_for(association.key)] = array.map{|a| a[:id] }
77+
else
78+
hash = (
79+
if serializer && serializer.object
80+
serialized_attributes_of(serializer, options)
81+
elsif opts[:virtual_value]
82+
opts[:virtual_value]
83+
end
84+
)
85+
86+
add(association.key, hash)
87+
88+
# add the id to the result
89+
result[id_name_for(association.key)] = hash[:id]
90+
end
91+
92+
end
93+
end
94+
95+
@serialized[key_name] ||= []
96+
add(key_name, result)
97+
98+
result
99+
end
100+
101+
def add(key, data)
102+
unless associations_contain?(data, key)
103+
if @serialized[key].is_a?(Hash)
104+
# make array
105+
value = @serialized[key]
106+
@serialized[key] = [value, data]
107+
else
108+
# already is array
109+
@serialized[key] << data
110+
end
111+
end
112+
end
113+
114+
def serialize_array(serializer, options)
115+
array = serializer.map { |s|
116+
js = JsonSideload.new(s)
117+
serialized = js.serialize_hash(options)
118+
119+
# keep the associations up to date
120+
append_to_serialized(js.serialized)
121+
122+
serialized
123+
}
124+
125+
# remove nils
126+
array.compact
127+
end
128+
129+
def ids_name_for(name)
130+
id_name_for(name).to_s.pluralize.to_sym
131+
end
132+
133+
def id_name_for(name)
134+
name.to_s.singularize.foreign_key.to_sym
135+
end
136+
137+
138+
def serialized_attributes_of(item, options)
139+
cache_check(item) do
140+
item.attributes(options)
141+
end
142+
end
143+
144+
def singularize_lone_objects
145+
temp = {}
146+
147+
@serialized.each do |key, data|
148+
if data.length > 1
149+
temp[key.to_s.pluralize.to_sym] = data
150+
else
151+
temp[key.to_s.singularize.to_sym] = data.first
152+
end
153+
end
154+
155+
@serialized = temp
156+
end
157+
158+
def append_to_serialized(serialized_objects)
159+
serialized_objects ||= {}
160+
161+
serialized_objects.each do |association_name, data|
162+
@serialized[association_name] ||= []
163+
164+
if data.is_a?(Array)
165+
data.each do |sub_data|
166+
append_to_serialized(association_name => sub_data)
167+
end
168+
else
169+
unless associations_contain?(data, association_name)
170+
add(association_name, data)
171+
end
172+
end
173+
end
174+
175+
@serialized
176+
end
177+
178+
def associations_contain?(item, key)
179+
return false if @serialized[key].nil?
180+
181+
@serialized[key] == item || @serialized[key].include?(item)
182+
end
183+
184+
end
185+
end
186+
end
187+
end

lib/active_model/serializer/configuration.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module Configuration
88
base.config.array_serializer = ActiveModel::Serializer::ArraySerializer
99
base.config.adapter = :flatten_json
1010
base.config.jsonapi_resource_type = :plural
11+
base.config.sideload_associations = false
1112
end
1213
end
1314
end

test/adapter/json/sideload_test.rb

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
require 'test_helper'
2+
3+
class ParentBlog < Blog; end
4+
class ParentBlogSerializer < BlogSerializer; end
5+
class HasOnePostSerializer < PostSerializer
6+
attributes :id, :title, :body
7+
8+
has_many :comments
9+
belongs_to :blog
10+
belongs_to :author
11+
has_one :parent_blog
12+
url :comments
13+
end
14+
15+
module ActiveModel
16+
class Serializer
17+
class Adapter
18+
class Json
19+
class SideloadTestTest < Minitest::Test
20+
def setup
21+
# ActiveModel::Serializer.config.sideload_associations = true
22+
ActionController::Base.cache_store.clear
23+
@author = Author.new(id: 1, name: 'Steve K.')
24+
@post = Post.new(id: 42, title: 'New Post', body: 'Body')
25+
@first_comment = Comment.new(id: 1, body: 'ZOMG A COMMENT', author: @author)
26+
@second_comment = Comment.new(id: 2, body: 'ZOMG ANOTHER COMMENT', author: @author)
27+
@post.comments = [@first_comment, @second_comment]
28+
@post.author = @author
29+
@first_comment.post = @post
30+
@second_comment.post = @post
31+
@blog = Blog.new(id: 1, name: "My Blog!!")
32+
@parent_blog = ParentBlog.new(id: 2, name: "Parent Blog!!")
33+
@post.blog = @blog
34+
@post.parent_blog = @parent_blog
35+
@tag = Tag.new(id: 1, name: "#hash_tag")
36+
@post.tags = [@tag]
37+
end
38+
39+
def teardown
40+
# ActiveModel::Serializer.config.sideload_associations = false
41+
end
42+
43+
def test_associations_not_present_in_base_model
44+
serializer = PostSerializer.new(@post)
45+
adapter = ActiveModel::Serializer::Adapter::JsonSideload.new(serializer)
46+
47+
assert_equal(nil, adapter.serializable_hash[:post][:comments])
48+
end
49+
50+
def test_associations_replaced_with_association_ids
51+
serializer = PostSerializer.new(@post)
52+
adapter = ActiveModel::Serializer::Adapter::JsonSideload.new(serializer)
53+
assert_equal([1, 2], adapter.serializable_hash[:post][:comment_ids])
54+
end
55+
56+
def test_relevant_associated_objects_in_json_root
57+
serializer = PostSerializer.new(@post)
58+
adapter = ActiveModel::Serializer::Adapter::JsonSideload.new(serializer)
59+
assert_equal([
60+
{:id=>1, :body=>"ZOMG A COMMENT", :post_id=>42, :author_id=>1},
61+
{:id=>2, :body=>"ZOMG ANOTHER COMMENT", :post_id=>42, :author_id=>1}
62+
], adapter.serializable_hash[:comments])
63+
end
64+
65+
def test_has_one_has_a_singular_key
66+
serializer = HasOnePostSerializer.new(@post)
67+
adapter = ActiveModel::Serializer::Adapter::JsonSideload.new(serializer)
68+
69+
assert_equal({
70+
id: 2, name: "Parent Blog!!"
71+
}, adapter.serializable_hash[:parent_blog])
72+
end
73+
74+
def test_belongs_to_has_a_singular_key
75+
serializer = PostSerializer.new(@post)
76+
adapter = ActiveModel::Serializer::Adapter::JsonSideload.new(serializer)
77+
assert_equal({
78+
id: 1, name: "Steve K."
79+
}, adapter.serializable_hash[:author])
80+
end
81+
82+
def test_append_to_serialized
83+
serializer = PostSerializer.new(@post)
84+
adapter = ActiveModel::Serializer::Adapter::JsonSideload.new(serializer)
85+
86+
adapter.serialized = {
87+
post: { id: 1 },
88+
comments: [{id: 1}]
89+
}
90+
91+
result = adapter.append_to_serialized({
92+
comments: [{id: 2}]
93+
})
94+
95+
assert_equal({
96+
post: { id: 1 },
97+
comments: [{id: 1}, {id: 2}]
98+
}, result)
99+
100+
end
101+
end
102+
end
103+
end
104+
end
105+
end

test/serializers/configuration_test.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ def test_array_serializer
1010
def test_default_adapter
1111
assert_equal :flatten_json, ActiveModel::Serializer.config.adapter
1212
end
13+
14+
def test_default_sideload_associations
15+
assert_equal false, ActiveModel::Serializer.config.sideload_associations
16+
end
17+
1318
end
1419
end
1520
end

test/test_helper.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
require 'action_controller/railtie'
88
require 'active_support/json'
99
require 'fileutils'
10+
11+
begin
12+
require 'awesome_print'
13+
require 'pry-byebug'
14+
rescue LoadError => e
15+
# not required for tests to run
16+
end
17+
1018
FileUtils.mkdir_p(File.expand_path('../../tmp/cache', __FILE__))
1119

1220
require 'minitest/autorun'

0 commit comments

Comments
 (0)