Did I already express my hate for textile today?

This commit is contained in:
José Valim 2011-12-01 07:48:56 +01:00
parent 51c07d0421
commit 45d3bb94fa

View File

@ -11,6 +11,8 @@ This guide describes how to use Active Model serializers to build non-trivial JS
This guide covers an intermediate topic and assumes familiarity with Rails conventions. It is suitable for applications that expose a This guide covers an intermediate topic and assumes familiarity with Rails conventions. It is suitable for applications that expose a
JSON API that may return different results based on the authorization status of the user. JSON API that may return different results based on the authorization status of the user.
endprologue.
h3. Serialization h3. Serialization
By default, Active Record objects can serialize themselves into JSON by using the `to_json` method. This method takes a series of additional By default, Active Record objects can serialize themselves into JSON by using the `to_json` method. This method takes a series of additional
@ -35,7 +37,7 @@ 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 <code><pre>
class PostSerializer class PostSerializer
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
@ -45,7 +47,7 @@ class PostSerializer
{ post: { title: @post.name, body: @post.body } } { post: { title: @post.name, body: @post.body } }
end end
end end
@@@ </pre></code>
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
authorization scope is the current user (+current_user+) but you can use a different object if you want. The serializer also authorization scope is the current user (+current_user+) but you can use a different object if you want. The serializer also
@ -53,14 +55,14 @@ 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 <code><pre>
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
@@@ </pre></code>
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
you use +respond_with+ as well. you use +respond_with+ as well.
@ -70,7 +72,7 @@ h4. +serializable_hash+
In general, you will want to implement +serializable_hash+ and +as_json+ to allow serializers to embed associated content In general, you will want to implement +serializable_hash+ and +as_json+ to allow serializers to embed associated content
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 <code><pre>
class PostSerializer class PostSerializer
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
@ -84,14 +86,14 @@ class PostSerializer
{ post: serializable_hash } { post: serializable_hash }
end end
end end
@@@ </pre></code>
h4. Authorization h4. Authorization
Let's update our serializer to include the email address of the author of the post, but only if the current user has superuser Let's update our serializer to include the email address of the author of the post, but only if the current user has superuser
access. access.
@@@ruby <code><pre>
class PostSerializer class PostSerializer
def initialize(post, scope) def initialize(post, scope)
@post, @scope = post, scope @post, @scope = post, scope
@ -120,14 +122,14 @@ private
@scope.superuser? @scope.superuser?
end end
end end
@@@ </pre></code>
h4. Testing h4. Testing
One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization
logic in isolation. logic in isolation.
@@@ruby <code><pre>
require "ostruct" require "ostruct"
class PostSerializerTest < ActiveSupport::TestCase class PostSerializerTest < ActiveSupport::TestCase
@ -157,7 +159,7 @@ class PostSerializerTest < ActiveSupport::TestCase
assert_empty hash assert_empty hash
end end
end end
@@@ </pre></code>
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.
In this case, the serializer expects to receive a post object with +name+, +body+ and +email+ attributes and an authorization In this case, the serializer expects to receive a post object with +name+, +body+ and +email+ attributes and an authorization
@ -168,7 +170,7 @@ the serializer doesn't need to concern itself with how the authorization scope d
whether it is set. In general, you should document these requirements in your serializer files and programatically via tests. whether it is set. In general, you should document these requirements in your serializer files and programatically via tests.
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 <code><pre>
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
@ -178,7 +180,7 @@ class PostSerializer
# ... # ...
end end
@@@ </pre></code>
h3. Attribute Sugar h3. Attribute Sugar
@ -189,7 +191,7 @@ For example, you will sometimes want to simply include a number of existing attr
JSON. In the above example, the +title+ and +body+ attributes were always included in the JSON. Let's see how to use JSON. In the above example, the +title+ and +body+ attributes were always included in the JSON. Let's see how to use
+ActiveModel::Serializer+ to simplify our post serializer. +ActiveModel::Serializer+ to simplify our post serializer.
@@@ruby <code><pre>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title, :body attributes :title, :body
@ -212,7 +214,7 @@ private
@scope.superuser? @scope.superuser?
end end
end end
@@@ </pre></code>
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
+attributes+ that extracts those attributes from the post model. +attributes+ that extracts those attributes from the post model.
@ -223,7 +225,7 @@ Next, we use the attributes methood in our +serializable_hash+ method, which all
earlier. We could also eliminate the +as_json+ method, as +ActiveModel::Serializer+ provides a default +as_json+ method for earlier. We could also eliminate the +as_json+ method, as +ActiveModel::Serializer+ provides a default +as_json+ method for
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 <code><pre>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title, :body attributes :title, :body
@ -238,7 +240,7 @@ private
@scope.superuser? @scope.superuser?
end end
end end
@@@ </pre></code>
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
+attributes+. We can call +super+ to get the hash based on the attributes we declared, and then add in any additional +attributes+. We can call +super+ to get the hash based on the attributes we declared, and then add in any additional
@ -251,7 +253,7 @@ h3. Associations
In most JSON APIs, you will want to include associated objects with your serialized object. In this case, let's include In most JSON APIs, you will want to include associated objects with your serialized object. In this case, let's include
the comments with the current post. the comments with the current post.
@@@ruby <code><pre>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title, :body attributes :title, :body
has_many :comments has_many :comments
@ -267,11 +269,11 @@ private
@scope.superuser? @scope.superuser?
end end
end end
@@@ </pre></code>
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 <code><pre>
{ {
post: { post: {
title: "Hello Blog!", title: "Hello Blog!",
@ -284,14 +286,14 @@ The default +serializable_hash+ method will include the comments as embedded obj
] ]
} }
} }
@@@ </pre></code>
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,
because you didn't define a +CommentSerializer+, Rails used the default +as_json+ on your comment object. because you didn't define a +CommentSerializer+, Rails used the default +as_json+ on your comment object.
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 <code><pre>
class CommentSerializer class CommentSerializer
def initialize(comment, scope) def initialize(comment, scope)
@comment, @scope = comment, scope @comment, @scope = comment, scope
@ -305,11 +307,11 @@ class CommentSerializer
{ comment: serializable_hash } { comment: serializable_hash }
end end
end end
@@@ </pre></code>
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 <code><pre>
{ {
post: { post: {
title: "Hello Blog!", title: "Hello Blog!",
@ -317,14 +319,14 @@ If we define the above comment serializer, the outputted JSON will change to:
comments: [{ title: "Awesome" }] comments: [{ title: "Awesome" }]
} }
} }
@@@ </pre></code>
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
users to see the comments they're entitled to see. By default, +has_many :comments+ will simply use the users to see the comments they're entitled to see. By default, +has_many :comments+ will simply use the
+comments+ accessor on the post object. We can override the +comments+ accessor to limit the comments used +comments+ accessor on the post object. We can override the +comments+ accessor to limit the comments used
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 <code><pre>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
attributes :title. :body attributes :title. :body
has_many :comments has_many :comments
@ -344,7 +346,7 @@ private
@scope.superuser? @scope.superuser?
end end
end end
@@@ </pre></code>
+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
for the current user. for the current user.
@ -359,7 +361,7 @@ 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 <code><pre>
{ {
post: { post: {
id: 1 id: 1
@ -380,11 +382,11 @@ For example, let's say our front-end expects the posts and comments in the follo
} }
] ]
} }
@@@ </pre></code>
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 <code><pre>
class CommentSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer
attributes :id, :title, :body attributes :id, :title, :body
@ -420,7 +422,7 @@ private
@scope.superuser? @scope.superuser?
end end
end end
@@@ </pre></code>
Here, we used two convenience methods: +associations+ and +association_ids+. The first, Here, we used two convenience methods: +associations+ and +association_ids+. The first,
+associations+, creates a hash of all of the define associations, using their defined +associations+, creates a hash of all of the define associations, using their defined
@ -442,7 +444,7 @@ For instance, we might want to provide the full comment when it is requested dir
but only its title when requested as part of the post. To achieve this, you can define but only its title when requested as part of the post. To achieve this, you can define
a serializer for associated objects nested inside the main serializer. a serializer for associated objects nested inside the main serializer.
@@@ruby <code><pre>
class PostSerializer < ActiveModel::Serializer class PostSerializer < ActiveModel::Serializer
class CommentSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer
attributes :id, :title attributes :id, :title
@ -451,7 +453,7 @@ class PostSerializer < ActiveModel::Serializer
# same as before # same as before
# ... # ...
end end
@@@ </pre></code>
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
look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+ look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+
@ -468,11 +470,11 @@ its +current_user+ method and pass that along to the serializer's initializer.
If you want to change that behavior, simply use the +serialization_scope+ class If you want to change that behavior, simply use the +serialization_scope+ class
method. method.
@@@ruby <code><pre>
class PostsController < ApplicationController class PostsController < ApplicationController
serialization_scope :current_app serialization_scope :current_app
end end
@@@ </pre></code>
You can also implement an instance method called (no surprise) +serialization_scope+, You can also implement an instance method called (no surprise) +serialization_scope+,
which allows you to define a dynamic authorization scope based on the current request. which allows you to define a dynamic authorization scope based on the current request.
@ -489,19 +491,19 @@ outside a request.
For instance, if you want to generate the JSON representation of a post for a user outside For instance, if you want to generate the JSON representation of a post for a user outside
of a request: of a request:
@@@ruby <code><pre>
user = get_user # some logic to get the user in question user = get_user # some logic to get the user in question
PostSerializer.new(post, user).to_json # reliably generate JSON output PostSerializer.new(post, user).to_json # reliably generate JSON output
@@@ </pre></code>
If you want to generate JSON for an anonymous user, you should be able to use whatever If you want to generate JSON for an anonymous user, you should be able to use whatever
technique you use in your application to generate anonymous users outside of a request. technique you use in your application to generate anonymous users outside of a request.
Typically, that means creating a new user and not saving it to the database: Typically, that means creating a new user and not saving it to the database:
@@@ruby <code><pre>
user = User.new # create a new anonymous user user = User.new # create a new anonymous user
PostSerializer.new(post, user).to_json PostSerializer.new(post, user).to_json
@@@ </pre></code>
In general, the better you encapsulate your authorization logic, the more easily you In general, the better you encapsulate your authorization logic, the more easily you
will be able to use the serializer outside of the context of a request. For instance, will be able to use the serializer outside of the context of a request. For instance,
@ -519,7 +521,7 @@ as the root).
For example, an Array of post objects would serialize as: For example, an Array of post objects would serialize as:
@@@plain <code><pre>
{ {
posts: [ posts: [
{ {
@ -531,12 +533,12 @@ For example, an Array of post objects would serialize as:
} }
] ]
} }
@@@ </pre></code>
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 <code><pre>
class ArraySerializer < ActiveModel::ArraySerializer class ArraySerializer < ActiveModel::ArraySerializer
def serializable_array def serializable_array
serializers.map do |serializer| serializers.map do |serializer|
@ -550,7 +552,7 @@ class ArraySerializer < ActiveModel::ArraySerializer
hash hash
end end
end end
@@@ </pre></code>
When generating embedded associations using the +associations+ helper inside a When generating embedded associations using the +associations+ helper inside a
regular serializer, it will create a new <code>ArraySerializer</code> with the regular serializer, it will create a new <code>ArraySerializer</code> with the