Space code.

This commit is contained in:
José Valim 2011-12-01 07:42:08 +01:00
parent d72b66d4c5
commit 8bcb12289a

View File

@ -36,15 +36,15 @@ h3. The Most Basic Serializer
A basic serializer is a simple Ruby object named after the model class it is serializing. A basic serializer is a simple Ruby object named after the model class it is serializing.
<ruby> <ruby>
class PostSerializer class PostSerializer
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
end end
def as_json def as_json
{ post: { title: @post.name, body: @post.body } } { post: { title: @post.name, body: @post.body } }
end end
end end
</ruby> </ruby>
A serializer is initialized with two parameters: the model object it should serialize and an authorization scope. By default, the A serializer is initialized with two parameters: the model object it should serialize and an authorization scope. By default, the
@ -54,12 +54,12 @@ implements an +as_json+ method, which returns a Hash that will be sent to the JS
Rails will transparently use your serializer when you use +render :json+ in your controller. Rails will transparently use your serializer when you use +render :json+ in your controller.
<ruby> <ruby>
class PostsController < ApplicationController class PostsController < ApplicationController
def show def show
@post = Post.find(params[:id]) @post = Post.find(params[:id])
render json: @post render json: @post
end end
end end
</ruby> </ruby>
Because +respond_with+ uses +render :json+ under the hood for JSON requests, Rails will automatically use your serializer when Because +respond_with+ uses +render :json+ under the hood for JSON requests, Rails will automatically use your serializer when
@ -71,19 +71,19 @@ In general, you will want to implement +serializable_hash+ and +as_json+ to allo
directly. The easiest way to implement these two methods is to have +as_json+ call +serializable_hash+ and insert the root. directly. The easiest way to implement these two methods is to have +as_json+ call +serializable_hash+ and insert the root.
<ruby> <ruby>
class PostSerializer class PostSerializer
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
end end
def serializable_hash def serializable_hash
{ title: @post.name, body: @post.body } { title: @post.name, body: @post.body }
end end
def as_json def as_json
{ post: serializable_hash } { post: serializable_hash }
end end
end end
</ruby> </ruby>
h4. Authorization h4. Authorization
@ -92,34 +92,34 @@ Let's update our serializer to include the email address of the author of the po
access. access.
<ruby> <ruby>
class PostSerializer class PostSerializer
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
end end
def as_json def as_json
{ post: serializable_hash } { post: serializable_hash }
end end
def serializable_hash def serializable_hash
hash = post hash = post
hash.merge!(super_data) if super? hash.merge!(super_data) if super?
hash hash
end end
private private
def post def post
{ title: @post.name, body: @post.body } { title: @post.name, body: @post.body }
end end
def super_data def super_data
{ email: @post.email } { email: @post.email }
end end
def super? def super?
@scope.superuser? @scope.superuser?
end end
end end
</ruby> </ruby>
h4. Testing h4. Testing
@ -128,35 +128,35 @@ One benefit of encapsulating our objects this way is that it becomes extremely s
logic in isolation. logic in isolation.
<ruby> <ruby>
require "ostruct" require "ostruct"
class PostSerializerTest < ActiveSupport::TestCase class PostSerializerTest < ActiveSupport::TestCase
# For now, we use a very simple authorization structure. These tests will need # For now, we use a very simple authorization structure. These tests will need
# refactoring if we change that. # refactoring if we change that.
plebe = OpenStruct.new(super?: false) plebe = OpenStruct.new(super?: false)
god = OpenStruct.new(super?: true) god = OpenStruct.new(super?: true)
post = OpenStruct.new(title: "Welcome to my blog!", body: "Blah blah blah", email: "tenderlove@gmail.com") post = OpenStruct.new(title: "Welcome to my blog!", body: "Blah blah blah", email: "tenderlove@gmail.com")
test "a regular user sees just the title and body" do test "a regular user sees just the title and body" do
json = PostSerializer.new(post, plebe).to_json json = PostSerializer.new(post, plebe).to_json
hash = JSON.parse(json) hash = JSON.parse(json)
assert_equal post.title, hash.delete("title") assert_equal post.title, hash.delete("title")
assert_equal post.body, hash.delete("body") assert_equal post.body, hash.delete("body")
assert_empty hash assert_empty hash
end end
test "a superuser sees the title, body and email" do test "a superuser sees the title, body and email" do
json = PostSerializer.new(post, god).to_json json = PostSerializer.new(post, god).to_json
hash = JSON.parse(json) hash = JSON.parse(json)
assert_equal post.title, hash.delete("title") assert_equal post.title, hash.delete("title")
assert_equal post.body, hash.delete("body") assert_equal post.body, hash.delete("body")
assert_equal post.email, hash.delete("email") assert_equal post.email, hash.delete("email")
assert_empty hash assert_empty hash
end end
end end
</ruby> </ruby>
It's important to note that serializer objects define a clear interface specifically for serializing an existing object. It's important to note that serializer objects define a clear interface specifically for serializing an existing object.
@ -169,15 +169,15 @@ whether it is set. In general, you should document these requirements in your se
The documentation library +YARD+ provides excellent tools for describing this kind of requirement: The documentation library +YARD+ provides excellent tools for describing this kind of requirement:
<ruby> <ruby>
class PostSerializer class PostSerializer
# @param [~body, ~title, ~email] post the post to serialize # @param [~body, ~title, ~email] post the post to serialize
# @param [~super] scope the authorization scope for this serializer # @param [~super] scope the authorization scope for this serializer
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
end end
# ... # ...
end end
</ruby> </ruby>
h3. Attribute Sugar h3. Attribute Sugar
@ -190,28 +190,28 @@ JSON. In the above example, the +title+ and +body+ attributes were always includ
+ActiveModel::Serializer+ to simplify our post serializer. +ActiveModel::Serializer+ to simplify our post serializer.
<ruby> <ruby>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title, :body attributes :title, :body
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
end end
def serializable_hash def serializable_hash
hash = attributes hash = attributes
hash.merge!(super_data) if super? hash.merge!(super_data) if super?
hash hash
end end
private private
def super_data def super_data
{ email: @post.email } { email: @post.email }
end end
def super? def super?
@scope.superuser? @scope.superuser?
end end
end end
</ruby> </ruby>
First, we specified the list of included attributes at the top of the class. This will create an instance method called First, we specified the list of included attributes at the top of the class. This will create an instance method called
@ -224,20 +224,20 @@ earlier. We could also eliminate the +as_json+ method, as +ActiveModel::Serializ
us that calls our +serializable_hash+ method and inserts a root. But we can go a step further! us that calls our +serializable_hash+ method and inserts a root. But we can go a step further!
<ruby> <ruby>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title, :body attributes :title, :body
private private
def attributes def attributes
hash = super hash = super
hash.merge!(email: post.email) if super? hash.merge!(email: post.email) if super?
hash hash
end end
def super? def super?
@scope.superuser? @scope.superuser?
end end
end end
</ruby> </ruby>
The superclass provides a default +initialize+ method as well as a default +serializable_hash+ method, which uses The superclass provides a default +initialize+ method as well as a default +serializable_hash+ method, which uses
@ -252,38 +252,38 @@ In most JSON APIs, you will want to include associated objects with your seriali
the comments with the current post. the comments with the current post.
<ruby> <ruby>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title, :body attributes :title, :body
has_many :comments has_many :comments
private private
def attributes def attributes
hash = super hash = super
hash.merge!(email: post.email) if super? hash.merge!(email: post.email) if super?
hash hash
end end
def super? def super?
@scope.superuser? @scope.superuser?
end end
end end
</ruby> </ruby>
The default +serializable_hash+ method will include the comments as embedded objects inside the post. The default +serializable_hash+ method will include the comments as embedded objects inside the post.
<javascript> <javascript>
{ {
post: { post: {
title: "Hello Blog!", title: "Hello Blog!",
body: "This is my first post. Isn't it fabulous!", body: "This is my first post. Isn't it fabulous!",
comments: [ comments: [
{ {
title: "Awesome", title: "Awesome",
body: "Your first post is great" body: "Your first post is great"
}
]
} }
] }
}
}
</javascript> </javascript>
Rails uses the same logic to generate embedded serializations as it does when you use +render :json+. In this case, Rails uses the same logic to generate embedded serializations as it does when you use +render :json+. In this case,
@ -292,31 +292,31 @@ because you didn't define a +CommentSerializer+, Rails used the default +as_json
If you define a serializer, Rails will automatically instantiate it with the existing authorization scope. If you define a serializer, Rails will automatically instantiate it with the existing authorization scope.
<ruby> <ruby>
class CommentSerializer class CommentSerializer
def initialize(comment, scope) def initialize(comment, scope)
@comment, @scope = comment, scope @comment, @scope = comment, scope
end end
def serializable_hash def serializable_hash
{ title: @comment.title } { title: @comment.title }
end end
def as_json def as_json
{ comment: serializable_hash } { comment: serializable_hash }
end end
end end
</ruby> </ruby>
If we define the above comment serializer, the outputted JSON will change to: If we define the above comment serializer, the outputted JSON will change to:
<javascript> <javascript>
{ {
post: { post: {
title: "Hello Blog!", title: "Hello Blog!",
body: "This is my first post. Isn't it fabulous!", body: "This is my first post. Isn't it fabulous!",
comments: [{ title: "Awesome" }] comments: [{ title: "Awesome" }]
} }
} }
</javascript> </javascript>
Let's imagine that our comment system allows an administrator to kill a comment, and we only want to allow Let's imagine that our comment system allows an administrator to kill a comment, and we only want to allow
@ -325,25 +325,25 @@ users to see the comments they're entitled to see. By default, +has_many :commen
to just the comments we want to allow for the current user. to just the comments we want to allow for the current user.
<ruby> <ruby>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title. :body attributes :title. :body
has_many :comments has_many :comments
private private
def attributes def attributes
hash = super hash = super
hash.merge!(email: post.email) if super? hash.merge!(email: post.email) if super?
hash hash
end end
def comments def comments
post.comments_for(scope) post.comments_for(scope)
end end
def super? def super?
@scope.superuser? @scope.superuser?
end end
end end
</ruby> </ruby>
+ActiveModel::Serializer+ will still embed the comments, but this time it will use just the comments +ActiveModel::Serializer+ will still embed the comments, but this time it will use just the comments
@ -360,66 +360,66 @@ build up the hash manually.
For example, let's say our front-end expects the posts and comments in the following format: For example, let's say our front-end expects the posts and comments in the following format:
<plain> <plain>
{
post: {
id: 1
title: "Hello Blog!",
body: "This is my first post. Isn't it fabulous!",
comments: [1,2]
},
comments: [
{ {
id: 1 post: {
title: "Awesome", id: 1
body: "Your first post is great" title: "Hello Blog!",
}, body: "This is my first post. Isn't it fabulous!",
{ comments: [1,2]
id: 2 },
title: "Not so awesome", comments: [
body: "Why is it so short!" {
id: 1
title: "Awesome",
body: "Your first post is great"
},
{
id: 2
title: "Not so awesome",
body: "Why is it so short!"
}
]
} }
]
}
</plain> </plain>
We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments. We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments.
<ruby> <ruby>
class CommentSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer
attributes :id, :title, :body attributes :id, :title, :body
# define any logic for dealing with authorization-based attributes here # define any logic for dealing with authorization-based attributes here
end end
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title, :body attributes :title, :body
has_many :comments has_many :comments
def as_json def as_json
{ post: serializable_hash }.merge!(associations) { post: serializable_hash }.merge!(associations)
end end
def serializable_hash def serializable_hash
post_hash = attributes post_hash = attributes
post_hash.merge!(association_ids) post_hash.merge!(association_ids)
post_hash post_hash
end end
private private
def attributes def attributes
hash = super hash = super
hash.merge!(email: post.email) if super? hash.merge!(email: post.email) if super?
hash hash
end end
def comments def comments
post.comments_for(scope) post.comments_for(scope)
end end
def super? def super?
@scope.superuser? @scope.superuser?
end end
end end
</ruby> </ruby>
Here, we used two convenience methods: +associations+ and +association_ids+. The first, Here, we used two convenience methods: +associations+ and +association_ids+. The first,
@ -443,14 +443,14 @@ but only its title when requested as part of the post. To achieve this, you can
a serializer for associated objects nested inside the main serializer. a serializer for associated objects nested inside the main serializer.
<ruby> <ruby>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
class CommentSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer
attributes :id, :title attributes :id, :title
end end
# same as before # same as before
# ... # ...
end end
</ruby> </ruby>
In other words, if a +PostSerializer+ is trying to serialize comments, it will first In other words, if a +PostSerializer+ is trying to serialize comments, it will first
@ -469,9 +469,9 @@ If you want to change that behavior, simply use the +serialization_scope+ class
method. method.
<ruby> <ruby>
class PostsController < ApplicationController class PostsController < ApplicationController
serialization_scope :current_app serialization_scope :current_app
end end
</ruby> </ruby>
You can also implement an instance method called (no surprise) +serialization_scope+, You can also implement an instance method called (no surprise) +serialization_scope+,
@ -520,36 +520,36 @@ as the root).
For example, an Array of post objects would serialize as: For example, an Array of post objects would serialize as:
<plain> <plain>
{
posts: [
{ {
title: "FIRST POST!", posts: [
body: "It's my first pooooost" {
}, title: "FIRST POST!",
{ title: "Second post!", body: "It's my first pooooost"
body: "Zomg I made it to my second post" },
{ title: "Second post!",
body: "Zomg I made it to my second post"
}
]
} }
]
}
</plain> </plain>
If you want to change the behavior of serialized Arrays, you need to create If you want to change the behavior of serialized Arrays, you need to create
a custom Array serializer. a custom Array serializer.
<ruby> <ruby>
class ArraySerializer < ActiveModel::ArraySerializer class ArraySerializer < ActiveModel::ArraySerializer
def serializable_array def serializable_array
serializers.map do |serializer| serializers.map do |serializer|
serializer.serializable_hash serializer.serializable_hash
end end
end end
def as_json def as_json
hash = { root => serializable_array } hash = { root => serializable_array }
hash.merge!(associations) hash.merge!(associations)
hash hash
end end
end end
</ruby> </ruby>
When generating embedded associations using the +associations+ helper inside a When generating embedded associations using the +associations+ helper inside a