From 88149d1ea4923f88cf54820f8acd9dde7a5bbad3 Mon Sep 17 00:00:00 2001 From: Tim Phang Date: Tue, 10 Jun 2014 13:10:22 -0400 Subject: [PATCH 1/4] deep merge into root --- lib/active_model/serializable.rb | 10 ++++++- test/fixtures/poro.rb | 28 +++++++++++++++++++ .../array_serializer/serialization_test.rb | 13 +++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/lib/active_model/serializable.rb b/lib/active_model/serializable.rb index 7a34bc04c..151e1957e 100644 --- a/lib/active_model/serializable.rb +++ b/lib/active_model/serializable.rb @@ -3,7 +3,15 @@ module Serializable def as_json(options={}) if root = options.fetch(:root, json_key) hash = { root => serializable_object } - hash.merge!(serializable_data) + + serializable_data.each_pair do |key, value| + if hash[key].is_a?(Array) + hash[key].concat(value) + else + hash[key] = value + end + end + hash else serializable_object diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index a43954b53..b299cb916 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -35,6 +35,22 @@ def comments class Comment < Model end +class Activity < Model + attr_writer :responses + + def responses + @responses ||= [Response.new(content: 'response')] + end +end + +class Response < Model + attr_writer :responses + + def responses + @responses ||= [] + end +end + ### ## Serializers ### @@ -62,3 +78,15 @@ class PostSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer attributes :content end + +class ActivitySerializer < ActiveModel::Serializer + attributes :content + + has_many :responses, embed: :ids, embed_in_root: true +end + +class ResponseSerializer < ActiveModel::Serializer + attributes :content + + has_many :responses, embed_in_root: true, embed: :ids +end diff --git a/test/unit/active_model/array_serializer/serialization_test.rb b/test/unit/active_model/array_serializer/serialization_test.rb index 3be851311..9bf89836e 100644 --- a/test/unit/active_model/array_serializer/serialization_test.rb +++ b/test/unit/active_model/array_serializer/serialization_test.rb @@ -79,6 +79,19 @@ class << @post2 PostSerializer._associations[:comments] = @old_association end + def test_associated_objects_of_multiple_instances_embedded_in_root_nested + @response2_1 = Response.new(content: 'r2-1') + @response1_1 = Response.new(content: 'r1-1') + @response2 = Response.new(content: 'r1') + @response1 = Response.new(content: 'r1') + + @response1.responses = [@response1_1] + @response2.responses = [@response2_1] + + @serializer = ArraySerializer.new([@response1, @response2], root: :responses) #, root: :activities) + assert_equal({}, @serializer.as_json) + end + def test_embed_object_for_has_one_association_with_nil_value @association = UserSerializer._associations[:profile] @old_association = @association.dup From dd51bc545a37429fb0e191342ba20b5b0a65e2a0 Mon Sep 17 00:00:00 2001 From: Tim Phang Date: Fri, 27 Jun 2014 10:50:13 -0400 Subject: [PATCH 2/4] refactors embedded in root associations --- lib/active_model/serializer.rb | 70 +++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index a21c2884a..cfe32aeac 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -128,15 +128,11 @@ def attributes end def associations - associations = self.class._associations - included_associations = filter(associations.keys) - associations.each_with_object({}) do |(name, association), hash| - if included_associations.include? name - if association.embed_ids? - hash[association.key] = serialize_ids association - elsif association.embed_objects? - hash[association.embedded_key] = serialize association - end + allowed_associations.each_with_object({}) do |(name, association), hash| + if association.embed_ids? + hash[association.key] = serialize_ids association + elsif association.embed_objects? + hash[association.embedded_key] = serialize association end end end @@ -152,23 +148,8 @@ def filter(keys) end def embedded_in_root_associations - associations = self.class._associations - included_associations = filter(associations.keys) - associations.each_with_object({}) do |(name, association), hash| - if included_associations.include? name - if association.embed_in_root? - association_serializer = build_serializer(association) - hash.merge! association_serializer.embedded_in_root_associations - - serialized_data = association_serializer.serializable_object - key = association.root_key - if hash.has_key?(key) - hash[key].concat(serialized_data).uniq! - else - hash[key] = serialized_data - end - end - end + allowed_associations.each_with_object({}) do |association, hash| + embed_serialized_assocation(association, hash) if association.embed_in_root? end end @@ -197,5 +178,42 @@ def serializable_object(options={}) @wrap_in_array ? [hash] : hash end alias_method :serializable_hash, :serializable_object + + private + + def embed_serialized_assocation(association, hash) + association_serializer = build_serializer(association) + embed_association_root_assocations(association_serializer, hash) + embed_data(hash, association.root_key, association_serializer.serializable_object) + end + + def embed_association_root_assocations(association_serializer, hash) + return if association_serializer.send(:object).nil? + + association_serializer.embedded_in_root_associations.each do |key, value| + embed_data(hash, key, value) + end + end + + def embed_data(hash, key, data) + if hash[key].is_a?(Array) && data.is_a?(Array) + hash[key].concat(data).uniq! + else + hash[key] = data + end + hash + end + + def allowed_associations + included_associations.values_at(*allowed_association_keys) + end + + def allowed_association_keys + filter(included_associations.keys) + end + + def included_associations + self.class._associations + end end end From 09f501ea13ae4ad94459f4e32e89a0ea50c447ef Mon Sep 17 00:00:00 2001 From: Tim Phang Date: Fri, 27 Jun 2014 11:16:27 -0400 Subject: [PATCH 3/4] adds tests --- lib/active_model/serializer.rb | 70 +++++++------------ test/fixtures/poro.rb | 29 +------- .../array_serializer/serialization_test.rb | 32 ++++++--- 3 files changed, 51 insertions(+), 80 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index cfe32aeac..a21c2884a 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -128,11 +128,15 @@ def attributes end def associations - allowed_associations.each_with_object({}) do |(name, association), hash| - if association.embed_ids? - hash[association.key] = serialize_ids association - elsif association.embed_objects? - hash[association.embedded_key] = serialize association + associations = self.class._associations + included_associations = filter(associations.keys) + associations.each_with_object({}) do |(name, association), hash| + if included_associations.include? name + if association.embed_ids? + hash[association.key] = serialize_ids association + elsif association.embed_objects? + hash[association.embedded_key] = serialize association + end end end end @@ -148,8 +152,23 @@ def filter(keys) end def embedded_in_root_associations - allowed_associations.each_with_object({}) do |association, hash| - embed_serialized_assocation(association, hash) if association.embed_in_root? + associations = self.class._associations + included_associations = filter(associations.keys) + associations.each_with_object({}) do |(name, association), hash| + if included_associations.include? name + if association.embed_in_root? + association_serializer = build_serializer(association) + hash.merge! association_serializer.embedded_in_root_associations + + serialized_data = association_serializer.serializable_object + key = association.root_key + if hash.has_key?(key) + hash[key].concat(serialized_data).uniq! + else + hash[key] = serialized_data + end + end + end end end @@ -178,42 +197,5 @@ def serializable_object(options={}) @wrap_in_array ? [hash] : hash end alias_method :serializable_hash, :serializable_object - - private - - def embed_serialized_assocation(association, hash) - association_serializer = build_serializer(association) - embed_association_root_assocations(association_serializer, hash) - embed_data(hash, association.root_key, association_serializer.serializable_object) - end - - def embed_association_root_assocations(association_serializer, hash) - return if association_serializer.send(:object).nil? - - association_serializer.embedded_in_root_associations.each do |key, value| - embed_data(hash, key, value) - end - end - - def embed_data(hash, key, data) - if hash[key].is_a?(Array) && data.is_a?(Array) - hash[key].concat(data).uniq! - else - hash[key] = data - end - hash - end - - def allowed_associations - included_associations.values_at(*allowed_association_keys) - end - - def allowed_association_keys - filter(included_associations.keys) - end - - def included_associations - self.class._associations - end end end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index b299cb916..32d617565 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -33,21 +33,8 @@ def comments end class Comment < Model -end - -class Activity < Model - attr_writer :responses - - def responses - @responses ||= [Response.new(content: 'response')] - end -end - -class Response < Model - attr_writer :responses - - def responses - @responses ||= [] + def comments + @comments ||= [] end end @@ -78,15 +65,3 @@ class PostSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer attributes :content end - -class ActivitySerializer < ActiveModel::Serializer - attributes :content - - has_many :responses, embed: :ids, embed_in_root: true -end - -class ResponseSerializer < ActiveModel::Serializer - attributes :content - - has_many :responses, embed_in_root: true, embed: :ids -end diff --git a/test/unit/active_model/array_serializer/serialization_test.rb b/test/unit/active_model/array_serializer/serialization_test.rb index 9bf89836e..60d5bad1c 100644 --- a/test/unit/active_model/array_serializer/serialization_test.rb +++ b/test/unit/active_model/array_serializer/serialization_test.rb @@ -79,17 +79,31 @@ class << @post2 PostSerializer._associations[:comments] = @old_association end - def test_associated_objects_of_multiple_instances_embedded_in_root_nested - @response2_1 = Response.new(content: 'r2-1') - @response1_1 = Response.new(content: 'r1-1') - @response2 = Response.new(content: 'r1') - @response1 = Response.new(content: 'r1') + def test_associated_objects_of_recursive_instances_embedded_in_root + CommentSerializer.has_many :comments + @association = CommentSerializer._associations[:comments] - @response1.responses = [@response1_1] - @response2.responses = [@response2_1] + @association.embed = :ids + @association.embed_in_root = true + + @comment1 = Comment.new(content: 'C1') + @comment2 = Comment.new(content: 'C2') + + class << @comment1 + attr_writer :comments + end + @comment1.comments = [Comment.new(content: 'C1-1')] - @serializer = ArraySerializer.new([@response1, @response2], root: :responses) #, root: :activities) - assert_equal({}, @serializer.as_json) + @serializer = ArraySerializer.new([@comment1, @comment2], root: :comments) + assert_equal({ + comments: [ + { content: 'C1', 'comment_ids' => @comment1.comments.map(&:object_id) }, + { content: 'C2', 'comment_ids' => [] }, + { content: 'C1-1', 'comment_ids' => []} + ] + }, @serializer.as_json) + ensure + CommentSerializer._associations = {} end def test_embed_object_for_has_one_association_with_nil_value From dd5a10b2525abef285a7b1c39db3992463b95a4e Mon Sep 17 00:00:00 2001 From: Tim Phang Date: Thu, 3 Jul 2014 09:24:33 -0400 Subject: [PATCH 4/4] fixes association definition order bug --- lib/active_model/serializable.rb | 2 +- lib/active_model/serializer.rb | 70 ++++++++++++------- test/fixtures/poro.rb | 8 +++ .../serializer/embed_in_root_test.rb | 56 +++++++++++++++ 4 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 test/unit/active_model/serializer/embed_in_root_test.rb diff --git a/lib/active_model/serializable.rb b/lib/active_model/serializable.rb index 151e1957e..248deee86 100644 --- a/lib/active_model/serializable.rb +++ b/lib/active_model/serializable.rb @@ -5,7 +5,7 @@ def as_json(options={}) hash = { root => serializable_object } serializable_data.each_pair do |key, value| - if hash[key].is_a?(Array) + if hash[key].is_a?(Array) && value.is_a?(Array) hash[key].concat(value) else hash[key] = value diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index a21c2884a..47ccf248a 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -128,15 +128,11 @@ def attributes end def associations - associations = self.class._associations - included_associations = filter(associations.keys) - associations.each_with_object({}) do |(name, association), hash| - if included_associations.include? name - if association.embed_ids? - hash[association.key] = serialize_ids association - elsif association.embed_objects? - hash[association.embedded_key] = serialize association - end + allowed_associations.each_with_object({}) do |association, hash| + if association.embed_ids? + hash[association.key] = serialize_ids association + elsif association.embed_objects? + hash[association.embedded_key] = serialize association end end end @@ -152,23 +148,8 @@ def filter(keys) end def embedded_in_root_associations - associations = self.class._associations - included_associations = filter(associations.keys) - associations.each_with_object({}) do |(name, association), hash| - if included_associations.include? name - if association.embed_in_root? - association_serializer = build_serializer(association) - hash.merge! association_serializer.embedded_in_root_associations - - serialized_data = association_serializer.serializable_object - key = association.root_key - if hash.has_key?(key) - hash[key].concat(serialized_data).uniq! - else - hash[key] = serialized_data - end - end - end + allowed_associations.each_with_object({}) do |association, hash| + embed_serialized_assocation(association, hash) if association.embed_in_root? end end @@ -197,5 +178,42 @@ def serializable_object(options={}) @wrap_in_array ? [hash] : hash end alias_method :serializable_hash, :serializable_object + + protected + + def embed_serialized_assocation(association, hash) + association_serializer = build_serializer(association) + embed_association_root_assocations(association_serializer, hash) + embed_data(hash, association.root_key, association_serializer.serializable_object) + end + + def embed_association_root_assocations(association_serializer, hash) + return if association_serializer.send(:object).nil? + + association_serializer.embedded_in_root_associations.each do |key, value| + embed_data(hash, key, value) + end + end + + def embed_data(hash, key, data) + if hash[key].is_a?(Array) && data.is_a?(Array) + hash[key].concat(data).uniq! + else + hash[key] = data + end + hash + end + + def allowed_associations + included_associations.values_at(*allowed_association_keys) + end + + def allowed_association_keys + filter(included_associations.keys) + end + + def included_associations + self.class._associations + end end end diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 32d617565..fe370cda5 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -26,6 +26,10 @@ class Profile < Model end class Post < Model + def user + @user + end + def comments @comments ||= [Comment.new(content: 'C1'), Comment.new(content: 'C2')] @@ -33,6 +37,10 @@ def comments end class Comment < Model + def user + @user + end + def comments @comments ||= [] end diff --git a/test/unit/active_model/serializer/embed_in_root_test.rb b/test/unit/active_model/serializer/embed_in_root_test.rb new file mode 100644 index 000000000..dacc95fab --- /dev/null +++ b/test/unit/active_model/serializer/embed_in_root_test.rb @@ -0,0 +1,56 @@ +require 'test_helper' + +module ActiveModel + class Serializer + class EmbedInRootTest < Minitest::Test + def setup + @old_post_associations = PostSerializer._associations.dup + @old_comment_associations = CommentSerializer._associations.dup + @old_user_associations = UserSerializer._associations.dup + + # reset associations + PostSerializer._associations = {} + UserSerializer._associations = {} + + # set associations + PostSerializer.has_one :user, embed: :ids, embed_in_root: true + PostSerializer.has_many :comments, embed: :ids, embed_in_root: true + CommentSerializer.has_one :user, embed: :ids, embed_in_root: true + + @post = Post.new({ title: 'Title 1', body: 'Body 1', date: '1/1/2000' }) + @post_serializer = PostSerializer.new(@post) + end + + def teardown + PostSerializer._associations = @old_post_associations + CommentSerializer._associations = @old_comment_associations + UserSerializer._associations = @old_user_associations + end + + def test_associations_embedding_ids_including_objects_with_same_key_serialization_using_as_json + @user1 = User.new(name: 'post author') + @user2 = User.new(name: 'comment author') + + @post.instance_variable_set(:@user, @user1) + @post.comments.first.instance_variable_set(:@user, @user2) + + assert_equal({ + 'post' => { + title: 'Title 1', + body: 'Body 1', + 'user_id' => @user1.object_id, + 'comment_ids' => @post.comments.map(&:object_id) + }, + 'users' => [ + { name: 'post author', email: nil }, + { name: 'comment author', email: nil } + ], + comments: [ + { content: 'C1', 'user_id' => @user2.object_id }, + { content: 'C2', 'user_id' => nil } + ] + }, @post_serializer.as_json) + end + end + end +end