Skip to content

Commit 8896b4f

Browse files
committed
Implement ArraySerializer and move old serialization API to a new namespace.
The following constants were renamed: ActiveModel::Serialization => ActiveModel::Serializable ActiveModel::Serializers::JSON => ActiveModel::Serializable::JSON ActiveModel::Serializers::Xml => ActiveModel::Serializable::XML The main motivation for such a change is that `ActiveModel::Serializers::JSON` was not actually a serializer, but a module that when included allows the target to be serializable to JSON. With such changes, we were able to clean up the namespace to add true serializers as the ArraySerializer.
1 parent 0536ea8 commit 8896b4f

File tree

15 files changed

+610
-436
lines changed

15 files changed

+610
-436
lines changed

activemodel/lib/active_model.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
module ActiveModel
3030
extend ActiveSupport::Autoload
3131

32+
autoload :ArraySerializer, 'active_model/serializer'
3233
autoload :AttributeMethods
3334
autoload :BlockValidator, 'active_model/validator'
3435
autoload :Callbacks
@@ -43,8 +44,9 @@ module ActiveModel
4344
autoload :Observer, 'active_model/observing'
4445
autoload :Observing
4546
autoload :SecurePassword
46-
autoload :Serializer
47+
autoload :Serializable
4748
autoload :Serialization
49+
autoload :Serializer
4850
autoload :TestCase
4951
autoload :Translation
5052
autoload :Validations
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
require 'active_support/core_ext/hash/except'
2+
require 'active_support/core_ext/hash/slice'
3+
require 'active_support/core_ext/array/wrap'
4+
5+
module ActiveModel
6+
# == Active Model Serializable
7+
#
8+
# Provides a basic serialization to a serializable_hash for your object.
9+
#
10+
# A minimal implementation could be:
11+
#
12+
# class Person
13+
#
14+
# include ActiveModel::Serializable
15+
#
16+
# attr_accessor :name
17+
#
18+
# def attributes
19+
# {'name' => name}
20+
# end
21+
#
22+
# end
23+
#
24+
# Which would provide you with:
25+
#
26+
# person = Person.new
27+
# person.serializable_hash # => {"name"=>nil}
28+
# person.name = "Bob"
29+
# person.serializable_hash # => {"name"=>"Bob"}
30+
#
31+
# You need to declare some sort of attributes hash which contains the attributes
32+
# you want to serialize and their current value.
33+
#
34+
# Most of the time though, you will want to include the JSON or XML
35+
# serializations. Both of these modules automatically include the
36+
# ActiveModel::Serialization module, so there is no need to explicitly
37+
# include it.
38+
#
39+
# So a minimal implementation including XML and JSON would be:
40+
#
41+
# class Person
42+
#
43+
# include ActiveModel::Serializable::JSON
44+
# include ActiveModel::Serializable::XML
45+
#
46+
# attr_accessor :name
47+
#
48+
# def attributes
49+
# {'name' => name}
50+
# end
51+
#
52+
# end
53+
#
54+
# Which would provide you with:
55+
#
56+
# person = Person.new
57+
# person.serializable_hash # => {"name"=>nil}
58+
# person.as_json # => {"name"=>nil}
59+
# person.to_json # => "{\"name\":null}"
60+
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
61+
#
62+
# person.name = "Bob"
63+
# person.serializable_hash # => {"name"=>"Bob"}
64+
# person.as_json # => {"name"=>"Bob"}
65+
# person.to_json # => "{\"name\":\"Bob\"}"
66+
# person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
67+
#
68+
# Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> .
69+
module Serializable
70+
extend ActiveSupport::Concern
71+
72+
autoload :JSON, "active_model/serializable/json"
73+
autoload :XML, "active_model/serializable/xml"
74+
75+
include ActiveModel::Serializer::Scope
76+
77+
module ClassMethods #:nodoc:
78+
def _model_serializer
79+
@_model_serializer ||= ActiveModel::Serializer::Finder.find(self, self)
80+
end
81+
end
82+
83+
def serializable_hash(options = nil)
84+
options ||= {}
85+
86+
attribute_names = attributes.keys.sort
87+
if only = options[:only]
88+
attribute_names &= Array.wrap(only).map(&:to_s)
89+
elsif except = options[:except]
90+
attribute_names -= Array.wrap(except).map(&:to_s)
91+
end
92+
93+
hash = {}
94+
attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
95+
96+
method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
97+
method_names.each { |n| hash[n] = send(n) }
98+
99+
serializable_add_includes(options) do |association, records, opts|
100+
hash[association] = if records.is_a?(Enumerable)
101+
records.map { |a| a.serializable_hash(opts) }
102+
else
103+
records.serializable_hash(opts)
104+
end
105+
end
106+
107+
hash
108+
end
109+
110+
# Returns a model serializer for this object considering its namespace.
111+
def model_serializer
112+
self.class._model_serializer
113+
end
114+
115+
private
116+
117+
# Hook method defining how an attribute value should be retrieved for
118+
# serialization. By default this is assumed to be an instance named after
119+
# the attribute. Override this method in subclasses should you need to
120+
# retrieve the value for a given attribute differently:
121+
#
122+
# class MyClass
123+
# include ActiveModel::Validations
124+
#
125+
# def initialize(data = {})
126+
# @data = data
127+
# end
128+
#
129+
# def read_attribute_for_serialization(key)
130+
# @data[key]
131+
# end
132+
# end
133+
#
134+
alias :read_attribute_for_serialization :send
135+
136+
# Add associations specified via the <tt>:include</tt> option.
137+
#
138+
# Expects a block that takes as arguments:
139+
# +association+ - name of the association
140+
# +records+ - the association record(s) to be serialized
141+
# +opts+ - options for the association records
142+
def serializable_add_includes(options = {}) #:nodoc:
143+
return unless include = options[:include]
144+
145+
unless include.is_a?(Hash)
146+
include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
147+
end
148+
149+
include.each do |association, opts|
150+
if records = send(association)
151+
yield association, records, opts
152+
end
153+
end
154+
end
155+
end
156+
end
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
require 'active_support/json'
2+
require 'active_support/core_ext/class/attribute'
3+
4+
module ActiveModel
5+
# == Active Model Serializable as JSON
6+
module Serializable
7+
module JSON
8+
extend ActiveSupport::Concern
9+
include ActiveModel::Serializable
10+
11+
included do
12+
extend ActiveModel::Naming
13+
14+
class_attribute :include_root_in_json
15+
self.include_root_in_json = true
16+
end
17+
18+
# Returns a hash representing the model. Some configuration can be
19+
# passed through +options+.
20+
#
21+
# The option <tt>include_root_in_json</tt> controls the top-level behavior
22+
# of +as_json+. If true (the default) +as_json+ will emit a single root
23+
# node named after the object's type. For example:
24+
#
25+
# user = User.find(1)
26+
# user.as_json
27+
# # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
28+
# "created_at": "2006/08/01", "awesome": true} }
29+
#
30+
# ActiveRecord::Base.include_root_in_json = false
31+
# user.as_json
32+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
33+
# "created_at": "2006/08/01", "awesome": true}
34+
#
35+
# This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in:
36+
#
37+
# user = User.find(1)
38+
# user.as_json(root: false)
39+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
40+
# "created_at": "2006/08/01", "awesome": true}
41+
#
42+
# The remainder of the examples in this section assume include_root_in_json is set to
43+
# <tt>false</tt>.
44+
#
45+
# Without any +options+, the returned Hash will include all the model's
46+
# attributes. For example:
47+
#
48+
# user = User.find(1)
49+
# user.as_json
50+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
51+
# "created_at": "2006/08/01", "awesome": true}
52+
#
53+
# The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
54+
# included, and work similar to the +attributes+ method. For example:
55+
#
56+
# user.as_json(:only => [ :id, :name ])
57+
# # => {"id": 1, "name": "Konata Izumi"}
58+
#
59+
# user.as_json(:except => [ :id, :created_at, :age ])
60+
# # => {"name": "Konata Izumi", "awesome": true}
61+
#
62+
# To include the result of some method calls on the model use <tt>:methods</tt>:
63+
#
64+
# user.as_json(:methods => :permalink)
65+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
66+
# "created_at": "2006/08/01", "awesome": true,
67+
# "permalink": "1-konata-izumi"}
68+
#
69+
# To include associations use <tt>:include</tt>:
70+
#
71+
# user.as_json(:include => :posts)
72+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
73+
# "created_at": "2006/08/01", "awesome": true,
74+
# "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"},
75+
# {"id": 2, author_id: 1, "title": "So I was thinking"}]}
76+
#
77+
# Second level and higher order associations work as well:
78+
#
79+
# user.as_json(:include => { :posts => {
80+
# :include => { :comments => {
81+
# :only => :body } },
82+
# :only => :title } })
83+
# # => {"id": 1, "name": "Konata Izumi", "age": 16,
84+
# "created_at": "2006/08/01", "awesome": true,
85+
# "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}],
86+
# "title": "Welcome to the weblog"},
87+
# {"comments": [{"body": "Don't think too hard"}],
88+
# "title": "So I was thinking"}]}
89+
def as_json(options = nil)
90+
root = include_root_in_json
91+
root = options[:root] if options.try(:key?, :root)
92+
if root
93+
root = self.class.model_name.element if root == true
94+
{ root => serializable_hash(options) }
95+
else
96+
serializable_hash(options)
97+
end
98+
end
99+
100+
def from_json(json, include_root=include_root_in_json)
101+
hash = ActiveSupport::JSON.decode(json)
102+
hash = hash.values.first if include_root
103+
self.attributes = hash
104+
self
105+
end
106+
end
107+
end
108+
end

0 commit comments

Comments
 (0)