diff --git a/README.textile b/README.textile index e376227b..0fd02ebc 100644 --- a/README.textile +++ b/README.textile @@ -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. -class PostSerializer - def initialize(post, scope) - @post, @scope = post, scope - end + class PostSerializer + def initialize(post, scope) + @post, @scope = post, scope + end - def as_json - { post: { title: @post.name, body: @post.body } } - end -end + def as_json + { post: { title: @post.name, body: @post.body } } + end + end 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. -class PostsController < ApplicationController - def show - @post = Post.find(params[:id]) - render json: @post - end -end + class PostsController < ApplicationController + def show + @post = Post.find(params[:id]) + render json: @post + end + end 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. -class PostSerializer - def initialize(post, scope) - @post, @scope = post, scope - end + class PostSerializer + def initialize(post, scope) + @post, @scope = post, scope + end - def serializable_hash - { title: @post.name, body: @post.body } - end + def serializable_hash + { title: @post.name, body: @post.body } + end - def as_json - { post: serializable_hash } - end -end + def as_json + { post: serializable_hash } + end + end h4. Authorization @@ -92,34 +92,34 @@ Let's update our serializer to include the email address of the author of the po access. -class PostSerializer - def initialize(post, scope) - @post, @scope = post, scope - end + class PostSerializer + def initialize(post, scope) + @post, @scope = post, scope + end - def as_json - { post: serializable_hash } - end + def as_json + { post: serializable_hash } + end - def serializable_hash - hash = post - hash.merge!(super_data) if super? - hash - end + def serializable_hash + hash = post + hash.merge!(super_data) if super? + hash + end -private - def post - { title: @post.name, body: @post.body } - end + private + def post + { title: @post.name, body: @post.body } + end - def super_data - { email: @post.email } - end + def super_data + { email: @post.email } + end - def super? - @scope.superuser? - end -end + def super? + @scope.superuser? + end + end h4. Testing @@ -128,35 +128,35 @@ One benefit of encapsulating our objects this way is that it becomes extremely s logic in isolation. -require "ostruct" + require "ostruct" -class PostSerializerTest < ActiveSupport::TestCase - # For now, we use a very simple authorization structure. These tests will need - # refactoring if we change that. - plebe = OpenStruct.new(super?: false) - god = OpenStruct.new(super?: true) + class PostSerializerTest < ActiveSupport::TestCase + # For now, we use a very simple authorization structure. These tests will need + # refactoring if we change that. + plebe = OpenStruct.new(super?: false) + 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 - json = PostSerializer.new(post, plebe).to_json - hash = JSON.parse(json) + test "a regular user sees just the title and body" do + json = PostSerializer.new(post, plebe).to_json + hash = JSON.parse(json) - assert_equal post.title, hash.delete("title") - assert_equal post.body, hash.delete("body") - assert_empty hash - end + assert_equal post.title, hash.delete("title") + assert_equal post.body, hash.delete("body") + assert_empty hash + end - test "a superuser sees the title, body and email" do - json = PostSerializer.new(post, god).to_json - hash = JSON.parse(json) + test "a superuser sees the title, body and email" do + json = PostSerializer.new(post, god).to_json + hash = JSON.parse(json) - assert_equal post.title, hash.delete("title") - assert_equal post.body, hash.delete("body") - assert_equal post.email, hash.delete("email") - assert_empty hash - end -end + assert_equal post.title, hash.delete("title") + assert_equal post.body, hash.delete("body") + assert_equal post.email, hash.delete("email") + assert_empty hash + end + end 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: -class PostSerializer - # @param [~body, ~title, ~email] post the post to serialize - # @param [~super] scope the authorization scope for this serializer - def initialize(post, scope) - @post, @scope = post, scope - end + class PostSerializer + # @param [~body, ~title, ~email] post the post to serialize + # @param [~super] scope the authorization scope for this serializer + def initialize(post, scope) + @post, @scope = post, scope + end - # ... -end + # ... + end 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. -class PostSerializer < ActiveModel::Serializer - attributes :title, :body + class PostSerializer < ActiveModel::Serializer + attributes :title, :body - def initialize(post, scope) - @post, @scope = post, scope - end + def initialize(post, scope) + @post, @scope = post, scope + end - def serializable_hash - hash = attributes - hash.merge!(super_data) if super? - hash - end + def serializable_hash + hash = attributes + hash.merge!(super_data) if super? + hash + end -private - def super_data - { email: @post.email } - end + private + def super_data + { email: @post.email } + end - def super? - @scope.superuser? - end -end + def super? + @scope.superuser? + end + end 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! -class PostSerializer < ActiveModel::Serializer - attributes :title, :body + class PostSerializer < ActiveModel::Serializer + attributes :title, :body -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end + private + def attributes + hash = super + hash.merge!(email: post.email) if super? + hash + end - def super? - @scope.superuser? - end -end + def super? + @scope.superuser? + end + end 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. -class PostSerializer < ActiveModel::Serializer - attributes :title, :body - has_many :comments + class PostSerializer < ActiveModel::Serializer + attributes :title, :body + has_many :comments -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end + private + def attributes + hash = super + hash.merge!(email: post.email) if super? + hash + end - def super? - @scope.superuser? - end -end + def super? + @scope.superuser? + end + end The default +serializable_hash+ method will include the comments as embedded objects inside the post. -{ - post: { - title: "Hello Blog!", - body: "This is my first post. Isn't it fabulous!", - comments: [ - { - title: "Awesome", - body: "Your first post is great" + { + post: { + title: "Hello Blog!", + body: "This is my first post. Isn't it fabulous!", + comments: [ + { + title: "Awesome", + body: "Your first post is great" + } + ] } - ] - } -} + } 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. -class CommentSerializer - def initialize(comment, scope) - @comment, @scope = comment, scope - end + class CommentSerializer + def initialize(comment, scope) + @comment, @scope = comment, scope + end - def serializable_hash - { title: @comment.title } - end + def serializable_hash + { title: @comment.title } + end - def as_json - { comment: serializable_hash } - end -end + def as_json + { comment: serializable_hash } + end + end If we define the above comment serializer, the outputted JSON will change to: -{ - post: { - title: "Hello Blog!", - body: "This is my first post. Isn't it fabulous!", - comments: [{ title: "Awesome" }] - } -} + { + post: { + title: "Hello Blog!", + body: "This is my first post. Isn't it fabulous!", + comments: [{ title: "Awesome" }] + } + } 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. -class PostSerializer < ActiveModel::Serializer - attributes :title. :body - has_many :comments + class PostSerializer < ActiveModel::Serializer + attributes :title. :body + has_many :comments -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end + private + def attributes + hash = super + hash.merge!(email: post.email) if super? + hash + end - def comments - post.comments_for(scope) - end + def comments + post.comments_for(scope) + end - def super? - @scope.superuser? - end -end + def super? + @scope.superuser? + end + end +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: -{ - post: { - id: 1 - title: "Hello Blog!", - body: "This is my first post. Isn't it fabulous!", - comments: [1,2] - }, - comments: [ { - id: 1 - title: "Awesome", - body: "Your first post is great" - }, - { - id: 2 - title: "Not so awesome", - body: "Why is it so short!" + post: { + id: 1 + title: "Hello Blog!", + body: "This is my first post. Isn't it fabulous!", + comments: [1,2] + }, + comments: [ + { + id: 1 + title: "Awesome", + body: "Your first post is great" + }, + { + id: 2 + title: "Not so awesome", + body: "Why is it so short!" + } + ] } - ] -} We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments. -class CommentSerializer < ActiveModel::Serializer - attributes :id, :title, :body + class CommentSerializer < ActiveModel::Serializer + attributes :id, :title, :body - # define any logic for dealing with authorization-based attributes here -end + # define any logic for dealing with authorization-based attributes here + end -class PostSerializer < ActiveModel::Serializer - attributes :title, :body - has_many :comments + class PostSerializer < ActiveModel::Serializer + attributes :title, :body + has_many :comments - def as_json - { post: serializable_hash }.merge!(associations) - end + def as_json + { post: serializable_hash }.merge!(associations) + end - def serializable_hash - post_hash = attributes - post_hash.merge!(association_ids) - post_hash - end + def serializable_hash + post_hash = attributes + post_hash.merge!(association_ids) + post_hash + end -private - def attributes - hash = super - hash.merge!(email: post.email) if super? - hash - end + private + def attributes + hash = super + hash.merge!(email: post.email) if super? + hash + end - def comments - post.comments_for(scope) - end + def comments + post.comments_for(scope) + end - def super? - @scope.superuser? - end -end + def super? + @scope.superuser? + end + end 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. -class PostSerializer < ActiveModel::Serializer - class CommentSerializer < ActiveModel::Serializer - attributes :id, :title - end + class PostSerializer < ActiveModel::Serializer + class CommentSerializer < ActiveModel::Serializer + attributes :id, :title + end - # same as before - # ... -end + # same as before + # ... + end 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. -class PostsController < ApplicationController - serialization_scope :current_app -end + class PostsController < ApplicationController + serialization_scope :current_app + end 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: -{ - posts: [ { - title: "FIRST POST!", - body: "It's my first pooooost" - }, - { title: "Second post!", - body: "Zomg I made it to my second post" + posts: [ + { + title: "FIRST POST!", + body: "It's my first pooooost" + }, + { title: "Second post!", + body: "Zomg I made it to my second post" + } + ] } - ] -} If you want to change the behavior of serialized Arrays, you need to create a custom Array serializer. -class ArraySerializer < ActiveModel::ArraySerializer - def serializable_array - serializers.map do |serializer| - serializer.serializable_hash - end - end + class ArraySerializer < ActiveModel::ArraySerializer + def serializable_array + serializers.map do |serializer| + serializer.serializable_hash + end + end - def as_json - hash = { root => serializable_array } - hash.merge!(associations) - hash - end -end + def as_json + hash = { root => serializable_array } + hash.merge!(associations) + hash + end + end When generating embedded associations using the +associations+ helper inside a