mirror of
https://github.com/github/rails.git
synced 2026-01-09 14:48:01 -05:00
Revert the serializers API as other alternatives are now also under discussion
This commit is contained in:
@@ -31,7 +31,6 @@ module ActionController
|
|||||||
autoload :RequestForgeryProtection
|
autoload :RequestForgeryProtection
|
||||||
autoload :Rescue
|
autoload :Rescue
|
||||||
autoload :Responder
|
autoload :Responder
|
||||||
autoload :Serialization
|
|
||||||
autoload :SessionManagement
|
autoload :SessionManagement
|
||||||
autoload :Streaming
|
autoload :Streaming
|
||||||
autoload :Testing
|
autoload :Testing
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
module ActionController
|
|
||||||
# Action Controller Serialization
|
|
||||||
#
|
|
||||||
# Overrides render :json to check if the given object implements +active_model_serializer+
|
|
||||||
# as a method. If so, use the returned serializer instead of calling +to_json+ in the object.
|
|
||||||
#
|
|
||||||
# This module also provides a serialization_scope method that allows you to configure the
|
|
||||||
# +serialization_scope+ of the serializer. Most apps will likely set the +serialization_scope+
|
|
||||||
# to the current user:
|
|
||||||
#
|
|
||||||
# class ApplicationController < ActionController::Base
|
|
||||||
# serialization_scope :current_user
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# If you need more complex scope rules, you can simply override the serialization_scope:
|
|
||||||
#
|
|
||||||
# class ApplicationController < ActionController::Base
|
|
||||||
# private
|
|
||||||
#
|
|
||||||
# def serialization_scope
|
|
||||||
# current_user
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
module Serialization
|
|
||||||
extend ActiveSupport::Concern
|
|
||||||
|
|
||||||
include ActionController::Renderers
|
|
||||||
|
|
||||||
included do
|
|
||||||
class_attribute :_serialization_scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def serialization_scope
|
|
||||||
send(_serialization_scope)
|
|
||||||
end
|
|
||||||
|
|
||||||
def _render_option_json(json, options)
|
|
||||||
if json.respond_to?(:active_model_serializer) && (serializer = json.active_model_serializer)
|
|
||||||
json = serializer.new(json, serialization_scope)
|
|
||||||
end
|
|
||||||
super
|
|
||||||
end
|
|
||||||
|
|
||||||
module ClassMethods
|
|
||||||
def serialization_scope(scope)
|
|
||||||
self._serialization_scope = scope
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -15,36 +15,9 @@ class RenderJsonTest < ActionController::TestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class JsonSerializer
|
|
||||||
def initialize(object, scope)
|
|
||||||
@object, @scope = object, scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json(*)
|
|
||||||
{ :object => @object.as_json, :scope => @scope.as_json }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class JsonSerializable
|
|
||||||
def initialize(skip=false)
|
|
||||||
@skip = skip
|
|
||||||
end
|
|
||||||
|
|
||||||
def active_model_serializer
|
|
||||||
JsonSerializer unless @skip
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json(*)
|
|
||||||
{ :serializable_object => true }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class TestController < ActionController::Base
|
class TestController < ActionController::Base
|
||||||
protect_from_forgery
|
protect_from_forgery
|
||||||
|
|
||||||
serialization_scope :current_user
|
|
||||||
attr_reader :current_user
|
|
||||||
|
|
||||||
def self.controller_path
|
def self.controller_path
|
||||||
'test'
|
'test'
|
||||||
end
|
end
|
||||||
@@ -88,16 +61,6 @@ class RenderJsonTest < ActionController::TestCase
|
|||||||
def render_json_without_options
|
def render_json_without_options
|
||||||
render :json => JsonRenderable.new
|
render :json => JsonRenderable.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_json_with_serializer
|
|
||||||
@current_user = Struct.new(:as_json).new(:current_user => true)
|
|
||||||
render :json => JsonSerializable.new
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_json_with_serializer_api_but_without_serializer
|
|
||||||
@current_user = Struct.new(:as_json).new(:current_user => true)
|
|
||||||
render :json => JsonSerializable.new(true)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
tests TestController
|
tests TestController
|
||||||
@@ -169,15 +132,4 @@ class RenderJsonTest < ActionController::TestCase
|
|||||||
get :render_json_without_options
|
get :render_json_without_options
|
||||||
assert_equal '{"a":"b"}', @response.body
|
assert_equal '{"a":"b"}', @response.body
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_render_json_with_serializer
|
|
||||||
get :render_json_with_serializer
|
|
||||||
assert_match '"scope":{"current_user":true}', @response.body
|
|
||||||
assert_match '"object":{"serializable_object":true}', @response.body
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_render_json_with_serializer_api_but_without_serializer
|
|
||||||
get :render_json_with_serializer_api_but_without_serializer
|
|
||||||
assert_match '{"serializable_object":true}', @response.body
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
## Rails 3.2.0 (unreleased) ##
|
## Rails 3.2.0 (unreleased) ##
|
||||||
|
|
||||||
* Add ActiveModel::Serializer that encapsulates an ActiveModel object serialization *José Valim*
|
|
||||||
|
|
||||||
* Renamed (with a deprecation the following constants):
|
* Renamed (with a deprecation the following constants):
|
||||||
|
|
||||||
ActiveModel::Serialization => ActiveModel::Serializable
|
ActiveModel::Serialization => ActiveModel::Serializable
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ require 'active_model/version'
|
|||||||
module ActiveModel
|
module ActiveModel
|
||||||
extend ActiveSupport::Autoload
|
extend ActiveSupport::Autoload
|
||||||
|
|
||||||
autoload :ArraySerializer, 'active_model/serializer'
|
|
||||||
autoload :AttributeMethods
|
autoload :AttributeMethods
|
||||||
autoload :BlockValidator, 'active_model/validator'
|
autoload :BlockValidator, 'active_model/validator'
|
||||||
autoload :Callbacks
|
autoload :Callbacks
|
||||||
@@ -46,7 +45,6 @@ module ActiveModel
|
|||||||
autoload :SecurePassword
|
autoload :SecurePassword
|
||||||
autoload :Serializable
|
autoload :Serializable
|
||||||
autoload :Serialization
|
autoload :Serialization
|
||||||
autoload :Serializer
|
|
||||||
autoload :TestCase
|
autoload :TestCase
|
||||||
autoload :Translation
|
autoload :Translation
|
||||||
autoload :Validations
|
autoload :Validations
|
||||||
|
|||||||
@@ -73,13 +73,6 @@ module ActiveModel
|
|||||||
autoload :JSON, "active_model/serializable/json"
|
autoload :JSON, "active_model/serializable/json"
|
||||||
autoload :XML, "active_model/serializable/xml"
|
autoload :XML, "active_model/serializable/xml"
|
||||||
|
|
||||||
module ClassMethods #:nodoc:
|
|
||||||
def active_model_serializer
|
|
||||||
return @active_model_serializer if defined?(@active_model_serializer)
|
|
||||||
@active_model_serializer = "#{self.name}Serializer".safe_constantize
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash(options = nil)
|
def serializable_hash(options = nil)
|
||||||
options ||= {}
|
options ||= {}
|
||||||
|
|
||||||
@@ -107,11 +100,6 @@ module ActiveModel
|
|||||||
hash
|
hash
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a model serializer for this object considering its namespace.
|
|
||||||
def active_model_serializer
|
|
||||||
self.class.active_model_serializer
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Hook method defining how an attribute value should be retrieved for
|
# Hook method defining how an attribute value should be retrieved for
|
||||||
|
|||||||
@@ -1,432 +0,0 @@
|
|||||||
require "cases/helper"
|
|
||||||
|
|
||||||
class SerializerTest < ActiveModel::TestCase
|
|
||||||
class Model
|
|
||||||
def initialize(hash={})
|
|
||||||
@attributes = hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_attribute_for_serialization(name)
|
|
||||||
@attributes[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json(*)
|
|
||||||
{ :model => "Model" }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class User
|
|
||||||
include ActiveModel::Serializable
|
|
||||||
|
|
||||||
attr_accessor :superuser
|
|
||||||
|
|
||||||
def initialize(hash={})
|
|
||||||
@attributes = hash.merge(:first_name => "Jose", :last_name => "Valim", :password => "oh noes yugive my password")
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_attribute_for_serialization(name)
|
|
||||||
@attributes[name]
|
|
||||||
end
|
|
||||||
|
|
||||||
def super_user?
|
|
||||||
@superuser
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Post < Model
|
|
||||||
attr_accessor :comments
|
|
||||||
def active_model_serializer; PostSerializer; end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Comment < Model
|
|
||||||
def active_model_serializer; CommentSerializer; end
|
|
||||||
end
|
|
||||||
|
|
||||||
class UserSerializer < ActiveModel::Serializer
|
|
||||||
attributes :first_name, :last_name
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
attributes.merge(:ok => true).merge(scope)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class DefaultUserSerializer < ActiveModel::Serializer
|
|
||||||
attributes :first_name, :last_name
|
|
||||||
end
|
|
||||||
|
|
||||||
class MyUserSerializer < ActiveModel::Serializer
|
|
||||||
attributes :first_name, :last_name
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
hash = attributes
|
|
||||||
hash = hash.merge(:super_user => true) if my_user.super_user?
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class CommentSerializer
|
|
||||||
def initialize(comment, scope)
|
|
||||||
@comment, @scope = comment, scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
{ :title => @comment.read_attribute_for_serialization(:title) }
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{ :comment => serializable_hash }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :title, :body
|
|
||||||
has_many :comments, :serializer => CommentSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_attributes
|
|
||||||
user = User.new
|
|
||||||
user_serializer = DefaultUserSerializer.new(user, {})
|
|
||||||
|
|
||||||
hash = user_serializer.as_json
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:default_user => { :first_name => "Jose", :last_name => "Valim" }
|
|
||||||
}, hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_attributes_method
|
|
||||||
user = User.new
|
|
||||||
user_serializer = UserSerializer.new(user, {})
|
|
||||||
|
|
||||||
hash = user_serializer.as_json
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:user => { :first_name => "Jose", :last_name => "Valim", :ok => true }
|
|
||||||
}, hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_serializer_receives_scope
|
|
||||||
user = User.new
|
|
||||||
user_serializer = UserSerializer.new(user, {:scope => true})
|
|
||||||
|
|
||||||
hash = user_serializer.as_json
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:user => {
|
|
||||||
:first_name => "Jose",
|
|
||||||
:last_name => "Valim",
|
|
||||||
:ok => true,
|
|
||||||
:scope => true
|
|
||||||
}
|
|
||||||
}, hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_pretty_accessors
|
|
||||||
user = User.new
|
|
||||||
user.superuser = true
|
|
||||||
user_serializer = MyUserSerializer.new(user, nil)
|
|
||||||
|
|
||||||
hash = user_serializer.as_json
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:my_user => {
|
|
||||||
:first_name => "Jose", :last_name => "Valim", :super_user => true
|
|
||||||
}
|
|
||||||
}, hash)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_has_many
|
|
||||||
user = User.new
|
|
||||||
|
|
||||||
post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
|
|
||||||
comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")]
|
|
||||||
post.comments = comments
|
|
||||||
|
|
||||||
post_serializer = PostSerializer.new(post, user)
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:post => {
|
|
||||||
:title => "New Post",
|
|
||||||
:body => "Body of new post",
|
|
||||||
:comments => [
|
|
||||||
{ :title => "Comment1" },
|
|
||||||
{ :title => "Comment2" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}, post_serializer.as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
class Blog < Model
|
|
||||||
attr_accessor :author
|
|
||||||
end
|
|
||||||
|
|
||||||
class AuthorSerializer < ActiveModel::Serializer
|
|
||||||
attributes :first_name, :last_name
|
|
||||||
end
|
|
||||||
|
|
||||||
class BlogSerializer < ActiveModel::Serializer
|
|
||||||
has_one :author, :serializer => AuthorSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_has_one
|
|
||||||
user = User.new
|
|
||||||
blog = Blog.new
|
|
||||||
blog.author = user
|
|
||||||
|
|
||||||
json = BlogSerializer.new(blog, user).as_json
|
|
||||||
assert_equal({
|
|
||||||
:blog => {
|
|
||||||
:author => {
|
|
||||||
:first_name => "Jose",
|
|
||||||
:last_name => "Valim"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_implicit_serializer
|
|
||||||
author_serializer = Class.new(ActiveModel::Serializer) do
|
|
||||||
attributes :first_name
|
|
||||||
end
|
|
||||||
|
|
||||||
blog_serializer = Class.new(ActiveModel::Serializer) do
|
|
||||||
const_set(:AuthorSerializer, author_serializer)
|
|
||||||
has_one :author
|
|
||||||
end
|
|
||||||
|
|
||||||
user = User.new
|
|
||||||
blog = Blog.new
|
|
||||||
blog.author = user
|
|
||||||
|
|
||||||
json = blog_serializer.new(blog, user).as_json
|
|
||||||
assert_equal({
|
|
||||||
:author => {
|
|
||||||
:first_name => "Jose"
|
|
||||||
}
|
|
||||||
}, json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_overridden_associations
|
|
||||||
author_serializer = Class.new(ActiveModel::Serializer) do
|
|
||||||
attributes :first_name
|
|
||||||
end
|
|
||||||
|
|
||||||
blog_serializer = Class.new(ActiveModel::Serializer) do
|
|
||||||
const_set(:PersonSerializer, author_serializer)
|
|
||||||
|
|
||||||
def person
|
|
||||||
object.author
|
|
||||||
end
|
|
||||||
|
|
||||||
has_one :person
|
|
||||||
end
|
|
||||||
|
|
||||||
user = User.new
|
|
||||||
blog = Blog.new
|
|
||||||
blog.author = user
|
|
||||||
|
|
||||||
json = blog_serializer.new(blog, user).as_json
|
|
||||||
assert_equal({
|
|
||||||
:person => {
|
|
||||||
:first_name => "Jose"
|
|
||||||
}
|
|
||||||
}, json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def post_serializer(type)
|
|
||||||
Class.new(ActiveModel::Serializer) do
|
|
||||||
attributes :title, :body
|
|
||||||
has_many :comments, :serializer => CommentSerializer
|
|
||||||
|
|
||||||
if type != :super
|
|
||||||
define_method :serializable_hash do
|
|
||||||
post_hash = attributes
|
|
||||||
post_hash.merge!(send(type))
|
|
||||||
post_hash
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_associations
|
|
||||||
post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
|
|
||||||
comments = [Comment.new(:title => "Comment1"), Comment.new(:title => "Comment2")]
|
|
||||||
post.comments = comments
|
|
||||||
|
|
||||||
serializer = post_serializer(:associations).new(post, nil)
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:title => "New Post",
|
|
||||||
:body => "Body of new post",
|
|
||||||
:comments => [
|
|
||||||
{ :title => "Comment1" },
|
|
||||||
{ :title => "Comment2" }
|
|
||||||
]
|
|
||||||
}, serializer.as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_association_ids
|
|
||||||
serializer = post_serializer(:association_ids)
|
|
||||||
|
|
||||||
serializer.class_eval do
|
|
||||||
def as_json(*)
|
|
||||||
{ :post => serializable_hash }.merge(associations)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
|
|
||||||
comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
|
|
||||||
post.comments = comments
|
|
||||||
|
|
||||||
serializer = serializer.new(post, nil)
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:post => {
|
|
||||||
:title => "New Post",
|
|
||||||
:body => "Body of new post",
|
|
||||||
:comments => [1, 2]
|
|
||||||
},
|
|
||||||
:comments => [
|
|
||||||
{ :title => "Comment1" },
|
|
||||||
{ :title => "Comment2" }
|
|
||||||
]
|
|
||||||
}, serializer.as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_associations_with_nil_association
|
|
||||||
user = User.new
|
|
||||||
blog = Blog.new
|
|
||||||
|
|
||||||
json = BlogSerializer.new(blog, user).as_json
|
|
||||||
assert_equal({
|
|
||||||
:blog => { :author => nil }
|
|
||||||
}, json)
|
|
||||||
|
|
||||||
serializer = Class.new(BlogSerializer) do
|
|
||||||
root :blog
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
attributes.merge(association_ids)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
json = serializer.new(blog, user).as_json
|
|
||||||
assert_equal({ :blog => { :author => nil } }, json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_custom_root
|
|
||||||
user = User.new
|
|
||||||
blog = Blog.new
|
|
||||||
|
|
||||||
serializer = Class.new(BlogSerializer) do
|
|
||||||
root :my_blog
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal({ :my_blog => { :author => nil } }, serializer.new(blog, user).as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_false_root
|
|
||||||
user = User.new
|
|
||||||
blog = Blog.new
|
|
||||||
|
|
||||||
serializer = Class.new(BlogSerializer) do
|
|
||||||
root false
|
|
||||||
end
|
|
||||||
|
|
||||||
assert_equal({ :author => nil }, serializer.new(blog, user).as_json)
|
|
||||||
|
|
||||||
# test inherited false root
|
|
||||||
serializer = Class.new(serializer)
|
|
||||||
assert_equal({ :author => nil }, serializer.new(blog, user).as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_embed_ids
|
|
||||||
serializer = post_serializer(:super)
|
|
||||||
|
|
||||||
serializer.class_eval do
|
|
||||||
root :post
|
|
||||||
embed :ids
|
|
||||||
end
|
|
||||||
|
|
||||||
post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
|
|
||||||
comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
|
|
||||||
post.comments = comments
|
|
||||||
|
|
||||||
serializer = serializer.new(post, nil)
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:post => {
|
|
||||||
:title => "New Post",
|
|
||||||
:body => "Body of new post",
|
|
||||||
:comments => [1, 2]
|
|
||||||
}
|
|
||||||
}, serializer.as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_embed_ids_include_true
|
|
||||||
serializer = post_serializer(:super)
|
|
||||||
|
|
||||||
serializer.class_eval do
|
|
||||||
root :post
|
|
||||||
embed :ids, :include => true
|
|
||||||
end
|
|
||||||
|
|
||||||
post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
|
|
||||||
comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
|
|
||||||
post.comments = comments
|
|
||||||
|
|
||||||
serializer = serializer.new(post, nil)
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:post => {
|
|
||||||
:title => "New Post",
|
|
||||||
:body => "Body of new post",
|
|
||||||
:comments => [1, 2]
|
|
||||||
},
|
|
||||||
:comments => [
|
|
||||||
{ :title => "Comment1" },
|
|
||||||
{ :title => "Comment2" }
|
|
||||||
]
|
|
||||||
}, serializer.as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_embed_objects
|
|
||||||
serializer = post_serializer(:super)
|
|
||||||
|
|
||||||
serializer.class_eval do
|
|
||||||
root :post
|
|
||||||
embed :objects
|
|
||||||
end
|
|
||||||
|
|
||||||
post = Post.new(:title => "New Post", :body => "Body of new post", :email => "tenderlove@tenderlove.com")
|
|
||||||
comments = [Comment.new(:title => "Comment1", :id => 1), Comment.new(:title => "Comment2", :id => 2)]
|
|
||||||
post.comments = comments
|
|
||||||
|
|
||||||
serializer = serializer.new(post, nil)
|
|
||||||
|
|
||||||
assert_equal({
|
|
||||||
:post => {
|
|
||||||
:title => "New Post",
|
|
||||||
:body => "Body of new post",
|
|
||||||
:comments => [
|
|
||||||
{ :title => "Comment1" },
|
|
||||||
{ :title => "Comment2" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}, serializer.as_json)
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_array_serializer
|
|
||||||
model = Model.new
|
|
||||||
user = User.new
|
|
||||||
comments = Comment.new(:title => "Comment1", :id => 1)
|
|
||||||
|
|
||||||
array = [model, user, comments]
|
|
||||||
serializer = array.active_model_serializer.new(array, {:scope => true})
|
|
||||||
assert_equal([
|
|
||||||
{ :model => "Model" },
|
|
||||||
{ :user => { :last_name=>"Valim", :ok=>true, :first_name=>"Jose", :scope => true } },
|
|
||||||
{ :comment => { :title => "Comment1" } }
|
|
||||||
], serializer.as_json)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,8 +7,6 @@
|
|||||||
Example:
|
Example:
|
||||||
config.railties_order = [Blog::Engine, :main_app, :all]
|
config.railties_order = [Blog::Engine, :main_app, :all]
|
||||||
|
|
||||||
* Add a serializer generator and add a hook for it in the scaffold generators *José Valim*
|
|
||||||
|
|
||||||
* Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box. *José Valim*
|
* Scaffold returns 204 No Content for API requests without content. This makes scaffold work with jQuery out of the box. *José Valim*
|
||||||
|
|
||||||
* Update Rails::Rack::Logger middleware to apply any tags set in config.log_tags to the newly ActiveSupport::TaggedLogging Rails.logger. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications *DHH*
|
* Update Rails::Rack::Logger middleware to apply any tags set in config.log_tags to the newly ActiveSupport::TaggedLogging Rails.logger. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications *DHH*
|
||||||
|
|||||||
@@ -1,563 +0,0 @@
|
|||||||
h2. Rails Serializers
|
|
||||||
|
|
||||||
This guide describes how to use Active Model serializers to build non-trivial JSON services in Rails. By reading this guide, you will learn:
|
|
||||||
|
|
||||||
* When to use the built-in Active Model serialization
|
|
||||||
* When to use a custom serializer for your models
|
|
||||||
* How to use serializers to encapsulate authorization concerns
|
|
||||||
* How to create serializer templates to describe the application-wide structure of your serialized JSON
|
|
||||||
* How to build resources not backed by a single database table for use with JSON services
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
endprologue.
|
|
||||||
|
|
||||||
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
|
|
||||||
parameter to control which properties and associations Rails should include in the serialized output.
|
|
||||||
|
|
||||||
When building a web application that uses JavaScript to retrieve JSON data from the server, this mechanism has historically been the primary
|
|
||||||
way that Rails developers prepared their responses. This works great for simple cases, as the logic for serializing an Active Record object
|
|
||||||
is neatly encapsulated in Active Record itself.
|
|
||||||
|
|
||||||
However, this solution quickly falls apart in the face of serialization requirements based on authorization. For instance, a web service
|
|
||||||
may choose to expose additional information about a resource only if the user is entitled to access it. In addition, a JavaScript front-end
|
|
||||||
may want information that is not neatly described in terms of serializing a single Active Record object, or in a different format than.
|
|
||||||
|
|
||||||
In addition, neither the controller nor the model seems like the correct place for logic that describes how to serialize an model object
|
|
||||||
*for the current user*.
|
|
||||||
|
|
||||||
Serializers solve these problems by encapsulating serialization in an object designed for this purpose. If the default +to_json+ semantics,
|
|
||||||
with at most a few configuration options serve your needs, by all means continue to use the built-in +to_json+. If you find yourself doing
|
|
||||||
hash-driven-development in your controllers, juggling authorization logic and other concerns, serializers are for you!
|
|
||||||
|
|
||||||
h3. The Most Basic Serializer
|
|
||||||
|
|
||||||
A basic serializer is a simple Ruby object named after the model class it is serializing.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer
|
|
||||||
def initialize(post, scope)
|
|
||||||
@post, @scope = post, scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{ post: { title: @post.name, body: @post.body } }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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
|
|
||||||
implements an +as_json+ method, which returns a Hash that will be sent to the JSON encoder.
|
|
||||||
|
|
||||||
Rails will transparently use your serializer when you use +render :json+ in your controller.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostsController < ApplicationController
|
|
||||||
def show
|
|
||||||
@post = Post.find(params[:id])
|
|
||||||
render json: @post
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
h4. +serializable_hash+
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer
|
|
||||||
def initialize(post, scope)
|
|
||||||
@post, @scope = post, scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
{ title: @post.name, body: @post.body }
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{ post: serializable_hash }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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
|
|
||||||
access.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer
|
|
||||||
def initialize(post, scope)
|
|
||||||
@post, @scope = post, scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{ post: serializable_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
|
|
||||||
|
|
||||||
def super_data
|
|
||||||
{ email: @post.email }
|
|
||||||
end
|
|
||||||
|
|
||||||
def super?
|
|
||||||
@scope.superuser?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
h4. Testing
|
|
||||||
|
|
||||||
One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization
|
|
||||||
logic in isolation.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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
|
|
||||||
scope with a +super?+ method.
|
|
||||||
|
|
||||||
By defining a clear interface, it's must easier to ensure that your authorization logic is behaving correctly. In this case,
|
|
||||||
the serializer doesn't need to concern itself with how the authorization scope decides whether to set the +super?+ flag, just
|
|
||||||
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:
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
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
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
h3. Attribute Sugar
|
|
||||||
|
|
||||||
To simplify this process for a number of common cases, Rails provides a default superclass named +ActiveModel::Serializer+
|
|
||||||
that you can use to implement your serializers.
|
|
||||||
|
|
||||||
For example, you will sometimes want to simply include a number of existing attributes from the source model into the outputted
|
|
||||||
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.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :title, :body
|
|
||||||
|
|
||||||
def initialize(post, scope)
|
|
||||||
@post, @scope = post, scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
hash = attributes
|
|
||||||
hash.merge!(super_data) if super?
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
def super_data
|
|
||||||
{ email: @post.email }
|
|
||||||
end
|
|
||||||
|
|
||||||
def super?
|
|
||||||
@scope.superuser?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
NOTE: Internally, +ActiveModel::Serializer+ uses +read_attribute_for_serialization+, which defaults to +read_attribute+, which defaults to +send+. So if you're rolling your own models for use with the serializer, you can use simple Ruby accessors for your attributes if you like.
|
|
||||||
|
|
||||||
Next, we use the attributes methood in our +serializable_hash+ method, which allowed us to eliminate the +post+ method we hand-rolled
|
|
||||||
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!
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :title, :body
|
|
||||||
|
|
||||||
private
|
|
||||||
def attributes
|
|
||||||
hash = super
|
|
||||||
hash.merge!(email: post.email) if super?
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def super?
|
|
||||||
@scope.superuser?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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 want to use.
|
|
||||||
|
|
||||||
NOTE: +ActiveModel::Serializer+ will create an accessor matching the name of the current class for the resource you pass in. In this case, because we have defined a PostSerializer, we can access the resource with the +post+ accessor.
|
|
||||||
|
|
||||||
h3. Associations
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :title, :body
|
|
||||||
has_many :comments
|
|
||||||
|
|
||||||
private
|
|
||||||
def attributes
|
|
||||||
hash = super
|
|
||||||
hash.merge!(email: post.email) if super?
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def super?
|
|
||||||
@scope.superuser?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
The default +serializable_hash+ method will include the comments as embedded objects inside the post.
|
|
||||||
|
|
||||||
<javascript>
|
|
||||||
{
|
|
||||||
post: {
|
|
||||||
title: "Hello Blog!",
|
|
||||||
body: "This is my first post. Isn't it fabulous!",
|
|
||||||
comments: [
|
|
||||||
{
|
|
||||||
title: "Awesome",
|
|
||||||
body: "Your first post is great"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</javascript>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
If you define a serializer, Rails will automatically instantiate it with the existing authorization scope.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class CommentSerializer
|
|
||||||
def initialize(comment, scope)
|
|
||||||
@comment, @scope = comment, scope
|
|
||||||
end
|
|
||||||
|
|
||||||
def serializable_hash
|
|
||||||
{ title: @comment.title }
|
|
||||||
end
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{ comment: serializable_hash }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
If we define the above comment serializer, the outputted JSON will change to:
|
|
||||||
|
|
||||||
<javascript>
|
|
||||||
{
|
|
||||||
post: {
|
|
||||||
title: "Hello Blog!",
|
|
||||||
body: "This is my first post. Isn't it fabulous!",
|
|
||||||
comments: [{ title: "Awesome" }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</javascript>
|
|
||||||
|
|
||||||
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
|
|
||||||
+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.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :title. :body
|
|
||||||
has_many :comments
|
|
||||||
|
|
||||||
private
|
|
||||||
def attributes
|
|
||||||
hash = super
|
|
||||||
hash.merge!(email: post.email) if super?
|
|
||||||
hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def comments
|
|
||||||
post.comments_for(scope)
|
|
||||||
end
|
|
||||||
|
|
||||||
def super?
|
|
||||||
@scope.superuser?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
+ActiveModel::Serializer+ will still embed the comments, but this time it will use just the comments
|
|
||||||
for the current user.
|
|
||||||
|
|
||||||
NOTE: The logic for deciding which comments a user should see still belongs in the model layer. In general, you should encapsulate concerns that require making direct Active Record queries in scopes or public methods on your models.
|
|
||||||
|
|
||||||
h3. Customizing Associations
|
|
||||||
|
|
||||||
Not all front-ends expect embedded documents in the same form. In these cases, you can override the
|
|
||||||
default +serializable_hash+, and use conveniences provided by +ActiveModel::Serializer+ to avoid having to
|
|
||||||
build up the hash manually.
|
|
||||||
|
|
||||||
For example, let's say our front-end expects the posts and comments in the following format:
|
|
||||||
|
|
||||||
<plain>
|
|
||||||
{
|
|
||||||
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!"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</plain>
|
|
||||||
|
|
||||||
We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class CommentSerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :title, :body
|
|
||||||
|
|
||||||
# define any logic for dealing with authorization-based attributes here
|
|
||||||
end
|
|
||||||
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
attributes :title, :body
|
|
||||||
has_many :comments
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{ post: serializable_hash }.merge!(associations)
|
|
||||||
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
|
|
||||||
|
|
||||||
def comments
|
|
||||||
post.comments_for(scope)
|
|
||||||
end
|
|
||||||
|
|
||||||
def super?
|
|
||||||
@scope.superuser?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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
|
|
||||||
serializers. The second, +association_ids+, generates a hash whose key is the association
|
|
||||||
name and whose value is an Array of the association's keys.
|
|
||||||
|
|
||||||
The +association_ids+ helper will use the overridden version of the association, so in
|
|
||||||
this case, +association_ids+ will only include the ids of the comments provided by the
|
|
||||||
+comments+ method.
|
|
||||||
|
|
||||||
h3. Special Association Serializers
|
|
||||||
|
|
||||||
So far, associations defined in serializers use either the +as_json+ method on the model
|
|
||||||
or the defined serializer for the association type. Sometimes, you may want to serialize
|
|
||||||
associated models differently when they are requested as part of another resource than
|
|
||||||
when they are requested on their own.
|
|
||||||
|
|
||||||
For instance, we might want to provide the full comment when it is requested directly,
|
|
||||||
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.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostSerializer < ActiveModel::Serializer
|
|
||||||
class CommentSerializer < ActiveModel::Serializer
|
|
||||||
attributes :id, :title
|
|
||||||
end
|
|
||||||
|
|
||||||
# same as before
|
|
||||||
# ...
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
In other words, if a +PostSerializer+ is trying to serialize comments, it will first
|
|
||||||
look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+
|
|
||||||
and finally +comment.as_json+.
|
|
||||||
|
|
||||||
h3. Overriding the Defaults
|
|
||||||
|
|
||||||
h4. Authorization Scope
|
|
||||||
|
|
||||||
By default, the authorization scope for serializers is +:current_user+. This means
|
|
||||||
that when you call +render json: @post+, the controller will automatically call
|
|
||||||
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
|
|
||||||
method.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
class PostsController < ApplicationController
|
|
||||||
serialization_scope :current_app
|
|
||||||
end
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
WARNING: If you use different objects as authorization scopes, make sure that they all implement whatever interface you use in your serializers to control what the outputted JSON looks like.
|
|
||||||
|
|
||||||
h3. Using Serializers Outside of a Request
|
|
||||||
|
|
||||||
The serialization API encapsulates the concern of generating a JSON representation of
|
|
||||||
a particular model for a particular user. As a result, you should be able to easily use
|
|
||||||
serializers, whether you define them yourself or whether you use +ActiveModel::Serializer+
|
|
||||||
outside a request.
|
|
||||||
|
|
||||||
For instance, if you want to generate the JSON representation of a post for a user outside
|
|
||||||
of a request:
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
user = get_user # some logic to get the user in question
|
|
||||||
PostSerializer.new(post, user).to_json # reliably generate JSON output
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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.
|
|
||||||
Typically, that means creating a new user and not saving it to the database:
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
user = User.new # create a new anonymous user
|
|
||||||
PostSerializer.new(post, user).to_json
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
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,
|
|
||||||
if you use an authorization library like Cancan, which uses a uniform +user.can?(action, model)+,
|
|
||||||
the authorization interface can very easily be replaced by a plain Ruby object for
|
|
||||||
testing or usage outside the context of a request.
|
|
||||||
|
|
||||||
h3. Collections
|
|
||||||
|
|
||||||
So far, we've talked about serializing individual model objects. By default, Rails
|
|
||||||
will serialize collections, including when using the +associations+ helper, by
|
|
||||||
looping over each element of the collection, calling +serializable_hash+ on the element,
|
|
||||||
and then grouping them by their type (using the plural version of their class name
|
|
||||||
as the root).
|
|
||||||
|
|
||||||
For example, an Array of post objects would serialize as:
|
|
||||||
|
|
||||||
<plain>
|
|
||||||
{
|
|
||||||
posts: [
|
|
||||||
{
|
|
||||||
title: "FIRST POST!",
|
|
||||||
body: "It's my first pooooost"
|
|
||||||
},
|
|
||||||
{ title: "Second post!",
|
|
||||||
body: "Zomg I made it to my second post"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
</plain>
|
|
||||||
|
|
||||||
If you want to change the behavior of serialized Arrays, you need to create
|
|
||||||
a custom Array serializer.
|
|
||||||
|
|
||||||
<ruby>
|
|
||||||
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
|
|
||||||
</ruby>
|
|
||||||
|
|
||||||
When generating embedded associations using the +associations+ helper inside a
|
|
||||||
regular serializer, it will create a new <code>ArraySerializer</code> with the
|
|
||||||
associated content and call its +serializable_array+ method. In this case, those
|
|
||||||
embedded associations will not recursively include associations.
|
|
||||||
|
|
||||||
When generating an Array using +render json: posts+, the controller will invoke
|
|
||||||
the +as_json+ method, which will include its associations and its root.
|
|
||||||
@@ -33,8 +33,7 @@ module Rails
|
|||||||
:stylesheets => '-y',
|
:stylesheets => '-y',
|
||||||
:stylesheet_engine => '-se',
|
:stylesheet_engine => '-se',
|
||||||
:template_engine => '-e',
|
:template_engine => '-e',
|
||||||
:test_framework => '-t',
|
:test_framework => '-t'
|
||||||
:serializer => '-z'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
:test_unit => {
|
:test_unit => {
|
||||||
@@ -59,7 +58,6 @@ module Rails
|
|||||||
:performance_tool => nil,
|
:performance_tool => nil,
|
||||||
:resource_controller => :controller,
|
:resource_controller => :controller,
|
||||||
:scaffold_controller => :scaffold_controller,
|
:scaffold_controller => :scaffold_controller,
|
||||||
:serializer => false,
|
|
||||||
:stylesheets => true,
|
:stylesheets => true,
|
||||||
:stylesheet_engine => :css,
|
:stylesheet_engine => :css,
|
||||||
:test_framework => false,
|
:test_framework => false,
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ module Rails
|
|||||||
class_option :stylesheet_engine, :desc => "Engine for Stylesheets"
|
class_option :stylesheet_engine, :desc => "Engine for Stylesheets"
|
||||||
|
|
||||||
hook_for :scaffold_controller, :required => true
|
hook_for :scaffold_controller, :required => true
|
||||||
hook_for :serializer
|
|
||||||
|
|
||||||
hook_for :assets do |assets|
|
hook_for :assets do |assets|
|
||||||
invoke assets, [controller_name]
|
invoke assets, [controller_name]
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
Description:
|
|
||||||
Generates a serializer for the given resource with tests.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
`rails generate serializer Account name created_at`
|
|
||||||
|
|
||||||
For TestUnit it creates:
|
|
||||||
Serializer: app/serializers/account_serializer.rb
|
|
||||||
TestUnit: test/unit/account_serializer_test.rb
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
module Rails
|
|
||||||
module Generators
|
|
||||||
class SerializerGenerator < NamedBase
|
|
||||||
check_class_collision :suffix => "Serializer"
|
|
||||||
|
|
||||||
argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
|
|
||||||
|
|
||||||
class_option :parent, :type => :string, :desc => "The parent class for the generated serializer"
|
|
||||||
|
|
||||||
def create_serializer_file
|
|
||||||
template 'serializer.rb', File.join('app/serializers', class_path, "#{file_name}_serializer.rb")
|
|
||||||
end
|
|
||||||
|
|
||||||
hook_for :test_framework
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def attributes_names
|
|
||||||
attributes.select { |attr| !attr.reference? }.map { |a| a.name.to_sym }
|
|
||||||
end
|
|
||||||
|
|
||||||
def association_names
|
|
||||||
attributes.select { |attr| attr.reference? }.map { |a| a.name.to_sym }
|
|
||||||
end
|
|
||||||
|
|
||||||
def parent_class_name
|
|
||||||
if options[:parent]
|
|
||||||
options[:parent]
|
|
||||||
elsif (n = Rails::Generators.namespace) && n.const_defined?(:ApplicationSerializer)
|
|
||||||
"ApplicationSerializer"
|
|
||||||
elsif Object.const_defined?(:ApplicationSerializer)
|
|
||||||
"ApplicationSerializer"
|
|
||||||
else
|
|
||||||
"ActiveModel::Serializer"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<% module_namespacing do -%>
|
|
||||||
class <%= class_name %>Serializer < <%= parent_class_name %>
|
|
||||||
<% if attributes.any? -%> attributes <%= attributes_names.map(&:inspect).join(", ") %>
|
|
||||||
<% end -%>
|
|
||||||
<% association_names.each do |attribute| -%>
|
|
||||||
has_one :<%= attribute %>
|
|
||||||
<% end -%>
|
|
||||||
end
|
|
||||||
<% end -%>
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
require 'rails/generators/test_unit'
|
|
||||||
|
|
||||||
module TestUnit
|
|
||||||
module Generators
|
|
||||||
class SerializerGenerator < Base
|
|
||||||
check_class_collision :suffix => "SerializerTest"
|
|
||||||
|
|
||||||
def create_test_files
|
|
||||||
template 'unit_test.rb', File.join('test/unit', class_path, "#{file_name}_serializer_test.rb")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
require 'test_helper'
|
|
||||||
|
|
||||||
<% module_namespacing do -%>
|
|
||||||
class <%= class_name %>SerializerTest < ActiveSupport::TestCase
|
|
||||||
# test "the truth" do
|
|
||||||
# assert true
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
<% end -%>
|
|
||||||
@@ -264,15 +264,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
|
|||||||
assert_file "app/assets/stylesheets/posts.css"
|
assert_file "app/assets/stylesheets/posts.css"
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_scaffold_also_generators_serializer
|
|
||||||
run_generator [ "posts", "name:string", "author:references", "--serializer" ]
|
|
||||||
assert_file "app/serializers/post_serializer.rb" do |serializer|
|
|
||||||
assert_match /class PostSerializer < ActiveModel::Serializer/, serializer
|
|
||||||
assert_match /^ attributes :name$/, serializer
|
|
||||||
assert_match /^ has_one :author$/, serializer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_scaffold_generator_outputs_error_message_on_missing_attribute_type
|
def test_scaffold_generator_outputs_error_message_on_missing_attribute_type
|
||||||
run_generator ["post", "title", "body:text", "author"]
|
run_generator ["post", "title", "body:text", "author"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
require 'generators/generators_test_helper'
|
|
||||||
require 'rails/generators/rails/serializer/serializer_generator'
|
|
||||||
|
|
||||||
class SerializerGeneratorTest < Rails::Generators::TestCase
|
|
||||||
include GeneratorsTestHelper
|
|
||||||
arguments %w(account name:string description:text business:references)
|
|
||||||
|
|
||||||
def test_generates_a_serializer
|
|
||||||
run_generator
|
|
||||||
assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < ActiveModel::Serializer/
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_generates_a_namespaced_serializer
|
|
||||||
run_generator ["admin/account"]
|
|
||||||
assert_file "app/serializers/admin/account_serializer.rb", /class Admin::AccountSerializer < ActiveModel::Serializer/
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_uses_application_serializer_if_one_exists
|
|
||||||
Object.const_set(:ApplicationSerializer, Class.new)
|
|
||||||
run_generator
|
|
||||||
assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < ApplicationSerializer/
|
|
||||||
ensure
|
|
||||||
Object.send :remove_const, :ApplicationSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_uses_namespace_application_serializer_if_one_exists
|
|
||||||
Object.const_set(:SerializerNamespace, Module.new)
|
|
||||||
SerializerNamespace.const_set(:ApplicationSerializer, Class.new)
|
|
||||||
Rails::Generators.namespace = SerializerNamespace
|
|
||||||
run_generator
|
|
||||||
assert_file "app/serializers/serializer_namespace/account_serializer.rb",
|
|
||||||
/module SerializerNamespace\n class AccountSerializer < ApplicationSerializer/
|
|
||||||
ensure
|
|
||||||
Object.send :remove_const, :SerializerNamespace
|
|
||||||
Rails::Generators.namespace = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_uses_given_parent
|
|
||||||
Object.const_set(:ApplicationSerializer, Class.new)
|
|
||||||
run_generator ["Account", "--parent=MySerializer"]
|
|
||||||
assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < MySerializer/
|
|
||||||
ensure
|
|
||||||
Object.send :remove_const, :ApplicationSerializer
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_generates_attributes_and_associations
|
|
||||||
run_generator
|
|
||||||
assert_file "app/serializers/account_serializer.rb" do |serializer|
|
|
||||||
assert_match(/^ attributes :name, :description$/, serializer)
|
|
||||||
assert_match(/^ has_one :business$/, serializer)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_with_no_attributes_does_not_add_extra_space
|
|
||||||
run_generator ["account"]
|
|
||||||
assert_file "app/serializers/account_serializer.rb", /class AccountSerializer < ActiveModel::Serializer\nend/
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_invokes_default_test_framework
|
|
||||||
run_generator
|
|
||||||
assert_file "test/unit/account_serializer_test.rb", /class AccountSerializerTest < ActiveSupport::TestCase/
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user