提交 fcacc698 编写于 作者: J José Valim

Merge branch 'serializers'

This implements the ActiveModel::Serializer object. Includes code, tests, generators and guides.

From José and Yehuda with love.

Conflicts:
	railties/CHANGELOG.md
......@@ -31,6 +31,7 @@ module ActionController
autoload :RequestForgeryProtection
autoload :Rescue
autoload :Responder
autoload :Serialization
autoload :SessionManagement
autoload :Streaming
autoload :Testing
......
......@@ -190,6 +190,7 @@ def self.without_modules(*modules)
Redirecting,
Rendering,
Renderers::All,
Serialization,
ConditionalGet,
RackDelegation,
SessionManagement,
......
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/blank'
require 'set'
module ActionController
# See <tt>Renderers.add</tt>
......@@ -12,16 +13,13 @@ module Renderers
included do
class_attribute :_renderers
self._renderers = {}.freeze
self._renderers = Set.new.freeze
end
module ClassMethods
def use_renderers(*args)
new = _renderers.dup
args.each do |key|
new[key] = RENDERERS[key]
end
self._renderers = new.freeze
renderers = _renderers + args
self._renderers = renderers.freeze
end
alias use_renderer use_renderers
end
......@@ -31,10 +29,10 @@ def render_to_body(options)
end
def _handle_render_options(options)
_renderers.each do |name, value|
if options.key?(name.to_sym)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
return send("_render_option_#{name}", options.delete(name.to_sym), options)
return send("_render_option_#{name}", options.delete(name), options)
end
end
nil
......@@ -42,7 +40,7 @@ def _handle_render_options(options)
# Hash of available renderers, mapping a renderer name to its proc.
# Default keys are :json, :js, :xml.
RENDERERS = {}
RENDERERS = Set.new
# Adds a new renderer to call within controller actions.
# A renderer is invoked by passing its name as an option to
......@@ -79,7 +77,7 @@ def _handle_render_options(options)
# <tt>ActionController::MimeResponds#respond_with</tt>
def self.add(key, &block)
define_method("_render_option_#{key}", &block)
RENDERERS[key] = block
RENDERERS << key.to_sym
end
module All
......
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,9 +15,36 @@ def to_json(options = {})
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
protect_from_forgery
serialization_scope :current_user
attr_reader :current_user
def self.controller_path
'test'
end
......@@ -61,6 +88,16 @@ def render_json_with_extra_options
def render_json_without_options
render :json => JsonRenderable.new
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
tests TestController
......@@ -132,4 +169,15 @@ def test_render_json_calls_to_json_from_object
get :render_json_without_options
assert_equal '{"a":"b"}', @response.body
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
* Added ActiveModel::Errors#added? to check if a specific error has been added *Martin Svalin*
## Rails 3.2.0 (unreleased) ##
* Add ActiveModel::Serializer that encapsulates an ActiveModel object serialization *José Valim*
* Renamed (with a deprecation the following constants):
ActiveModel::Serialization => ActiveModel::Serializable
ActiveModel::Serializers::JSON => ActiveModel::Serializable::JSON
ActiveModel::Serializers::Xml => ActiveModel::Serializable::XML
*José Valim*
* Add ActiveModel::Errors#added? to check if a specific error has been added *Martin Svalin*
* Add ability to define strict validation(with :strict => true option) that always raises exception when fails *Bogdan Gusiev*
......
......@@ -29,6 +29,7 @@
module ActiveModel
extend ActiveSupport::Autoload
autoload :ArraySerializer, 'active_model/serializer'
autoload :AttributeMethods
autoload :BlockValidator, 'active_model/validator'
autoload :Callbacks
......@@ -43,7 +44,9 @@ module ActiveModel
autoload :Observer, 'active_model/observing'
autoload :Observing
autoload :SecurePassword
autoload :Serializable
autoload :Serialization
autoload :Serializer
autoload :TestCase
autoload :Translation
autoload :Validations
......
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/string/inflections'
module ActiveModel
# == Active Model Serializable
#
# Provides a basic serialization to a serializable_hash for your object.
#
# A minimal implementation could be:
#
# class Person
#
# include ActiveModel::Serializable
#
# attr_accessor :name
#
# def attributes
# {'name' => name}
# end
#
# end
#
# Which would provide you with:
#
# person = Person.new
# person.serializable_hash # => {"name"=>nil}
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
#
# You need to declare some sort of attributes hash which contains the attributes
# you want to serialize and their current value.
#
# Most of the time though, you will want to include the JSON or XML
# serializations. Both of these modules automatically include the
# ActiveModel::Serialization module, so there is no need to explicitly
# include it.
#
# So a minimal implementation including XML and JSON would be:
#
# class Person
#
# include ActiveModel::Serializable::JSON
# include ActiveModel::Serializable::XML
#
# attr_accessor :name
#
# def attributes
# {'name' => name}
# end
#
# end
#
# Which would provide you with:
#
# person = Person.new
# person.serializable_hash # => {"name"=>nil}
# person.as_json # => {"name"=>nil}
# person.to_json # => "{\"name\":null}"
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
# person.as_json # => {"name"=>"Bob"}
# person.to_json # => "{\"name\":\"Bob\"}"
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> .
module Serializable
extend ActiveSupport::Concern
autoload :JSON, "active_model/serializable/json"
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)
options ||= {}
attribute_names = attributes.keys.sort
if only = options[:only]
attribute_names &= Array.wrap(only).map(&:to_s)
elsif except = options[:except]
attribute_names -= Array.wrap(except).map(&:to_s)
end
hash = {}
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
method_names.each { |n| hash[n] = send(n) }
serializable_add_includes(options) do |association, records, opts|
hash[association] = if records.is_a?(Enumerable)
records.map { |a| a.serializable_hash(opts) }
else
records.serializable_hash(opts)
end
end
hash
end
# Returns a model serializer for this object considering its namespace.
def active_model_serializer
self.class.active_model_serializer
end
private
# Hook method defining how an attribute value should be retrieved for
# serialization. By default this is assumed to be an instance named after
# the attribute. Override this method in subclasses should you need to
# retrieve the value for a given attribute differently:
#
# class MyClass
# include ActiveModel::Validations
#
# def initialize(data = {})
# @data = data
# end
#
# def read_attribute_for_serialization(key)
# @data[key]
# end
# end
#
alias :read_attribute_for_serialization :send
# Add associations specified via the <tt>:include</tt> option.
#
# Expects a block that takes as arguments:
# +association+ - name of the association
# +records+ - the association record(s) to be serialized
# +opts+ - options for the association records
def serializable_add_includes(options = {}) #:nodoc:
return unless include = options[:include]
unless include.is_a?(Hash)
include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
end
include.each do |association, opts|
if records = send(association)
yield association, records, opts
end
end
end
end
end
require 'active_support/json'
require 'active_support/core_ext/class/attribute'
module ActiveModel
# == Active Model Serializable as JSON
module Serializable
module JSON
extend ActiveSupport::Concern
include ActiveModel::Serializable
included do
extend ActiveModel::Naming
class_attribute :include_root_in_json
self.include_root_in_json = true
end
# Returns a hash representing the model. Some configuration can be
# passed through +options+.
#
# The option <tt>include_root_in_json</tt> controls the top-level behavior
# of +as_json+. If true (the default) +as_json+ will emit a single root
# node named after the object's type. For example:
#
# user = User.find(1)
# user.as_json
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true} }
#
# ActiveRecord::Base.include_root_in_json = false
# user.as_json
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in:
#
# user = User.find(1)
# user.as_json(root: false)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# The remainder of the examples in this section assume include_root_in_json is set to
# <tt>false</tt>.
#
# Without any +options+, the returned Hash will include all the model's
# attributes. For example:
#
# user = User.find(1)
# user.as_json
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
# included, and work similar to the +attributes+ method. For example:
#
# user.as_json(:only => [ :id, :name ])
# # => {"id": 1, "name": "Konata Izumi"}
#
# user.as_json(:except => [ :id, :created_at, :age ])
# # => {"name": "Konata Izumi", "awesome": true}
#
# To include the result of some method calls on the model use <tt>:methods</tt>:
#
# user.as_json(:methods => :permalink)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "permalink": "1-konata-izumi"}
#
# To include associations use <tt>:include</tt>:
#
# user.as_json(:include => :posts)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
# {"id": 2, author_id: 1, "title": "So I was thinking"}]}
#
# Second level and higher order associations work as well:
#
# user.as_json(:include => { :posts => {
# :include => { :comments => {
# :only => :body } },
# :only => :title } })
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
# "title": "Welcome to the weblog"},
# {"comments": [{"body": "Don't think too hard"}],
# "title": "So I was thinking"}]}
def as_json(options = nil)
root = include_root_in_json
root = options[:root] if options.try(:key?, :root)
if root
root = self.class.model_name.element if root == true
{ root => serializable_hash(options) }
else
serializable_hash(options)
end
end
def from_json(json, include_root=include_root_in_json)
hash = ActiveSupport::JSON.decode(json)
hash = hash.values.first if include_root
self.attributes = hash
self
end
end
end
end
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
module ActiveModel
# == Active Model Serializable as XML
module Serializable
module XML
extend ActiveSupport::Concern
include ActiveModel::Serializable
class Serializer #:nodoc:
class Attribute #:nodoc:
attr_reader :name, :value, :type
def initialize(name, serializable, value)
@name, @serializable = name, serializable
value = value.in_time_zone if value.respond_to?(:in_time_zone)
@value = value
@type = compute_type
end
def decorations
decorations = {}
decorations[:encoding] = 'base64' if type == :binary
decorations[:type] = (type == :string) ? nil : type
decorations[:nil] = true if value.nil?
decorations
end
protected
def compute_type
return if value.nil?
type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
type ||= :string if value.respond_to?(:to_str)
type ||= :yaml
type
end
end
class MethodAttribute < Attribute #:nodoc:
end
attr_reader :options
def initialize(serializable, options = nil)
@serializable = serializable
@options = options ? options.dup : {}
end
def serializable_hash
@serializable.serializable_hash(@options.except(:include))
end
def serializable_collection
methods = Array.wrap(options[:methods]).map(&:to_s)
serializable_hash.map do |name, value|
name = name.to_s
if methods.include?(name)
self.class::MethodAttribute.new(name, @serializable, value)
else
self.class::Attribute.new(name, @serializable, value)
end
end
end
def serialize
require 'builder' unless defined? ::Builder
options[:indent] ||= 2
options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
@builder = options[:builder]
@builder.instruct! unless options[:skip_instruct]
root = (options[:root] || @serializable.class.model_name.element).to_s
root = ActiveSupport::XmlMini.rename_key(root, options)
args = [root]
args << {:xmlns => options[:namespace]} if options[:namespace]
args << {:type => options[:type]} if options[:type] && !options[:skip_types]
@builder.tag!(*args) do
add_attributes_and_methods
add_includes
add_extra_behavior
add_procs
yield @builder if block_given?
end
end
private
def add_extra_behavior
end
def add_attributes_and_methods
serializable_collection.each do |attribute|
key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
ActiveSupport::XmlMini.to_tag(key, attribute.value,
options.merge(attribute.decorations))
end
end
def add_includes
@serializable.send(:serializable_add_includes, options) do |association, records, opts|
add_associations(association, records, opts)
end
end
# TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
def add_associations(association, records, opts)
merged_options = opts.merge(options.slice(:builder, :indent))
merged_options[:skip_instruct] = true
if records.is_a?(Enumerable)
tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
type = options[:skip_types] ? { } : {:type => "array"}
association_name = association.to_s.singularize
merged_options[:root] = association_name
if records.empty?
@builder.tag!(tag, type)
else
@builder.tag!(tag, type) do
records.each do |record|
if options[:skip_types]
record_type = {}
else
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
record_type = {:type => record_class}
end
record.to_xml merged_options.merge(record_type)
end
end
end
else
merged_options[:root] = association.to_s
records.to_xml(merged_options)
end
end
def add_procs
if procs = options.delete(:procs)
Array.wrap(procs).each do |proc|
if proc.arity == 1
proc.call(options)
else
proc.call(options, @serializable)
end
end
end
end
end
# Returns XML representing the model. Configuration can be
# passed through +options+.
#
# Without any +options+, the returned XML string will include all the model's
# attributes. For example:
#
# user = User.find(1)
# user.to_xml
#
# <?xml version="1.0" encoding="UTF-8"?>
# <user>
# <id type="integer">1</id>
# <name>David</name>
# <age type="integer">16</age>
# <created-at type="datetime">2011-01-30T22:29:23Z</created-at>
# </user>
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
# included, and work similar to the +attributes+ method.
#
# To include the result of some method calls on the model use <tt>:methods</tt>.
#
# To include associations use <tt>:include</tt>.
#
# For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml.
def to_xml(options = {}, &block)
Serializer.new(self, options).serialize(&block)
end
def from_xml(xml)
self.attributes = Hash.from_xml(xml).values.first
self
end
end
end
end
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/array/wrap'
module ActiveModel
# == Active Model Serialization
#
# Provides a basic serialization to a serializable_hash for your object.
#
# A minimal implementation could be:
#
# class Person
#
# include ActiveModel::Serialization
#
# attr_accessor :name
#
# def attributes
# {'name' => name}
# end
#
# end
#
# Which would provide you with:
#
# person = Person.new
# person.serializable_hash # => {"name"=>nil}
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
#
# You need to declare some sort of attributes hash which contains the attributes
# you want to serialize and their current value.
#
# Most of the time though, you will want to include the JSON or XML
# serializations. Both of these modules automatically include the
# ActiveModel::Serialization module, so there is no need to explicitly
# include it.
#
# So a minimal implementation including XML and JSON would be:
#
# class Person
#
# include ActiveModel::Serializers::JSON
# include ActiveModel::Serializers::Xml
#
# attr_accessor :name
#
# def attributes
# {'name' => name}
# end
#
# end
#
# Which would provide you with:
#
# person = Person.new
# person.serializable_hash # => {"name"=>nil}
# person.as_json # => {"name"=>nil}
# person.to_json # => "{\"name\":null}"
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
# person.as_json # => {"name"=>"Bob"}
# person.to_json # => "{\"name\":\"Bob\"}"
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> .
module Serialization
def serializable_hash(options = nil)
options ||= {}
attribute_names = attributes.keys.sort
if only = options[:only]
attribute_names &= Array.wrap(only).map(&:to_s)
elsif except = options[:except]
attribute_names -= Array.wrap(except).map(&:to_s)
end
hash = {}
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
method_names.each { |n| hash[n] = send(n) }
serializable_add_includes(options) do |association, records, opts|
hash[association] = if records.is_a?(Enumerable)
records.map { |a| a.serializable_hash(opts) }
else
records.serializable_hash(opts)
end
end
extend ActiveSupport::Concern
include ActiveModel::Serializable
hash
included do
ActiveSupport::Deprecation.warn "ActiveModel::Serialization is deprecated in favor of ActiveModel::Serializable"
end
private
# Hook method defining how an attribute value should be retrieved for
# serialization. By default this is assumed to be an instance named after
# the attribute. Override this method in subclasses should you need to
# retrieve the value for a given attribute differently:
#
# class MyClass
# include ActiveModel::Validations
#
# def initialize(data = {})
# @data = data
# end
#
# def read_attribute_for_serialization(key)
# @data[key]
# end
# end
#
alias :read_attribute_for_serialization :send
# Add associations specified via the <tt>:include</tt> option.
#
# Expects a block that takes as arguments:
# +association+ - name of the association
# +records+ - the association record(s) to be serialized
# +opts+ - options for the association records
def serializable_add_includes(options = {}) #:nodoc:
return unless include = options[:include]
unless include.is_a?(Hash)
include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
end
include.each do |association, opts|
if records = send(association)
yield association, records, opts
end
end
end
end
end
end
\ No newline at end of file
require "active_support/core_ext/class/attribute"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/module/anonymous"
require "set"
module ActiveModel
# Active Model Array Serializer
#
# It serializes an array checking if each element that implements
# the +active_model_serializer+ method passing down the current scope.
class ArraySerializer
attr_reader :object, :scope
def initialize(object, scope)
@object, @scope = object, scope
end
def serializable_array
@object.map do |item|
if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer)
serializer.new(item, scope)
else
item
end
end
end
def as_json(*args)
serializable_array.as_json(*args)
end
end
# Active Model Serializer
#
# Provides a basic serializer implementation that allows you to easily
# control how a given object is going to be serialized. On initialization,
# it expects to object as arguments, a resource and a scope. For example,
# one may do in a controller:
#
# PostSerializer.new(@post, current_user).to_json
#
# The object to be serialized is the +@post+ and the scope is +current_user+.
#
# We use the scope to check if a given attribute should be serialized or not.
# For example, some attributes maybe only be returned if +current_user+ is the
# author of the post:
#
# class PostSerializer < ActiveModel::Serializer
# attributes :title, :body
# has_many :comments
#
# private
#
# def attributes
# hash = super
# hash.merge!(:email => post.email) if author?
# hash
# end
#
# def author?
# post.author == scope
# end
# end
#
class Serializer
module Associations #:nodoc:
class Config < Struct.new(:name, :options) #:nodoc:
def serializer
options[:serializer]
end
end
class HasMany < Config #:nodoc:
def serialize(collection, scope)
collection.map do |item|
serializer.new(item, scope).serializable_hash
end
end
def serialize_ids(collection, scope)
# use named scopes if they are present
# return collection.ids if collection.respond_to?(:ids)
collection.map do |item|
item.read_attribute_for_serialization(:id)
end
end
end
class HasOne < Config #:nodoc:
def serialize(object, scope)
object && serializer.new(object, scope).serializable_hash
end
def serialize_ids(object, scope)
object && object.read_attribute_for_serialization(:id)
end
end
end
class_attribute :_attributes
self._attributes = Set.new
class_attribute :_associations
self._associations = []
class_attribute :_root
class_attribute :_embed
self._embed = :objects
class_attribute :_root_embed
class << self
# Define attributes to be used in the serialization.
def attributes(*attrs)
self._attributes += attrs
end
def associate(klass, attrs) #:nodoc:
options = attrs.extract_options!
self._associations += attrs.map do |attr|
unless method_defined?(attr)
class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__
end
options[:serializer] ||= const_get("#{attr.to_s.camelize}Serializer")
klass.new(attr, options)
end
end
# Defines an association in the object should be rendered.
#
# The serializer object should implement the association name
# as a method which should return an array when invoked. If a method
# with the association name does not exist, the association name is
# dispatched to the serialized object.
def has_many(*attrs)
associate(Associations::HasMany, attrs)
end
# Defines an association in the object should be rendered.
#
# The serializer object should implement the association name
# as a method which should return an object when invoked. If a method
# with the association name does not exist, the association name is
# dispatched to the serialized object.
def has_one(*attrs)
associate(Associations::HasOne, attrs)
end
# Define how associations should be embedded.
#
# embed :objects # Embed associations as full objects
# embed :ids # Embed only the association ids
# embed :ids, :include => true # Embed the association ids and include objects in the root
#
def embed(type, options={})
self._embed = type
self._root_embed = true if options[:include]
end
# Defines the root used on serialization. If false, disables the root.
def root(name)
self._root = name
end
def inherited(klass) #:nodoc:
return if klass.anonymous?
name = klass.name.demodulize.underscore.sub(/_serializer$/, '')
klass.class_eval do
alias_method name.to_sym, :object
root name.to_sym unless self._root == false
end
end
end
attr_reader :object, :scope
def initialize(object, scope)
@object, @scope = object, scope
end
# Returns a json representation of the serializable
# object including the root.
def as_json(*)
if _root
hash = { _root => serializable_hash }
hash.merge!(associations) if _root_embed
hash
else
serializable_hash
end
end
# Returns a hash representation of the serializable
# object without the root.
def serializable_hash
if _embed == :ids
attributes.merge(association_ids)
elsif _embed == :objects
attributes.merge(associations)
else
attributes
end
end
# Returns a hash representation of the serializable
# object associations.
def associations
hash = {}
_associations.each do |association|
associated_object = send(association.name)
hash[association.name] = association.serialize(associated_object, scope)
end
hash
end
# Returns a hash representation of the serializable
# object associations ids.
def association_ids
hash = {}
_associations.each do |association|
associated_object = send(association.name)
hash[association.name] = association.serialize_ids(associated_object, scope)
end
hash
end
# Returns a hash representation of the serializable
# object attributes.
def attributes
hash = {}
_attributes.each do |name|
hash[name] = @object.read_attribute_for_serialization(name)
end
hash
end
end
end
class Array
# Array uses ActiveModel::ArraySerializer.
def active_model_serializer
ActiveModel::ArraySerializer
end
end
\ No newline at end of file
require 'active_support/json'
require 'active_support/core_ext/class/attribute'
module ActiveModel
# == Active Model JSON Serializer
module Serializers
module JSON
extend ActiveSupport::Concern
include ActiveModel::Serialization
include ActiveModel::Serializable::JSON
included do
extend ActiveModel::Naming
class_attribute :include_root_in_json
self.include_root_in_json = true
end
# Returns a hash representing the model. Some configuration can be
# passed through +options+.
#
# The option <tt>include_root_in_json</tt> controls the top-level behavior
# of +as_json+. If true (the default) +as_json+ will emit a single root
# node named after the object's type. For example:
#
# user = User.find(1)
# user.as_json
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true} }
#
# ActiveRecord::Base.include_root_in_json = false
# user.as_json
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in:
#
# user = User.find(1)
# user.as_json(root: false)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# The remainder of the examples in this section assume include_root_in_json is set to
# <tt>false</tt>.
#
# Without any +options+, the returned Hash will include all the model's
# attributes. For example:
#
# user = User.find(1)
# user.as_json
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true}
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
# included, and work similar to the +attributes+ method. For example:
#
# user.as_json(:only => [ :id, :name ])
# # => {"id": 1, "name": "Konata Izumi"}
#
# user.as_json(:except => [ :id, :created_at, :age ])
# # => {"name": "Konata Izumi", "awesome": true}
#
# To include the result of some method calls on the model use <tt>:methods</tt>:
#
# user.as_json(:methods => :permalink)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "permalink": "1-konata-izumi"}
#
# To include associations use <tt>:include</tt>:
#
# user.as_json(:include => :posts)
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
# {"id": 2, author_id: 1, "title": "So I was thinking"}]}
#
# Second level and higher order associations work as well:
#
# user.as_json(:include => { :posts => {
# :include => { :comments => {
# :only => :body } },
# :only => :title } })
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
# "created_at": "2006/08/01", "awesome": true,
# "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
# "title": "Welcome to the weblog"},
# {"comments": [{"body": "Don't think too hard"}],
# "title": "So I was thinking"}]}
def as_json(options = nil)
root = include_root_in_json
root = options[:root] if options.try(:key?, :root)
if root
root = self.class.model_name.element if root == true
{ root => serializable_hash(options) }
else
serializable_hash(options)
end
end
def from_json(json, include_root=include_root_in_json)
hash = ActiveSupport::JSON.decode(json)
hash = hash.values.first if include_root
self.attributes = hash
self
ActiveSupport::Deprecation.warn "ActiveModel::Serializers::JSON is deprecated in favor of ActiveModel::Serializable::JSON"
end
end
end
end
end
\ No newline at end of file
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
module ActiveModel
# == Active Model XML Serializer
module Serializers
module Xml
extend ActiveSupport::Concern
include ActiveModel::Serialization
class Serializer #:nodoc:
class Attribute #:nodoc:
attr_reader :name, :value, :type
def initialize(name, serializable, value)
@name, @serializable = name, serializable
value = value.in_time_zone if value.respond_to?(:in_time_zone)
@value = value
@type = compute_type
end
def decorations
decorations = {}
decorations[:encoding] = 'base64' if type == :binary
decorations[:type] = (type == :string) ? nil : type
decorations[:nil] = true if value.nil?
decorations
end
protected
def compute_type
return if value.nil?
type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name]
type ||= :string if value.respond_to?(:to_str)
type ||= :yaml
type
end
end
class MethodAttribute < Attribute #:nodoc:
end
attr_reader :options
def initialize(serializable, options = nil)
@serializable = serializable
@options = options ? options.dup : {}
end
def serializable_hash
@serializable.serializable_hash(@options.except(:include))
end
def serializable_collection
methods = Array.wrap(options[:methods]).map(&:to_s)
serializable_hash.map do |name, value|
name = name.to_s
if methods.include?(name)
self.class::MethodAttribute.new(name, @serializable, value)
else
self.class::Attribute.new(name, @serializable, value)
end
end
end
def serialize
require 'builder' unless defined? ::Builder
options[:indent] ||= 2
options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
@builder = options[:builder]
@builder.instruct! unless options[:skip_instruct]
include ActiveModel::Serializable::XML
root = (options[:root] || @serializable.class.model_name.element).to_s
root = ActiveSupport::XmlMini.rename_key(root, options)
args = [root]
args << {:xmlns => options[:namespace]} if options[:namespace]
args << {:type => options[:type]} if options[:type] && !options[:skip_types]
@builder.tag!(*args) do
add_attributes_and_methods
add_includes
add_extra_behavior
add_procs
yield @builder if block_given?
end
end
private
def add_extra_behavior
end
def add_attributes_and_methods
serializable_collection.each do |attribute|
key = ActiveSupport::XmlMini.rename_key(attribute.name, options)
ActiveSupport::XmlMini.to_tag(key, attribute.value,
options.merge(attribute.decorations))
end
end
def add_includes
@serializable.send(:serializable_add_includes, options) do |association, records, opts|
add_associations(association, records, opts)
end
end
# TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well.
def add_associations(association, records, opts)
merged_options = opts.merge(options.slice(:builder, :indent))
merged_options[:skip_instruct] = true
if records.is_a?(Enumerable)
tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
type = options[:skip_types] ? { } : {:type => "array"}
association_name = association.to_s.singularize
merged_options[:root] = association_name
if records.empty?
@builder.tag!(tag, type)
else
@builder.tag!(tag, type) do
records.each do |record|
if options[:skip_types]
record_type = {}
else
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
record_type = {:type => record_class}
end
record.to_xml merged_options.merge(record_type)
end
end
end
else
merged_options[:root] = association.to_s
records.to_xml(merged_options)
end
end
def add_procs
if procs = options.delete(:procs)
Array.wrap(procs).each do |proc|
if proc.arity == 1
proc.call(options)
else
proc.call(options, @serializable)
end
end
end
end
end
# Returns XML representing the model. Configuration can be
# passed through +options+.
#
# Without any +options+, the returned XML string will include all the model's
# attributes. For example:
#
# user = User.find(1)
# user.to_xml
#
# <?xml version="1.0" encoding="UTF-8"?>
# <user>
# <id type="integer">1</id>
# <name>David</name>
# <age type="integer">16</age>
# <created-at type="datetime">2011-01-30T22:29:23Z</created-at>
# </user>
#
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
# included, and work similar to the +attributes+ method.
#
# To include the result of some method calls on the model use <tt>:methods</tt>.
#
# To include associations use <tt>:include</tt>.
#
# For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml.
def to_xml(options = {}, &block)
Serializer.new(self, options).serialize(&block)
end
Serializer = ActiveModel::Serializable::XML::Serializer
def from_xml(xml)
self.attributes = Hash.from_xml(xml).values.first
self
included do
ActiveSupport::Deprecation.warn "ActiveModel::Serializers::Xml is deprecated in favor of ActiveModel::Serializable::XML"
end
end
end
end
end
\ No newline at end of file
......@@ -5,7 +5,7 @@
class Contact
extend ActiveModel::Naming
include ActiveModel::Serializers::JSON
include ActiveModel::Serializable::JSON
include ActiveModel::Validations
def attributes=(hash)
......
......@@ -5,7 +5,7 @@
class Contact
extend ActiveModel::Naming
include ActiveModel::Serializers::Xml
include ActiveModel::Serializable::XML
attr_accessor :address, :friends
......@@ -24,7 +24,7 @@ class Customer < Struct.new(:name)
class Address
extend ActiveModel::Naming
include ActiveModel::Serializers::Xml
include ActiveModel::Serializable::XML
attr_accessor :street, :city, :state, :zip
......
......@@ -3,7 +3,7 @@
class SerializationTest < ActiveModel::TestCase
class User
include ActiveModel::Serialization
include ActiveModel::Serializable
attr_accessor :name, :email, :gender, :address, :friends
......@@ -22,7 +22,7 @@ def foo
end
class Address
include ActiveModel::Serialization
include ActiveModel::Serializable
attr_accessor :street, :city, :state, :zip
......
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
\ No newline at end of file
......@@ -2,7 +2,7 @@ module ActiveRecord #:nodoc:
# = Active Record Serialization
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializers::JSON
include ActiveModel::Serializable::JSON
def serializable_hash(options = nil)
options = options.try(:clone) || {}
......
......@@ -3,7 +3,7 @@
module ActiveRecord #:nodoc:
module Serialization
include ActiveModel::Serializers::Xml
include ActiveModel::Serializable::XML
# Builds an XML document to represent the model. Some configuration is
# available through +options+. However more complicated cases should
......@@ -176,13 +176,13 @@ def to_xml(options = {}, &block)
end
end
class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
class XmlSerializer < ActiveModel::Serializable::XML::Serializer #:nodoc:
def initialize(*args)
super
options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column)
end
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
class Attribute < ActiveModel::Serializable::XML::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
type = if klass.serialized_attributes.key?(name)
......
......@@ -10,10 +10,10 @@
# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
klass.class_eval <<-RUBY, __FILE__, __LINE__
klass.class_eval do
# Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
def to_json(options = nil)
ActiveSupport::JSON.encode(self, options)
end
RUBY
end
end
......@@ -527,7 +527,7 @@ def remove_unloadable_constants!
class ClassCache
def initialize
@store = Hash.new { |h, k| h[k] = Inflector.constantize(k) }
@store = Hash.new
end
def empty?
......@@ -538,23 +538,24 @@ def key?(key)
@store.key?(key)
end
def []=(key, value)
return unless key.respond_to?(:name)
raise(ArgumentError, 'anonymous classes cannot be cached') if key.name.blank?
@store[key.name] = value
def get(key)
key = key.name if key.respond_to?(:name)
@store[key] ||= Inflector.constantize(key)
end
alias :[] :get
def [](key)
def safe_get(key)
key = key.name if key.respond_to?(:name)
@store[key]
@store[key] || begin
klass = Inflector.safe_constantize(key)
@store[key] = klass
end
end
alias :get :[]
def store(name)
self[name] = name
def store(klass)
return self unless klass.respond_to?(:name)
raise(ArgumentError, 'anonymous classes cannot be cached') if klass.name.empty?
@store[klass.name] = klass
self
end
......@@ -571,10 +572,17 @@ def reference(klass)
end
# Get the reference for class named +name+.
# Raises an exception if referenced class does not exist.
def constantize(name)
Reference.get(name)
end
# Get the reference for class named +name+ if one exists.
# Otherwise returns nil.
def safe_constantize(name)
Reference.safe_get(name)
end
# Determine if the given constant has been automatically loaded.
def autoloaded?(desc)
# No name => anonymous module.
......
......@@ -50,9 +50,9 @@ def encode(value, use_options = true)
end
# like encode, but only calls as_json, without encoding to string
def as_json(value)
def as_json(value, use_options = true)
check_for_circular_references(value) do
value.as_json(options_for(value))
use_options ? value.as_json(options_for(value)) : value.as_json
end
end
......@@ -212,7 +212,7 @@ class Array
def as_json(options = nil) #:nodoc:
# use encoder as a proxy to call as_json on all elements, to protect from circular references
encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
map { |v| encoder.as_json(v) }
map { |v| encoder.as_json(v, options) }
end
def encode_json(encoder) #:nodoc:
......@@ -239,7 +239,7 @@ def as_json(options = nil) #:nodoc:
# use encoder as a proxy to call as_json on all values in the subset, to protect from circular references
encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
result = self.is_a?(ActiveSupport::OrderedHash) ? ActiveSupport::OrderedHash : Hash
result[subset.map { |k, v| [k.to_s, encoder.as_json(v)] }]
result[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
end
def encode_json(encoder)
......
......@@ -10,45 +10,58 @@ def setup
def test_empty?
assert @cache.empty?
@cache[ClassCacheTest] = ClassCacheTest
@cache.store(ClassCacheTest)
assert !@cache.empty?
end
def test_clear!
assert @cache.empty?
@cache[ClassCacheTest] = ClassCacheTest
@cache.store(ClassCacheTest)
assert !@cache.empty?
@cache.clear!
assert @cache.empty?
end
def test_set_key
@cache[ClassCacheTest] = ClassCacheTest
@cache.store(ClassCacheTest)
assert @cache.key?(ClassCacheTest.name)
end
def test_set_rejects_strings
@cache[ClassCacheTest.name] = ClassCacheTest
assert @cache.empty?
end
def test_get_with_class
@cache[ClassCacheTest] = ClassCacheTest
assert_equal ClassCacheTest, @cache[ClassCacheTest]
@cache.store(ClassCacheTest)
assert_equal ClassCacheTest, @cache.get(ClassCacheTest)
end
def test_get_with_name
@cache[ClassCacheTest] = ClassCacheTest
assert_equal ClassCacheTest, @cache[ClassCacheTest.name]
@cache.store(ClassCacheTest)
assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name)
end
def test_get_constantizes
assert @cache.empty?
assert_equal ClassCacheTest, @cache[ClassCacheTest.name]
assert_equal ClassCacheTest, @cache.get(ClassCacheTest.name)
end
def test_get_is_an_alias
assert_equal @cache[ClassCacheTest], @cache.get(ClassCacheTest.name)
def test_get_constantizes_fails_on_invalid_names
assert @cache.empty?
assert_raise NameError do
@cache.get("OmgTotallyInvalidConstantName")
end
end
def test_get_alias
assert @cache.empty?
assert_equal @cache[ClassCacheTest.name], @cache.get(ClassCacheTest.name)
end
def test_safe_get_constantizes
assert @cache.empty?
assert_equal ClassCacheTest, @cache.safe_get(ClassCacheTest.name)
end
def test_safe_get_constantizes_doesnt_fail_on_invalid_names
assert @cache.empty?
assert_equal nil, @cache.safe_get("OmgTotallyInvalidConstantName")
end
def test_new_rejects_strings
......
## Rails 3.2.0 (unreleased) ##
* Added displaying of mounted engine's routes with `rake routes ENGINES=true`. *Piotr Sarnacki*
* Add displaying of mounted engine's routes with `rake routes ENGINES=true` *Piotr Sarnacki*
* Allow to change the loading order of railties with `config.railties_order=`. *Piotr Sarnacki*
* Allow to change the loading order of railties with `config.railties_order=` *Piotr Sarnacki*
Example:
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*
* Updated 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*
* Default options to `rails new` can be set in ~/.railsrc *Guillermo Iguaran*
* Added destroy alias to Rails engines. *Guillermo Iguaran*
* Add destroy alias to Rails engines *Guillermo Iguaran*
* Added destroy alias for Rails command line. This allows the following: `rails d model post`. *Andrey Ognevsky*
* Add destroy alias for Rails command line. This allows the following: `rails d model post` *Andrey Ognevsky*
* Attributes on scaffold and model generators default to string. This allows the following: "rails g scaffold Post title body:text author" *José Valim*
* Removed old plugin generator (`rails generate plugin`) in favor of `rails plugin new` command. *Guillermo Iguaran*
* Remove old plugin generator (`rails generate plugin`) in favor of `rails plugin new` command *Guillermo Iguaran*
* Removed old 'config.paths.app.controller' API in favor of 'config.paths["app/controller"]' API. *Guillermo Iguaran*
* Remove old 'config.paths.app.controller' API in favor of 'config.paths["app/controller"]' API *Guillermo Iguaran*
* Rails 3.1.1
......
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,7 +33,8 @@ module Generators
:stylesheets => '-y',
:stylesheet_engine => '-se',
:template_engine => '-e',
:test_framework => '-t'
:test_framework => '-t',
:serializer => '-z'
},
:test_unit => {
......@@ -58,6 +59,7 @@ module Generators
:performance_tool => nil,
:resource_controller => :controller,
:scaffold_controller => :scaffold_controller,
:serializer => false,
:stylesheets => true,
:stylesheet_engine => :css,
:test_framework => false,
......
......@@ -10,6 +10,7 @@ class ScaffoldGenerator < ResourceGenerator #metagenerator
class_option :stylesheet_engine, :desc => "Engine for Stylesheets"
hook_for :scaffold_controller, :required => true
hook_for :serializer
hook_for :assets do |assets|
invoke assets, [controller_name]
......
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
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
<% 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 -%>
\ No newline at end of file
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
require 'test_helper'
<% module_namespacing do -%>
class <%= class_name %>SerializerTest < ActiveSupport::TestCase
# test "the truth" do
# assert true
# end
end
<% end -%>
......@@ -264,6 +264,15 @@ def test_scaffold_generator_no_javascripts
assert_file "app/assets/stylesheets/posts.css"
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
run_generator ["post", "title", "body:text", "author"]
......
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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册