From 5bd7dd71dcf52690354dacddbb9e5bca1fb1da38 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Mon, 29 Jul 2013 17:02:04 -0700 Subject: [PATCH 1/5] Add has_many associations to polymorphic tests. --- test/serializer_test.rb | 132 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 6 deletions(-) diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 5da28d8aa..ac3400c43 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -1089,8 +1089,23 @@ def read_attribute_for_serialization(name) end def tests_can_handle_polymorphism + recipient_serializer = Class.new(ActiveModel::Serializer) do + attribute :address + end + + recipient_class = Class.new(Model) do + def self.to_s + "Recipient" + end + + define_method :active_model_serializer do + recipient_serializer + end + end + email_serializer = Class.new(ActiveModel::Serializer) do attributes :subject, :body + has_many :recipients, polymorphic: true end email_class = Class.new(Model) do @@ -1098,6 +1113,10 @@ def self.to_s "Email" end + define_method :recipients do + @attributes[:recipients] + end + define_method :active_model_serializer do email_serializer end @@ -1108,7 +1127,10 @@ def self.to_s has_one :attachable, polymorphic: true end - email = email_class.new subject: 'foo', body: 'bar' + email = email_class.new subject: 'foo', body: 'bar', recipients: [ + recipient_class.new(id: 1, address: 'person@example.com'), + recipient_class.new(id: 2, address: 'me@0.0.0.0') + ] attachment = Attachment.new name: 'logo.png', url: 'http://example.com/logo.png', attachable: email @@ -1119,14 +1141,49 @@ def self.to_s url: 'http://example.com/logo.png', attachable: { type: :email, - email: { subject: 'foo', body: 'bar' } + email: { + subject: 'foo', + body: 'bar', + recipients: [ + { + type: 'recipient', + recipient: { + id: 1, + address: 'person@example.com' + } + }, + { + type: 'recipient', + recipient: { + id: 2, + address: 'me@0.0.0.0' + } + } + ] + } } }, actual) end def test_can_handle_polymoprhic_ids + recipient_serializer = Class.new(ActiveModel::Serializer) do + attribute :address + end + + recipient_class = Class.new(Model) do + def self.to_s + "Recipient" + end + + define_method :active_model_serializer do + recipient_serializer + end + end + email_serializer = Class.new(ActiveModel::Serializer) do + embed :ids attributes :subject, :body + has_many :recipients, polymorphic: true end email_class = Class.new(Model) do @@ -1134,6 +1191,10 @@ def self.to_s "Email" end + define_method :recipients do + @attributes[:recipients] + end + define_method :active_model_serializer do email_serializer end @@ -1145,7 +1206,10 @@ def self.to_s has_one :attachable, polymorphic: true end - email = email_class.new id: 1 + email = email_class.new id: 1, recipients: [ + recipient_class.new(id: 1, address: 'person@example.com'), + recipient_class.new(id: 2, address: 'me@0.0.0.0') + ] attachment = Attachment.new name: 'logo.png', url: 'http://example.com/logo.png', attachable: email @@ -1156,14 +1220,45 @@ def self.to_s url: 'http://example.com/logo.png', attachable: { type: :email, - id: 1 + id: 1, } }, actual) + + assert_equal({ + subject: nil, + body: nil, + recipients: [ + { + type: :recipient, + id: 1 + }, + { + type: :recipient, + id: 2 + } + ] + }, email_serializer.new(email, {}).as_json) end def test_polymorphic_associations_are_included_at_root + recipient_serializer = Class.new(ActiveModel::Serializer) do + attribute :address + end + + recipient_class = Class.new(Model) do + def self.to_s + "Recipient" + end + + define_method :active_model_serializer do + recipient_serializer + end + end + email_serializer = Class.new(ActiveModel::Serializer) do + embed :ids, include: true attributes :subject, :body, :id + has_many :recipients, polymorphic: true end email_class = Class.new(Model) do @@ -1171,6 +1266,10 @@ def self.to_s "Email" end + define_method :recipients do + @attributes[:recipients] + end + define_method :active_model_serializer do email_serializer end @@ -1183,7 +1282,10 @@ def self.to_s has_one :attachable, polymorphic: true end - email = email_class.new id: 1, subject: "Hello", body: "World" + email = email_class.new id: 1, subject: "Hello", body: "World", recipients: [ + recipient_class.new(id: 1, address: 'person@example.com'), + recipient_class.new(id: 2, address: 'me@0.0.0.0') + ] attachment = Attachment.new name: 'logo.png', url: 'http://example.com/logo.png', attachable: email @@ -1200,7 +1302,25 @@ def self.to_s emails: [{ id: 1, subject: "Hello", - body: "World" + body: "World", + recipients: [ + { + type: 'recipient', + id: 1 + }, + { + type: 'recipient', + id: 2 + } + ] + }], + recipients: [{ + id: 1, + address: 'person@example.com' + }, + { + id: 2, + address: 'me@0.0.0.0' }] }, actual) end From e56ffb7e0c5ae8599439fb772b9dfaf7a3006fee Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Mon, 29 Jul 2013 17:32:09 -0700 Subject: [PATCH 2/5] Pass basic `has_many` polymorphic tests. Need to test cases with polymorphic associations containing multiple types and where the association name does not match the name of the contained types. --- lib/active_model/serializer/associations.rb | 38 ++++++++++++++++++-- test/serializer_test.rb | 39 +++++++++++---------- 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 1f2b0b53f..4f5d6704f 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -72,6 +72,11 @@ def find_serializable(object) end class HasMany < Association #:nodoc: + def initialize(name, options={}, serializer_options={}) + super + @polymorphic = options[:polymorphic] + end + def root options[:root] || name end @@ -88,20 +93,49 @@ def serializables def serialize object.map do |item| - find_serializable(item).serializable_hash + if polymorphic? + { + :type => polymorphic_key_for_item(item), + polymorphic_key_for_item(item) => find_serializable(item).serializable_hash + } + else + find_serializable(item).serializable_hash + end end end def serialize_ids object.map do |item| serializer = find_serializable(item) - if serializer.respond_to?(embed_key) + id = if serializer.respond_to?(embed_key) serializer.send(embed_key) else item.read_attribute_for_serialization(embed_key) end + + if polymorphic? + { + type: polymorphic_key_for_item(item), + id: id + } + else + id + end end end + + private + + attr_reader :polymorphic + alias polymorphic? polymorphic + + def use_id_key? + embed_ids? && !polymorphic? + end + + def polymorphic_key_for_item(item) + item.class.to_s.demodulize.underscore.to_sym + end end class HasOne < Association #:nodoc: diff --git a/test/serializer_test.rb b/test/serializer_test.rb index ac3400c43..2eeca0bba 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -1090,7 +1090,7 @@ def read_attribute_for_serialization(name) def tests_can_handle_polymorphism recipient_serializer = Class.new(ActiveModel::Serializer) do - attribute :address + attributes :address, :id end recipient_class = Class.new(Model) do @@ -1146,14 +1146,14 @@ def self.to_s body: 'bar', recipients: [ { - type: 'recipient', + type: :recipient, recipient: { id: 1, address: 'person@example.com' } }, { - type: 'recipient', + type: :recipient, recipient: { id: 2, address: 'me@0.0.0.0' @@ -1242,7 +1242,7 @@ def self.to_s def test_polymorphic_associations_are_included_at_root recipient_serializer = Class.new(ActiveModel::Serializer) do - attribute :address + attributes :address, :id end recipient_class = Class.new(Model) do @@ -1292,36 +1292,37 @@ def self.to_s actual = attachment_serializer.new(attachment, {}).as_json assert_equal({ - attachment: { - name: 'logo.png', - url: 'http://example.com/logo.png', - attachable: { - type: :email, - id: 1 - }}, emails: [{ - id: 1, subject: "Hello", body: "World", + id: 1, recipients: [ { - type: 'recipient', + type: :recipient, id: 1 }, { - type: 'recipient', + type: :recipient, id: 2 } ] }], recipients: [{ - id: 1, - address: 'person@example.com' + address: 'person@example.com', + id: 1 }, { - id: 2, - address: 'me@0.0.0.0' - }] + address: 'me@0.0.0.0', + id: 2 + }], + attachment: { + name: 'logo.png', + url: 'http://example.com/logo.png', + attachable: { + type: :email, + id: 1 + } + } }, actual) end From caa8ee6b1b8746248a1f84a59d1bd6805a5af10e Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 30 Jul 2013 10:47:49 -0700 Subject: [PATCH 3/5] Polymorphic has_many associations root keys. Polymorphic has_many associations using `include: true` include their items under keys matching the type names of each item. --- lib/active_model/serializer.rb | 4 +- lib/active_model/serializer/associations.rb | 38 +++++++++++----- test/serializer_test.rb | 48 +++++++++++---------- 3 files changed, 56 insertions(+), 34 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index e2a6228b6..000b36eec 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -387,7 +387,9 @@ def include!(name, options={}) if association.embed_in_root? && hash.nil? raise IncludeError.new(self.class, association.name) elsif association.embed_in_root? && association.embeddable? - merge_association hash, association.root, association.serializables, unique_values + association.roots.each do |root| + merge_association hash, root, association.serializables_for_root(root), unique_values + end end elsif association.embed_objects? node[association.key] = association.serialize diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 4f5d6704f..df65119d6 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -77,17 +77,31 @@ def initialize(name, options={}, serializer_options={}) @polymorphic = options[:polymorphic] end - def root - options[:root] || name + def roots + if polymorphic? && options[:root].nil? + object.map do |item| + polymorphic_root_for_item(item) + end.uniq + else + [ options[:root] || name ] + end end def id_key "#{name.to_s.singularize}_ids".to_sym end - def serializables - object.map do |item| - find_serializable(item) + def serializables_for_root(root) + if polymorphic? && options[:root].nil? + object.select do |item| + polymorphic_root_for_item(item) == root + end.map do |item| + find_serializable(item) + end + else + object.map do |item| + find_serializable(item) + end end end @@ -133,6 +147,10 @@ def use_id_key? embed_ids? && !polymorphic? end + def polymorphic_root_for_item(item) + item.class.to_s.demodulize.pluralize.underscore.to_sym + end + def polymorphic_key_for_item(item) item.class.to_s.demodulize.underscore.to_sym end @@ -144,13 +162,13 @@ def initialize(name, options={}, serializer_options={}) @polymorphic = options[:polymorphic] end - def root + def roots if root = options[:root] - root + [root] elsif polymorphic? - object.class.to_s.pluralize.demodulize.underscore.to_sym + [object.class.to_s.pluralize.demodulize.underscore.to_sym] else - name.to_s.pluralize.to_sym + [name.to_s.pluralize.to_sym] end end @@ -162,7 +180,7 @@ def embeddable? super || !polymorphic? end - def serializables + def serializables_for_root(root) value = object && find_serializable(object) value ? [value] : [] end diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 2eeca0bba..3d429fc4f 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -1105,7 +1105,7 @@ def self.to_s email_serializer = Class.new(ActiveModel::Serializer) do attributes :subject, :body - has_many :recipients, polymorphic: true + has_many :receivables, polymorphic: true end email_class = Class.new(Model) do @@ -1113,8 +1113,8 @@ def self.to_s "Email" end - define_method :recipients do - @attributes[:recipients] + define_method :receivables do + @attributes[:receivables] end define_method :active_model_serializer do @@ -1127,7 +1127,7 @@ def self.to_s has_one :attachable, polymorphic: true end - email = email_class.new subject: 'foo', body: 'bar', recipients: [ + email = email_class.new subject: 'foo', body: 'bar', receivables: [ recipient_class.new(id: 1, address: 'person@example.com'), recipient_class.new(id: 2, address: 'me@0.0.0.0') ] @@ -1144,7 +1144,7 @@ def self.to_s email: { subject: 'foo', body: 'bar', - recipients: [ + receivables: [ { type: :recipient, recipient: { @@ -1183,7 +1183,7 @@ def self.to_s email_serializer = Class.new(ActiveModel::Serializer) do embed :ids attributes :subject, :body - has_many :recipients, polymorphic: true + has_many :receivables, polymorphic: true end email_class = Class.new(Model) do @@ -1191,8 +1191,8 @@ def self.to_s "Email" end - define_method :recipients do - @attributes[:recipients] + define_method :receivables do + @attributes[:receivables] end define_method :active_model_serializer do @@ -1206,7 +1206,7 @@ def self.to_s has_one :attachable, polymorphic: true end - email = email_class.new id: 1, recipients: [ + email = email_class.new id: 1, receivables: [ recipient_class.new(id: 1, address: 'person@example.com'), recipient_class.new(id: 2, address: 'me@0.0.0.0') ] @@ -1227,7 +1227,7 @@ def self.to_s assert_equal({ subject: nil, body: nil, - recipients: [ + receivables: [ { type: :recipient, id: 1 @@ -1258,7 +1258,7 @@ def self.to_s email_serializer = Class.new(ActiveModel::Serializer) do embed :ids, include: true attributes :subject, :body, :id - has_many :recipients, polymorphic: true + has_many :receivables, polymorphic: true end email_class = Class.new(Model) do @@ -1266,8 +1266,8 @@ def self.to_s "Email" end - define_method :recipients do - @attributes[:recipients] + define_method :receivables do + @attributes[:receivables] end define_method :active_model_serializer do @@ -1282,7 +1282,7 @@ def self.to_s has_one :attachable, polymorphic: true end - email = email_class.new id: 1, subject: "Hello", body: "World", recipients: [ + email = email_class.new id: 1, subject: "Hello", body: "World", receivables: [ recipient_class.new(id: 1, address: 'person@example.com'), recipient_class.new(id: 2, address: 'me@0.0.0.0') ] @@ -1296,7 +1296,7 @@ def self.to_s subject: "Hello", body: "World", id: 1, - recipients: [ + receivables: [ { type: :recipient, id: 1 @@ -1307,14 +1307,16 @@ def self.to_s } ] }], - recipients: [{ - address: 'person@example.com', - id: 1 - }, - { - address: 'me@0.0.0.0', - id: 2 - }], + recipients: [ + { + address: 'person@example.com', + id: 1 + }, + { + address: 'me@0.0.0.0', + id: 2 + } + ], attachment: { name: 'logo.png', url: 'http://example.com/logo.png', From 75ae1251320d4de7df78769c17149ae56fae9794 Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 30 Jul 2013 11:16:43 -0700 Subject: [PATCH 4/5] Cleanup associations.rb implementation. --- lib/active_model/serializer/associations.rb | 148 +++++++++----------- 1 file changed, 63 insertions(+), 85 deletions(-) diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index df65119d6..e393526a8 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -32,6 +32,7 @@ def initialize(name, options={}, serializer_options={}) @embed_objects = embed == :object || embed == :objects @embed_key = options[:embed_key] || :id @embed_in_root = options[:include] + @polymorphic = options[:polymorphic] || false serializer = options[:serializer] @serializer_class = serializer.is_a?(String) ? serializer.constantize : serializer @@ -44,7 +45,6 @@ def initialize(name, options={}, serializer_options={}) alias embeddable? object alias embed_objects? embed_objects alias embed_ids? embed_ids - alias use_id_key? embed_ids? alias embed_in_root? embed_in_root def key @@ -59,7 +59,8 @@ def key private - attr_reader :embed_key, :serializer_class, :options, :serializer_options + attr_reader :embed_key, :serializer_class, :options, :serializer_options, :polymorphic + alias polymorphic? polymorphic def find_serializable(object) if serializer_class @@ -71,19 +72,59 @@ def find_serializable(object) end end - class HasMany < Association #:nodoc: - def initialize(name, options={}, serializer_options={}) - super - @polymorphic = options[:polymorphic] + def type_name(object) + object.class.to_s.demodulize.underscore.to_sym + end + + def serialize_item(object) + serializable_hash = find_serializable(object).serializable_hash + if polymorphic? + type_name = type_name object + { + :type => type_name, + type_name => serializable_hash + } + else + serializable_hash + end + end + + def serialization_id(object) + serializer = find_serializable(object) + if serializer.respond_to?(embed_key) + serializer.send(embed_key) + else + object.read_attribute_for_serialization(embed_key) end + end + + def serialize_id(object) + id = serialization_id object + + if polymorphic? + { + type: type_name(object), + id: id + } + else + id + end + end + + def use_id_key? + embed_ids? && !polymorphic? + end + class HasMany < Association #:nodoc: def roots - if polymorphic? && options[:root].nil? + if options[:root] + [options[:root]] + elsif polymorphic? object.map do |item| polymorphic_root_for_item(item) end.uniq else - [ options[:root] || name ] + [name.to_s.pluralize.to_sym] end end @@ -92,14 +133,14 @@ def id_key end def serializables_for_root(root) - if polymorphic? && options[:root].nil? - object.select do |item| - polymorphic_root_for_item(item) == root - end.map do |item| + if options[:root] || !polymorphic? + object.map do |item| find_serializable(item) end else - object.map do |item| + object.select do |item| + polymorphic_root_for_item(item) == root + end.map do |item| find_serializable(item) end end @@ -107,10 +148,12 @@ def serializables_for_root(root) def serialize object.map do |item| + serialize_item item if polymorphic? + type_name = type_name item { - :type => polymorphic_key_for_item(item), - polymorphic_key_for_item(item) => find_serializable(item).serializable_hash + :type => type_name, + type_name => find_serializable(item).serializable_hash } else find_serializable(item).serializable_hash @@ -120,51 +163,21 @@ def serialize def serialize_ids object.map do |item| - serializer = find_serializable(item) - id = if serializer.respond_to?(embed_key) - serializer.send(embed_key) - else - item.read_attribute_for_serialization(embed_key) - end - - if polymorphic? - { - type: polymorphic_key_for_item(item), - id: id - } - else - id - end + serialize_id item end end private - attr_reader :polymorphic - alias polymorphic? polymorphic - - def use_id_key? - embed_ids? && !polymorphic? - end - def polymorphic_root_for_item(item) item.class.to_s.demodulize.pluralize.underscore.to_sym end - - def polymorphic_key_for_item(item) - item.class.to_s.demodulize.underscore.to_sym - end end class HasOne < Association #:nodoc: - def initialize(name, options={}, serializer_options={}) - super - @polymorphic = options[:polymorphic] - end - def roots - if root = options[:root] - [root] + if options[:root] + [options[:root]] elsif polymorphic? [object.class.to_s.pluralize.demodulize.underscore.to_sym] else @@ -187,50 +200,15 @@ def serializables_for_root(root) def serialize if object - if polymorphic? - { - :type => polymorphic_key, - polymorphic_key => find_serializable(object).serializable_hash - } - else - find_serializable(object).serializable_hash - end + serialize_item object end end def serialize_ids if object - serializer = find_serializable(object) - id = - if serializer.respond_to?(embed_key) - serializer.send(embed_key) - else - object.read_attribute_for_serialization(embed_key) - end - - if polymorphic? - { - type: polymorphic_key, - id: id - } - else - id - end + serialize_id object end end - - private - - attr_reader :polymorphic - alias polymorphic? polymorphic - - def use_id_key? - embed_ids? && !polymorphic? - end - - def polymorphic_key - object.class.to_s.demodulize.underscore.to_sym - end end end end From 1038ae1226f67818eb5fdf7e32f8fcbf8ae81fda Mon Sep 17 00:00:00 2001 From: Jonah Williams Date: Tue, 30 Jul 2013 11:29:03 -0700 Subject: [PATCH 5/5] Cleanup polymorphic serializer tests. Separate embedded non-included has_one and has_many polymorphic association tests. --- test/serializer_test.rb | 61 +++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 3d429fc4f..1bae478c7 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -1165,7 +1165,45 @@ def self.to_s }, actual) end - def test_can_handle_polymoprhic_ids + def test_has_one_can_handle_polymoprhic_ids + email_serializer = Class.new(ActiveModel::Serializer) do + embed :ids + attributes :subject, :body + end + + email_class = Class.new(Model) do + def self.to_s + "Email" + end + + define_method :active_model_serializer do + email_serializer + end + end + + attachment_serializer = Class.new(ActiveModel::Serializer) do + embed :ids + attributes :name, :url + has_one :attachable, polymorphic: true + end + + email = email_class.new id: 1 + + attachment = Attachment.new name: 'logo.png', url: 'http://example.com/logo.png', attachable: email + + actual = attachment_serializer.new(attachment, {}).as_json + + assert_equal({ + name: 'logo.png', + url: 'http://example.com/logo.png', + attachable: { + type: :email, + id: 1, + } + }, actual) + end + + def test_has_many_can_handle_polymoprhic_ids recipient_serializer = Class.new(ActiveModel::Serializer) do attribute :address end @@ -1200,29 +1238,12 @@ def self.to_s end end - attachment_serializer = Class.new(ActiveModel::Serializer) do - embed :ids - attributes :name, :url - has_one :attachable, polymorphic: true - end - email = email_class.new id: 1, receivables: [ recipient_class.new(id: 1, address: 'person@example.com'), recipient_class.new(id: 2, address: 'me@0.0.0.0') ] - attachment = Attachment.new name: 'logo.png', url: 'http://example.com/logo.png', attachable: email - - actual = attachment_serializer.new(attachment, {}).as_json - - assert_equal({ - name: 'logo.png', - url: 'http://example.com/logo.png', - attachable: { - type: :email, - id: 1, - } - }, actual) + actual = email_serializer.new(email, {}).as_json assert_equal({ subject: nil, @@ -1237,7 +1258,7 @@ def self.to_s id: 2 } ] - }, email_serializer.new(email, {}).as_json) + }, actual) end def test_polymorphic_associations_are_included_at_root