Merge pull request #609 from polmiro/feature/support_polymorphic_associations_v2

Feature/support polymorphic associations v2
This commit is contained in:
Steve Klabnik 2014-08-26 11:38:16 -04:00
commit a04ae2c2e2
9 changed files with 583 additions and 64 deletions

View File

@ -1,12 +1,12 @@
[![Build Status](https://api.travis-ci.org/rails-api/active_model_serializers.png?branch=0-9-stable)](https://travis-ci.org/rails-api/active_model_serializers) [![Build Status](https://api.travis-ci.org/rails-api/active_model_serializers.png?branch=0-9-stable)](https://travis-ci.org/rails-api/active_model_serializers)
[![Code Climate](https://codeclimate.com/github/rails-api/active_model_serializers.png)](https://codeclimate.com/github/rails-api/active_model_serializers) [![Code Climate](https://codeclimate.com/github/rails-api/active_model_serializers.png)](https://codeclimate.com/github/rails-api/active_model_serializers)
# ActiveModel::Serializers # ActiveModel::Serializers
## Purpose ## Purpose
`ActiveModel::Serializers` encapsulates the JSON serialization of objects. `ActiveModel::Serializers` encapsulates the JSON serialization of objects.
Objects that respond to read\_attribute\_for\_serialization Objects that respond to read\_attribute\_for\_serialization
(including `ActiveModel` and `ActiveRecord` objects) are supported. (including `ActiveModel` and `ActiveRecord` objects) are supported.
Serializers know about both a model and the `current_user`, so you can Serializers know about both a model and the `current_user`, so you can
@ -229,7 +229,7 @@ ActiveModel::Serializer.setup do |config|
config.key_format = :lower_camel config.key_format = :lower_camel
end end
class BlogLowerCamelSerializer < ActiveModel::Serializer class BlogLowerCamelSerializer < ActiveModel::Serializer
format_keys :lower_camel format_keys :lower_camel
end end
@ -467,9 +467,6 @@ You may also use the `:serializer` option to specify a custom serializer class a
Serializers are only concerned with multiplicity, and not ownership. `belongs_to` ActiveRecord associations can be included using `has_one` in your serializer. Serializers are only concerned with multiplicity, and not ownership. `belongs_to` ActiveRecord associations can be included using `has_one` in your serializer.
NOTE: polymorphic was removed because was only supported for has\_one
associations and is in the TODO list of the project.
## Embedding Associations ## Embedding Associations
By default, associations will be embedded inside the serialized object. So if By default, associations will be embedded inside the serialized object. So if
@ -646,8 +643,8 @@ The above would yield the following JSON document:
} }
``` ```
When side-loading data, your serializer cannot have the `{ root: false }` option, When side-loading data, your serializer cannot have the `{ root: false }` option,
as this would lead to invalid JSON. If you do not have a root key, the `include` as this would lead to invalid JSON. If you do not have a root key, the `include`
instruction will be ignored instruction will be ignored
You can also specify a different root for the embedded objects than the key You can also specify a different root for the embedded objects than the key
@ -714,6 +711,78 @@ data looking for information, is extremely useful.
If you are mostly working with the data in simple scenarios and manually making If you are mostly working with the data in simple scenarios and manually making
Ajax requests, you probably just want to use the default embedded behavior. Ajax requests, you probably just want to use the default embedded behavior.
## Embedding Polymorphic Associations
Because we need both the id and the type to be able to identify a polymorphic associated model, these are serialized in a slightly different format than common ones.
When embedding entire objects:
```ruby
class PostSerializer < ActiveModel::Serializer
attributes :id, :title
has_many :attachments, polymorphic: true
end
```
```json
{
"post": {
"id": 1,
"title": "New post",
"attachments": [
{
"type": "image"
"image": {
"id": 3
"name": "logo"
"url": "http://images.com/logo.jpg"
}
},
{
"type": "video"
"video": {
"id": 12
"uid": "XCSSMDFWW"
"source": "youtube"
}
}
]
}
}
```
When embedding ids:
```ruby
class PostSerializer < ActiveModel::Serializer
embed :ids
attributes :id, :title
has_many :attachments, polymorphic: true
end
```
```json
{
"post": {
"id": 1,
"title": "New post",
"attachment_ids": [
{
"type": "image"
"id": 12
},
{
"type": "video"
"id": 3
}
]
}
}
```
## Customizing Scope ## Customizing Scope
In a serializer, `current_user` is the current authorization scope which the controller In a serializer, `current_user` is the current authorization scope which the controller

View File

@ -15,6 +15,7 @@ module ActiveModel
@object = object @object = object
@scope = options[:scope] @scope = options[:scope]
@root = options.fetch(:root, self.class._root) @root = options.fetch(:root, self.class._root)
@polymorphic = options.fetch(:polymorphic, false)
@meta_key = options[:meta_key] || :meta @meta_key = options[:meta_key] || :meta
@meta = options[@meta_key] @meta = options[@meta_key]
@each_serializer = options[:each_serializer] @each_serializer = options[:each_serializer]
@ -33,7 +34,7 @@ module ActiveModel
def serializer_for(item) def serializer_for(item)
serializer_class = @each_serializer || Serializer.serializer_for(item) || DefaultSerializer serializer_class = @each_serializer || Serializer.serializer_for(item) || DefaultSerializer
serializer_class.new(item, scope: scope, key_format: key_format, only: @only, except: @except) serializer_class.new(item, scope: scope, key_format: key_format, only: @only, except: @except, polymorphic: @polymorphic)
end end
def serializable_object def serializable_object

View File

@ -1,6 +1,6 @@
require 'active_model/array_serializer' require 'active_model/array_serializer'
require 'active_model/serializable' require 'active_model/serializable'
require 'active_model/serializer/associations' require 'active_model/serializer/association'
require 'active_model/serializer/config' require 'active_model/serializer/config'
require 'thread' require 'thread'
@ -130,6 +130,7 @@ end
@object = object @object = object
@scope = options[:scope] @scope = options[:scope]
@root = options.fetch(:root, self.class._root) @root = options.fetch(:root, self.class._root)
@polymorphic = options.fetch(:polymorphic, false)
@meta_key = options[:meta_key] || :meta @meta_key = options[:meta_key] || :meta
@meta = options[@meta_key] @meta = options[@meta_key]
@wrap_in_array = options[:_wrap_in_array] @wrap_in_array = options[:_wrap_in_array]
@ -138,7 +139,7 @@ end
@key_format = options[:key_format] @key_format = options[:key_format]
@context = options[:context] @context = options[:context]
end end
attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format, :context attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format, :context, :polymorphic
def json_key def json_key
key = if root == true || root.nil? key = if root == true || root.nil?
@ -225,9 +226,9 @@ end
def serialize_ids(association) def serialize_ids(association)
associated_data = send(association.name) associated_data = send(association.name)
if associated_data.respond_to?(:to_ary) if associated_data.respond_to?(:to_ary)
associated_data.map { |elem| elem.read_attribute_for_serialization(association.embed_key) } associated_data.map { |elem| serialize_id(elem, association) }
else else
associated_data.read_attribute_for_serialization(association.embed_key) if associated_data serialize_id(associated_data, association) if associated_data
end end
end end
@ -260,9 +261,19 @@ end
hash = attributes hash = attributes
hash.merge! associations hash.merge! associations
hash = convert_keys(hash) if key_format.present? hash = convert_keys(hash) if key_format.present?
hash = { :type => type_name(@object), type_name(@object) => hash } if @polymorphic
@wrap_in_array ? [hash] : hash @wrap_in_array ? [hash] : hash
end end
alias_method :serializable_hash, :serializable_object alias_method :serializable_hash, :serializable_object
def serialize_id(elem, association)
id = elem.read_attribute_for_serialization(association.embed_key)
association.polymorphic? ? { id: id, type: type_name(elem) } : id
end
def type_name(elem)
elem.class.to_s.demodulize.underscore.to_sym
end
end end
end end

View File

@ -1,4 +1,6 @@
require 'active_model/default_serializer' require 'active_model/default_serializer'
require 'active_model/serializer/association/has_one'
require 'active_model/serializer/association/has_many'
module ActiveModel module ActiveModel
class Serializer class Serializer
@ -13,6 +15,7 @@ module ActiveModel
@name = name.to_s @name = name.to_s
@options = options @options = options
self.embed = options.fetch(:embed) { CONFIG.embed } self.embed = options.fetch(:embed) { CONFIG.embed }
@polymorphic = options.fetch(:polymorphic, false)
@embed_in_root = options.fetch(:embed_in_root) { options.fetch(:include) { CONFIG.embed_in_root } } @embed_in_root = options.fetch(:embed_in_root) { options.fetch(:include) { CONFIG.embed_in_root } }
@key_format = options.fetch(:key_format) { CONFIG.key_format } @key_format = options.fetch(:key_format) { CONFIG.key_format }
@embed_key = options[:embed_key] || :id @embed_key = options[:embed_key] || :id
@ -25,13 +28,14 @@ module ActiveModel
@serializer_from_options = serializer.is_a?(String) ? serializer.constantize : serializer @serializer_from_options = serializer.is_a?(String) ? serializer.constantize : serializer
end end
attr_reader :name, :embed_ids, :embed_objects attr_reader :name, :embed_ids, :embed_objects, :polymorphic
attr_accessor :embed_in_root, :embed_key, :key, :embedded_key, :root_key, :serializer_from_options, :options, :key_format, :embed_in_root_key, :embed_namespace attr_accessor :embed_in_root, :embed_key, :key, :embedded_key, :root_key, :serializer_from_options, :options, :key_format, :embed_in_root_key, :embed_namespace
alias embed_ids? embed_ids alias embed_ids? embed_ids
alias embed_objects? embed_objects alias embed_objects? embed_objects
alias embed_in_root? embed_in_root alias embed_in_root? embed_in_root
alias embed_in_root_key? embed_in_root_key alias embed_in_root_key? embed_in_root_key
alias embed_namespace? embed_namespace alias embed_namespace? embed_namespace
alias polymorphic? polymorphic
def embed=(embed) def embed=(embed)
@embed_ids = embed == :id || embed == :ids @embed_ids = embed == :id || embed == :ids
@ -49,54 +53,6 @@ module ActiveModel
def build_serializer(object, options = {}) def build_serializer(object, options = {})
serializer_class(object).new(object, options.merge(self.options)) serializer_class(object).new(object, options.merge(self.options))
end end
class HasOne < Association
def initialize(name, *args)
super
@root_key = @embedded_key.to_s.pluralize
@key ||= "#{name}_id"
end
def serializer_class(object)
serializer_from_options || serializer_from_object(object) || default_serializer
end
def build_serializer(object, options = {})
options[:_wrap_in_array] = embed_in_root?
super
end
end
class HasMany < Association
def initialize(name, *args)
super
@root_key = @embedded_key
@key ||= "#{name.to_s.singularize}_ids"
end
def serializer_class(object)
if use_array_serializer?
ArraySerializer
else
serializer_from_options
end
end
def options
if use_array_serializer?
{ each_serializer: serializer_from_options }.merge! super
else
super
end
end
private
def use_array_serializer?
!serializer_from_options ||
serializer_from_options && !(serializer_from_options <= ArraySerializer)
end
end
end end
end end
end end

View File

@ -0,0 +1,36 @@
module ActiveModel
class Serializer
class Association
class HasMany < Association
def initialize(name, *args)
super
@root_key = @embedded_key
@key ||= "#{name.to_s.singularize}_ids"
end
def serializer_class(object)
if use_array_serializer?
ArraySerializer
else
serializer_from_options
end
end
def options
if use_array_serializer?
{ each_serializer: serializer_from_options }.merge! super
else
super
end
end
private
def use_array_serializer?
!serializer_from_options ||
serializer_from_options && !(serializer_from_options <= ArraySerializer)
end
end
end
end
end

View File

@ -0,0 +1,22 @@
module ActiveModel
class Serializer
class Association
class HasOne < Association
def initialize(name, *args)
super
@root_key = @embedded_key.to_s.pluralize
@key ||= "#{name}_id"
end
def serializer_class(object)
serializer_from_options || serializer_from_object(object) || default_serializer
end
def build_serializer(object, options = {})
options[:_wrap_in_array] = embed_in_root?
super
end
end
end
end
end

39
test/fixtures/poro.rb vendored
View File

@ -38,6 +38,25 @@ end
class WebLog < Model class WebLog < Model
end end
class Interview < Model
def attachment
@attachment ||= Image.new(url: 'U1')
end
end
class Mail < Model
def attachments
@attachments ||= [Image.new(url: 'U1'),
Video.new(html: 'H1')]
end
end
class Image < Model
end
class Video < Model
end
### ###
## Serializers ## Serializers
### ###
@ -73,3 +92,23 @@ end
class WebLogLowerCamelSerializer < WebLogSerializer class WebLogLowerCamelSerializer < WebLogSerializer
format_keys :lower_camel format_keys :lower_camel
end end
class InterviewSerializer < ActiveModel::Serializer
attributes :text
has_one :attachment, polymorphic: true
end
class MailSerializer < ActiveModel::Serializer
attributes :body
has_many :attachments, polymorphic: true
end
class ImageSerializer < ActiveModel::Serializer
attributes :url
end
class VideoSerializer < ActiveModel::Serializer
attributes :html
end

View File

@ -0,0 +1,189 @@
require 'test_helper'
module ActiveModel
class Serializer
class HasManyPolymorphicTest < ActiveModel::TestCase
def setup
@association = MailSerializer._associations[:attachments]
@old_association = @association.dup
@mail = Mail.new({ body: 'Body 1' })
@mail_serializer = MailSerializer.new(@mail)
end
def teardown
MailSerializer._associations[:attachments] = @old_association
end
def model_name(object)
object.class.to_s.demodulize.underscore.to_sym
end
def test_associations_definition
assert_equal 1, MailSerializer._associations.length
assert_kind_of Association::HasMany, @association
assert_equal true, @association.polymorphic
assert_equal 'attachments', @association.name
end
def test_associations_embedding_ids_serialization_using_serializable_hash
@association.embed = :ids
assert_equal({
body: 'Body 1',
'attachment_ids' => @mail.attachments.map do |c|
{ id: c.object_id, type: model_name(c) }
end
}, @mail_serializer.serializable_hash)
end
def test_associations_embedding_ids_serialization_using_as_json
@association.embed = :ids
assert_equal({
'mail' => {
:body => 'Body 1',
'attachment_ids' => @mail.attachments.map do |c|
{ id: c.object_id, type: model_name(c) }
end
}
}, @mail_serializer.as_json)
end
def test_associations_embedding_ids_serialization_using_serializable_hash_and_key_from_options
@association.embed = :ids
@association.key = 'key'
assert_equal({
body: 'Body 1',
'key' => @mail.attachments.map do |c|
{ id: c.object_id, type: model_name(c) }
end
}, @mail_serializer.serializable_hash)
end
def test_associations_embedding_objects_serialization_using_serializable_hash
@association.embed = :objects
assert_equal({
body: 'Body 1',
:attachments => [
{ type: :image, image: { url: 'U1' }},
{ type: :video, video: { html: 'H1' }}
]
}, @mail_serializer.serializable_hash)
end
def test_associations_embedding_objects_serialization_using_as_json
@association.embed = :objects
assert_equal({
'mail' => {
body: 'Body 1',
attachments: [
{ type: :image, image: { url: 'U1' }},
{ type: :video, video: { html: 'H1' }}
]
}
}, @mail_serializer.as_json)
end
def test_associations_embedding_nil_objects_serialization_using_as_json
@association.embed = :objects
@mail.instance_eval do
def attachments
[nil]
end
end
assert_equal({
'mail' => {
:body => 'Body 1',
:attachments => [nil]
}
}, @mail_serializer.as_json)
end
def test_associations_embedding_objects_serialization_using_serializable_hash_and_root_from_options
@association.embed = :objects
@association.embedded_key = 'root'
assert_equal({
body: 'Body 1',
'root' => [
{ type: :image, image: { url: 'U1' }},
{ type: :video, video: { html: 'H1' }}
]
}, @mail_serializer.serializable_hash)
end
def test_associations_embedding_ids_including_objects_serialization_using_serializable_hash
@association.embed = :ids
@association.embed_in_root = true
assert_equal({
body: 'Body 1',
'attachment_ids' => @mail.attachments.map do |c|
{ id: c.object_id, type: model_name(c) }
end
}, @mail_serializer.serializable_hash)
end
def test_associations_embedding_ids_including_objects_serialization_using_as_json
@association.embed = :ids
@association.embed_in_root = true
assert_equal({
'mail' => {
body: 'Body 1',
'attachment_ids' => @mail.attachments.map do |c|
{ id: c.object_id, type: model_name(c) }
end,
},
attachments: [
{ type: :image, image: { url: 'U1' }},
{ type: :video, video: { html: 'H1' }}
]
}, @mail_serializer.as_json)
end
def test_associations_embedding_nothing_including_objects_serialization_using_as_json
@association.embed = nil
@association.embed_in_root = true
assert_equal({
'mail' => { body: 'Body 1' },
attachments: [
{ type: :image, image: { url: 'U1' }},
{ type: :video, video: { html: 'H1' }}
]
}, @mail_serializer.as_json)
end
def test_associations_using_a_given_serializer
@association.embed = :ids
@association.embed_in_root = true
@association.serializer_from_options = Class.new(ActiveModel::Serializer) do
def fake
'fake'
end
attributes :fake
end
assert_equal({
'mail' => {
body: 'Body 1',
'attachment_ids' => @mail.attachments.map do |c|
{ id: c.object_id, type: model_name(c) }
end
},
attachments: [
{ type: :image, image: { fake: 'fake' }},
{ type: :video, video: { fake: 'fake' }}
]
}, @mail_serializer.as_json)
end
end
end
end

View File

@ -0,0 +1,196 @@
require 'test_helper'
module ActiveModel
class Serializer
class HasOnePolymorphicTest < ActiveModel::TestCase
def setup
@association = InterviewSerializer._associations[:attachment]
@old_association = @association.dup
@interview = Interview.new({ text: 'Text 1' })
@interview_serializer = InterviewSerializer.new(@interview)
end
def teardown
InterviewSerializer._associations[:attachment] = @old_association
end
def model_name(object)
object.class.to_s.demodulize.underscore.to_sym
end
def test_associations_definition
assert_equal 1, InterviewSerializer._associations.length
assert_kind_of Association::HasOne, @association
assert_equal true, @association.polymorphic
assert_equal 'attachment', @association.name
end
def test_associations_embedding_ids_serialization_using_serializable_hash
@association.embed = :ids
assert_equal({
text: 'Text 1',
'attachment_id' => {
type: model_name(@interview.attachment),
id: @interview.attachment.object_id
}
}, @interview_serializer.serializable_hash)
end
def test_associations_embedding_ids_serialization_using_as_json
@association.embed = :ids
assert_equal({
'interview' => {
text: 'Text 1',
'attachment_id' => {
type: model_name(@interview.attachment),
id: @interview.attachment.object_id
}
}
}, @interview_serializer.as_json)
end
def test_associations_embedding_ids_serialization_using_serializable_hash_and_key_from_options
@association.embed = :ids
@association.key = 'key'
assert_equal({
text: 'Text 1',
'key' => {
type: model_name(@interview.attachment),
id: @interview.attachment.object_id
}
}, @interview_serializer.serializable_hash)
end
def test_associations_embedding_objects_serialization_using_serializable_hash
@association.embed = :objects
assert_equal({
text: 'Text 1',
attachment: {
type: model_name(@interview.attachment),
model_name(@interview.attachment) => { url: 'U1'}
}
}, @interview_serializer.serializable_hash)
end
def test_associations_embedding_objects_serialization_using_as_json
@association.embed = :objects
assert_equal({
'interview' => {
text: 'Text 1',
attachment: {
type: model_name(@interview.attachment),
model_name(@interview.attachment) => { url: 'U1'}
}
}
}, @interview_serializer.as_json)
end
def test_associations_embedding_nil_ids_serialization_using_as_json
@association.embed = :ids
@interview.instance_eval do
def attachment
nil
end
end
assert_equal({
'interview' => { text: 'Text 1', 'attachment_id' => nil }
}, @interview_serializer.as_json)
end
def test_associations_embedding_nil_objects_serialization_using_as_json
@association.embed = :objects
@interview.instance_eval do
def attachment
nil
end
end
assert_equal({
'interview' => { text: 'Text 1', attachment: nil }
}, @interview_serializer.as_json)
end
def test_associations_embedding_objects_serialization_using_serializable_hash_and_root_from_options
@association.embed = :objects
@association.embedded_key = 'root'
assert_equal({
text: 'Text 1',
'root' => {
type: model_name(@interview.attachment),
model_name(@interview.attachment) => { url: 'U1'}
}
}, @interview_serializer.serializable_hash)
end
def test_associations_embedding_ids_including_objects_serialization_using_serializable_hash
@association.embed = :ids
@association.embed_in_root = true
assert_equal({
text: 'Text 1',
'attachment_id' => {
type: model_name(@interview.attachment),
id: @interview.attachment.object_id
}
}, @interview_serializer.serializable_hash)
end
def test_associations_embedding_ids_including_objects_serialization_using_as_json
@association.embed = :ids
@association.embed_in_root = true
assert_equal({
'interview' => {
text: 'Text 1',
'attachment_id' => {
type: model_name(@interview.attachment),
id: @interview.attachment.object_id
}
},
"attachments" => [{
type: model_name(@interview.attachment),
model_name(@interview.attachment) => {
url: 'U1'
}
}]
}, @interview_serializer.as_json)
end
def test_associations_using_a_given_serializer
@association.embed = :ids
@association.embed_in_root = true
@association.serializer_from_options = Class.new(ActiveModel::Serializer) do
def name
'fake'
end
attributes :name
end
assert_equal({
'interview' => {
text: 'Text 1',
'attachment_id' => {
type: model_name(@interview.attachment),
id: @interview.attachment.object_id
}
},
"attachments" => [{
type: model_name(@interview.attachment),
model_name(@interview.attachment) => {
name: 'fake'
}
}]
}, @interview_serializer.as_json)
end
end
end
end