Revert "Implement ArraySerializer and move old serialization API to a new namespace."

This reverts commit 8896b4fdc8a543157cdf4dfc378607ebf6c10ab0.

Conflicts:

	activemodel/lib/active_model.rb
	activemodel/lib/active_model/serializable.rb
	activemodel/lib/active_model/serializer.rb
	activemodel/test/cases/serializer_test.rb
This commit is contained in:
José Valim 2011-11-30 18:38:28 +01:00
parent be99f0c7eb
commit 5b2eb64ceb
16 changed files with 433 additions and 736 deletions

@ -5,14 +5,6 @@
*Jon Leighton*
* 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*

@ -43,7 +43,6 @@ module ActiveModel
autoload :Observer, 'active_model/observing'
autoload :Observing
autoload :SecurePassword
autoload :Serializable
autoload :Serialization
autoload :TestCase
autoload :Translation

@ -1,144 +0,0 @@
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"
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
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

@ -1,108 +0,0 @@
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

@ -1,195 +0,0 @@
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,10 +1,139 @@
module ActiveModel
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializable
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/array/wrap'
included do
ActiveSupport::Deprecation.warn "ActiveModel::Serialization is deprecated in favor of ActiveModel::Serializable"
module ActiveModel
# == Active Model Serialization
#
# Provides a basic serialization to a serializable_hash for your object.
#
# A minimal implementation could be:
#
# class Person
#
# include ActiveModel::Serialization
#
# attr_accessor :name
#
# def attributes
# {'name' => name}
# end
#
# end
#
# Which would provide you with:
#
# person = Person.new
# person.serializable_hash # => {"name"=>nil}
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
#
# You need to declare some sort of attributes hash which contains the attributes
# you want to serialize and their current value.
#
# Most of the time though, you will want to include the JSON or XML
# serializations. Both of these modules automatically include the
# ActiveModel::Serialization module, so there is no need to explicitly
# include it.
#
# So a minimal implementation including XML and JSON would be:
#
# class Person
#
# include ActiveModel::Serializers::JSON
# include ActiveModel::Serializers::Xml
#
# attr_accessor :name
#
# def attributes
# {'name' => name}
# end
#
# end
#
# Which would provide you with:
#
# person = Person.new
# person.serializable_hash # => {"name"=>nil}
# person.as_json # => {"name"=>nil}
# person.to_json # => "{\"name\":null}"
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
# person.as_json # => {"name"=>"Bob"}
# person.to_json # => "{\"name\":\"Bob\"}"
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
#
# Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> .
module Serialization
def serializable_hash(options = nil)
options ||= {}
attribute_names = attributes.keys.sort
if only = options[:only]
attribute_names &= Array.wrap(only).map(&:to_s)
elsif except = options[:except]
attribute_names -= Array.wrap(except).map(&:to_s)
end
hash = {}
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
method_names.each { |n| hash[n] = send(n) }
serializable_add_includes(options) do |association, records, opts|
hash[association] = if records.is_a?(Enumerable)
records.map { |a| a.serializable_hash(opts) }
else
records.serializable_hash(opts)
end
end
hash
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

@ -1,253 +0,0 @@
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,12 +1,108 @@
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::Serializable::JSON
include ActiveModel::Serialization
included do
ActiveSupport::Deprecation.warn "ActiveModel::Serializers::JSON is deprecated in favor of ActiveModel::Serializable::JSON"
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
end

@ -1,14 +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 XML Serializer
module Serializers
module Xml
extend ActiveSupport::Concern
include ActiveModel::Serializable::XML
include ActiveModel::Serialization
Serializer = ActiveModel::Serializable::XML::Serializer
class Serializer #:nodoc:
class Attribute #:nodoc:
attr_reader :name, :value, :type
included do
ActiveSupport::Deprecation.warn "ActiveModel::Serializers::Xml is deprecated in favor of ActiveModel::Serializable::XML"
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
end

@ -3,7 +3,7 @@
class SerializationTest < ActiveModel::TestCase
class User
include ActiveModel::Serializable
include ActiveModel::Serialization
attr_accessor :name, :email, :gender, :address, :friends
@ -22,7 +22,7 @@ def foo
end
class Address
include ActiveModel::Serializable
include ActiveModel::Serialization
attr_accessor :street, :city, :state, :zip

@ -5,7 +5,7 @@
class Contact
extend ActiveModel::Naming
include ActiveModel::Serializable::JSON
include ActiveModel::Serializers::JSON
include ActiveModel::Validations
def attributes=(hash)

@ -5,7 +5,7 @@
class Contact
extend ActiveModel::Naming
include ActiveModel::Serializable::XML
include ActiveModel::Serializers::Xml
attr_accessor :address, :friends
@ -26,7 +26,7 @@ class Customer < Struct.new(:name)
class Address
extend ActiveModel::Naming
include ActiveModel::Serializable::XML
include ActiveModel::Serializers::Xml
attr_accessor :street, :city, :state, :zip

@ -2,7 +2,7 @@ module ActiveRecord #:nodoc:
# = Active Record Serialization
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializable::JSON
include ActiveModel::Serializers::JSON
def serializable_hash(options = nil)
options = options.try(:clone) || {}

@ -3,7 +3,7 @@
module ActiveRecord #:nodoc:
module Serialization
include ActiveModel::Serializable::XML
include ActiveModel::Serializers::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::Serializable::XML::Serializer #:nodoc:
class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc:
def initialize(*args)
super
options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column)
end
class Attribute < ActiveModel::Serializable::XML::Serializer::Attribute #:nodoc:
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
type = if klass.serialized_attributes.key?(name)

@ -1477,7 +1477,7 @@ class Base
extend ActiveModel::Naming
include CustomMethods, Observing, Validations
include ActiveModel::Conversion
include ActiveModel::Serializable::JSON
include ActiveModel::Serializable::XML
include ActiveModel::Serializers::JSON
include ActiveModel::Serializers::Xml
end
end

@ -281,4 +281,4 @@ def as_json(options = nil) #:nodoc:
strftime('%Y/%m/%d %H:%M:%S %z')
end
end
end
end