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
This commit is contained in:
commit
fcacc6986a
@ -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,
|
||||
|
@ -1,5 +1,6 @@
|
||||
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
|
||||
|
51
actionpack/lib/action_controller/metal/serialization.rb
Normal file
51
actionpack/lib/action_controller/metal/serialization.rb
Normal file
@ -0,0 +1,51 @@
|
||||
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
|
||||
|
@ -1,4 +1,16 @@
|
||||
* 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
|
||||
|
156
activemodel/lib/active_model/serializable.rb
Normal file
156
activemodel/lib/active_model/serializable.rb
Normal file
@ -0,0 +1,156 @@
|
||||
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
|
108
activemodel/lib/active_model/serializable/json.rb
Normal file
108
activemodel/lib/active_model/serializable/json.rb
Normal file
@ -0,0 +1,108 @@
|
||||
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
|
195
activemodel/lib/active_model/serializable/xml.rb
Normal file
195
activemodel/lib/active_model/serializable/xml.rb
Normal file
@ -0,0 +1,195 @@
|
||||
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
|
@ -1,139 +1,10 @@
|
||||
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 ||= {}
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::Serializable
|
||||
|
||||
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
|
||||
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
|
253
activemodel/lib/active_model/serializer.rb
Normal file
253
activemodel/lib/active_model/serializer.rb
Normal file
@ -0,0 +1,253 @@
|
||||
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
|
@ -1,108 +1,12 @@
|
||||
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
|
@ -1,195 +1,14 @@
|
||||
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
|
||||
include ActiveModel::Serializable::XML
|
||||
|
||||
class Serializer #:nodoc:
|
||||
class Attribute #:nodoc:
|
||||
attr_reader :name, :value, :type
|
||||
Serializer = ActiveModel::Serializable::XML::Serializer
|
||||
|
||||
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
|
||||
included do
|
||||
ActiveSupport::Deprecation.warn "ActiveModel::Serializers::Xml is deprecated in favor of ActiveModel::Serializable::XML"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -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
|
||||
|
432
activemodel/test/cases/serializer_test.rb
Normal file
432
activemodel/test/cases/serializer_test.rb
Normal file
@ -0,0 +1,432 @@
|
||||
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
|
@ -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
|
||||
end
|
||||
|
||||
def [](key)
|
||||
def get(key)
|
||||
key = key.name if key.respond_to?(:name)
|
||||
|
||||
@store[key]
|
||||
@store[key] ||= Inflector.constantize(key)
|
||||
end
|
||||
alias :get :[]
|
||||
alias :[] :get
|
||||
|
||||
def store(name)
|
||||
self[name] = name
|
||||
def safe_get(key)
|
||||
key = key.name if key.respond_to?(:name)
|
||||
@store[key] || begin
|
||||
klass = Inflector.safe_constantize(key)
|
||||
@store[key] = klass
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
@ -1,27 +1,29 @@
|
||||
## 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
|
||||
|
563
railties/guides/source/serializers.textile
Normal file
563
railties/guides/source/serializers.textile
Normal file
@ -0,0 +1,563 @@
|
||||
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]
|
||||
|
9
railties/lib/rails/generators/rails/serializer/USAGE
Normal file
9
railties/lib/rails/generators/rails/serializer/USAGE
Normal file
@ -0,0 +1,9 @@
|
||||
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
|
@ -0,0 +1,39 @@
|
||||
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
|
@ -0,0 +1,9 @@
|
||||
<% 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 -%>
|
@ -0,0 +1,13 @@
|
||||
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
|
@ -0,0 +1,9 @@
|
||||
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"]
|
||||
|
||||
|
63
railties/test/generators/serializer_generator_test.rb
Normal file
63
railties/test/generators/serializer_generator_test.rb
Normal file
@ -0,0 +1,63 @@
|
||||
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
|
Loading…
Reference in New Issue
Block a user