active_model_serializers 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/README.textile +558 -0
- data/Rakefile +13 -0
- data/active_model_serializers.gemspec +18 -0
- data/lib/action_controller/serialization.rb +52 -0
- data/lib/active_model/serializer.rb +353 -0
- data/lib/active_model_serializers.rb +48 -0
- data/lib/generators/serializer/USAGE +9 -0
- data/lib/generators/serializer/serializer_generator.rb +41 -0
- data/lib/generators/serializer/templates/serializer.rb +9 -0
- data/test/generators_test.rb +67 -0
- data/test/serialization_test.rb +186 -0
- data/test/serializer_support_test.rb +11 -0
- data/test/serializer_test.rb +787 -0
- data/test/test_helper.rb +24 -0
- metadata +92 -0
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rake/testtask"
|
4
|
+
|
5
|
+
desc 'Run tests'
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
7
|
+
t.libs << 'lib'
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = true
|
11
|
+
end
|
12
|
+
|
13
|
+
task :default => :test
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |gem|
|
3
|
+
gem.authors = ["José Valim", "Yehuda Katz"]
|
4
|
+
gem.email = ["jose.valim@gmail.com", "wycats@gmail.com"]
|
5
|
+
gem.description = %q{Making it easy to serialize models for client-side use}
|
6
|
+
gem.summary = %q{Bringing consistency and object orientation to model serialization. Works great for client-side MVC frameworks!}
|
7
|
+
gem.homepage = ""
|
8
|
+
|
9
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
10
|
+
gem.files = `git ls-files`.split("\n")
|
11
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
12
|
+
gem.name = "active_model_serializers"
|
13
|
+
gem.require_paths = ["lib"]
|
14
|
+
gem.version = "0.1.0"
|
15
|
+
|
16
|
+
gem.add_dependency 'activemodel', '~> 3.0'
|
17
|
+
gem.add_development_dependency "rails", "~> 3.0"
|
18
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ActionController
|
2
|
+
# Action Controller Serialization
|
3
|
+
#
|
4
|
+
# Overrides render :json to check if the given object implements +active_model_serializer+
|
5
|
+
# as a method. If so, use the returned serializer instead of calling +to_json+ in the object.
|
6
|
+
#
|
7
|
+
# This module also provides a serialization_scope method that allows you to configure the
|
8
|
+
# +serialization_scope+ of the serializer. Most apps will likely set the +serialization_scope+
|
9
|
+
# to the current user:
|
10
|
+
#
|
11
|
+
# class ApplicationController < ActionController::Base
|
12
|
+
# serialization_scope :current_user
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# If you need more complex scope rules, you can simply override the serialization_scope:
|
16
|
+
#
|
17
|
+
# class ApplicationController < ActionController::Base
|
18
|
+
# private
|
19
|
+
#
|
20
|
+
# def serialization_scope
|
21
|
+
# current_user
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
module Serialization
|
26
|
+
extend ActiveSupport::Concern
|
27
|
+
|
28
|
+
include ActionController::Renderers
|
29
|
+
|
30
|
+
included do
|
31
|
+
class_attribute :_serialization_scope
|
32
|
+
self._serialization_scope = :current_user
|
33
|
+
end
|
34
|
+
|
35
|
+
def serialization_scope
|
36
|
+
send(_serialization_scope)
|
37
|
+
end
|
38
|
+
|
39
|
+
def _render_option_json(json, options)
|
40
|
+
if json.respond_to?(:active_model_serializer) && (serializer = json.active_model_serializer)
|
41
|
+
json = serializer.new(json, serialization_scope, options)
|
42
|
+
end
|
43
|
+
super
|
44
|
+
end
|
45
|
+
|
46
|
+
module ClassMethods
|
47
|
+
def serialization_scope(scope)
|
48
|
+
self._serialization_scope = scope
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,353 @@
|
|
1
|
+
require "active_support/core_ext/class/attribute"
|
2
|
+
require "active_support/core_ext/module/anonymous"
|
3
|
+
|
4
|
+
module ActiveModel
|
5
|
+
# Active Model Array Serializer
|
6
|
+
#
|
7
|
+
# It serializes an array checking if each element that implements
|
8
|
+
# the +active_model_serializer+ method passing down the current scope.
|
9
|
+
class ArraySerializer
|
10
|
+
attr_reader :object, :scope
|
11
|
+
|
12
|
+
def initialize(object, scope, options={})
|
13
|
+
@object, @scope, @options = object, scope, options
|
14
|
+
@hash = options[:hash]
|
15
|
+
end
|
16
|
+
|
17
|
+
def serializable_array
|
18
|
+
@object.map do |item|
|
19
|
+
if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer)
|
20
|
+
serializer.new(item, scope, :hash => @hash)
|
21
|
+
else
|
22
|
+
item
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def as_json(*args)
|
28
|
+
@hash = {}
|
29
|
+
array = serializable_array.as_json(*args)
|
30
|
+
|
31
|
+
if root = @options[:root]
|
32
|
+
@hash.merge!(root => array)
|
33
|
+
else
|
34
|
+
array
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Active Model Serializer
|
40
|
+
#
|
41
|
+
# Provides a basic serializer implementation that allows you to easily
|
42
|
+
# control how a given object is going to be serialized. On initialization,
|
43
|
+
# it expects to object as arguments, a resource and a scope. For example,
|
44
|
+
# one may do in a controller:
|
45
|
+
#
|
46
|
+
# PostSerializer.new(@post, current_user).to_json
|
47
|
+
#
|
48
|
+
# The object to be serialized is the +@post+ and the scope is +current_user+.
|
49
|
+
#
|
50
|
+
# We use the scope to check if a given attribute should be serialized or not.
|
51
|
+
# For example, some attributes maybe only be returned if +current_user+ is the
|
52
|
+
# author of the post:
|
53
|
+
#
|
54
|
+
# class PostSerializer < ActiveModel::Serializer
|
55
|
+
# attributes :title, :body
|
56
|
+
# has_many :comments
|
57
|
+
#
|
58
|
+
# private
|
59
|
+
#
|
60
|
+
# def attributes
|
61
|
+
# hash = super
|
62
|
+
# hash.merge!(:email => post.email) if author?
|
63
|
+
# hash
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# def author?
|
67
|
+
# post.author == scope
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
class Serializer
|
72
|
+
module Associations #:nodoc:
|
73
|
+
class Config < Struct.new(:name, :options) #:nodoc:
|
74
|
+
def serializer
|
75
|
+
options[:serializer]
|
76
|
+
end
|
77
|
+
|
78
|
+
def key
|
79
|
+
options[:key] || name
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def find_serializable(object, scope, context, options)
|
85
|
+
if serializer
|
86
|
+
serializer.new(object, scope, options)
|
87
|
+
elsif object.respond_to?(:active_model_serializer) && (ams = object.active_model_serializer)
|
88
|
+
ams.new(object, scope, options)
|
89
|
+
else
|
90
|
+
object
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class HasMany < Config #:nodoc:
|
96
|
+
def serialize(collection, scope, context, options)
|
97
|
+
array = collection.map do |item|
|
98
|
+
find_serializable(item, scope, context, options).as_json(:root => false)
|
99
|
+
end
|
100
|
+
{ key => array }
|
101
|
+
end
|
102
|
+
|
103
|
+
def serialize_ids(collection, scope)
|
104
|
+
# Use pluck or select_columns if available
|
105
|
+
# return collection.ids if collection.respond_to?(:ids)
|
106
|
+
|
107
|
+
array = collection.map do |item|
|
108
|
+
item.read_attribute_for_serialization(:id)
|
109
|
+
end
|
110
|
+
|
111
|
+
{ key => array }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class HasOne < Config #:nodoc:
|
116
|
+
def serialize(object, scope, context, options)
|
117
|
+
{ key => object && find_serializable(object, scope, context, options).as_json(:root => false) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def serialize_ids(object, scope)
|
121
|
+
{ key => object && object.read_attribute_for_serialization(:id) }
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class_attribute :_attributes
|
127
|
+
self._attributes = {}
|
128
|
+
|
129
|
+
class_attribute :_associations
|
130
|
+
self._associations = []
|
131
|
+
|
132
|
+
class_attribute :_root
|
133
|
+
class_attribute :_embed
|
134
|
+
self._embed = :objects
|
135
|
+
class_attribute :_root_embed
|
136
|
+
|
137
|
+
class << self
|
138
|
+
# Define attributes to be used in the serialization.
|
139
|
+
def attributes(*attrs)
|
140
|
+
self._attributes = _attributes.dup
|
141
|
+
|
142
|
+
attrs.each do |attr|
|
143
|
+
self._attributes[attr] = attr
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def attribute(attr, options={})
|
148
|
+
self._attributes = _attributes.merge(attr => options[:key] || attr)
|
149
|
+
end
|
150
|
+
|
151
|
+
def associate(klass, attrs) #:nodoc:
|
152
|
+
options = attrs.extract_options!
|
153
|
+
self._associations += attrs.map do |attr|
|
154
|
+
unless method_defined?(attr)
|
155
|
+
class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__
|
156
|
+
end
|
157
|
+
klass.new(attr, options)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
# Defines an association in the object should be rendered.
|
162
|
+
#
|
163
|
+
# The serializer object should implement the association name
|
164
|
+
# as a method which should return an array when invoked. If a method
|
165
|
+
# with the association name does not exist, the association name is
|
166
|
+
# dispatched to the serialized object.
|
167
|
+
def has_many(*attrs)
|
168
|
+
associate(Associations::HasMany, attrs)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Defines an association in the object should be rendered.
|
172
|
+
#
|
173
|
+
# The serializer object should implement the association name
|
174
|
+
# as a method which should return an object when invoked. If a method
|
175
|
+
# with the association name does not exist, the association name is
|
176
|
+
# dispatched to the serialized object.
|
177
|
+
def has_one(*attrs)
|
178
|
+
associate(Associations::HasOne, attrs)
|
179
|
+
end
|
180
|
+
|
181
|
+
# Return a schema hash for the current serializer. This information
|
182
|
+
# can be used to generate clients for the serialized output.
|
183
|
+
#
|
184
|
+
# The schema hash has two keys: +attributes+ and +associations+.
|
185
|
+
#
|
186
|
+
# The +attributes+ hash looks like this:
|
187
|
+
#
|
188
|
+
# { :name => :string, :age => :integer }
|
189
|
+
#
|
190
|
+
# The +associations+ hash looks like this:
|
191
|
+
#
|
192
|
+
# { :posts => { :has_many => :posts } }
|
193
|
+
#
|
194
|
+
# If :key is used:
|
195
|
+
#
|
196
|
+
# class PostsSerializer < ActiveModel::Serializer
|
197
|
+
# has_many :posts, :key => :my_posts
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# the hash looks like this:
|
201
|
+
#
|
202
|
+
# { :my_posts => { :has_many => :posts }
|
203
|
+
#
|
204
|
+
# This information is extracted from the serializer's model class,
|
205
|
+
# which is provided by +SerializerClass.model_class+.
|
206
|
+
#
|
207
|
+
# The schema method uses the +columns_hash+ and +reflect_on_association+
|
208
|
+
# methods, provided by default by ActiveRecord. You can implement these
|
209
|
+
# methods on your custom models if you want the serializer's schema method
|
210
|
+
# to work.
|
211
|
+
#
|
212
|
+
# TODO: This is currently coupled to Active Record. We need to
|
213
|
+
# figure out a way to decouple those two.
|
214
|
+
def schema
|
215
|
+
klass = model_class
|
216
|
+
columns = klass.columns_hash
|
217
|
+
|
218
|
+
attrs = _attributes.inject({}) do |hash, (name,key)|
|
219
|
+
column = columns[name.to_s]
|
220
|
+
hash.merge key => column.type
|
221
|
+
end
|
222
|
+
|
223
|
+
associations = _associations.inject({}) do |hash, association|
|
224
|
+
model_association = klass.reflect_on_association(association.name)
|
225
|
+
hash.merge association.key => { model_association.macro => model_association.name }
|
226
|
+
end
|
227
|
+
|
228
|
+
{ :attributes => attrs, :associations => associations }
|
229
|
+
end
|
230
|
+
|
231
|
+
# The model class associated with this serializer.
|
232
|
+
def model_class
|
233
|
+
name.sub(/Serializer$/, '').constantize
|
234
|
+
end
|
235
|
+
|
236
|
+
# Define how associations should be embedded.
|
237
|
+
#
|
238
|
+
# embed :objects # Embed associations as full objects
|
239
|
+
# embed :ids # Embed only the association ids
|
240
|
+
# embed :ids, :include => true # Embed the association ids and include objects in the root
|
241
|
+
#
|
242
|
+
def embed(type, options={})
|
243
|
+
self._embed = type
|
244
|
+
self._root_embed = true if options[:include]
|
245
|
+
end
|
246
|
+
|
247
|
+
# Defines the root used on serialization. If false, disables the root.
|
248
|
+
def root(name)
|
249
|
+
self._root = name
|
250
|
+
end
|
251
|
+
|
252
|
+
def inherited(klass) #:nodoc:
|
253
|
+
return if klass.anonymous?
|
254
|
+
name = klass.name.demodulize.underscore.sub(/_serializer$/, '')
|
255
|
+
|
256
|
+
klass.class_eval do
|
257
|
+
alias_method name.to_sym, :object
|
258
|
+
root name.to_sym unless self._root == false
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
attr_reader :object, :scope
|
264
|
+
|
265
|
+
def initialize(object, scope, options={})
|
266
|
+
@object, @scope, @options = object, scope, options
|
267
|
+
@hash = options[:hash]
|
268
|
+
end
|
269
|
+
|
270
|
+
# Returns a json representation of the serializable
|
271
|
+
# object including the root.
|
272
|
+
def as_json(options=nil)
|
273
|
+
options ||= {}
|
274
|
+
if root = options.fetch(:root, @options.fetch(:root, _root))
|
275
|
+
@hash = hash = {}
|
276
|
+
hash.merge!(root => serializable_hash)
|
277
|
+
hash
|
278
|
+
else
|
279
|
+
@hash = serializable_hash
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Returns a hash representation of the serializable
|
284
|
+
# object without the root.
|
285
|
+
def serializable_hash
|
286
|
+
if _embed == :ids
|
287
|
+
merge_associations(@hash, associations) if _root_embed
|
288
|
+
attributes.merge(association_ids)
|
289
|
+
elsif _embed == :objects
|
290
|
+
attributes.merge(associations)
|
291
|
+
else
|
292
|
+
attributes
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
# Merge associations for embed case by always adding
|
297
|
+
# root associations to the given hash.
|
298
|
+
def merge_associations(hash, associations)
|
299
|
+
associations.each do |key, value|
|
300
|
+
if hash[key]
|
301
|
+
hash[key] |= value
|
302
|
+
elsif value
|
303
|
+
hash[key] = value
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Returns a hash representation of the serializable
|
309
|
+
# object associations.
|
310
|
+
def associations
|
311
|
+
hash = {}
|
312
|
+
|
313
|
+
_associations.each do |association|
|
314
|
+
associated_object = send(association.name)
|
315
|
+
hash.merge! association.serialize(associated_object, scope, self, :hash => @hash)
|
316
|
+
end
|
317
|
+
|
318
|
+
hash
|
319
|
+
end
|
320
|
+
|
321
|
+
# Returns a hash representation of the serializable
|
322
|
+
# object associations ids.
|
323
|
+
def association_ids
|
324
|
+
hash = {}
|
325
|
+
|
326
|
+
_associations.each do |association|
|
327
|
+
associated_object = send(association.name)
|
328
|
+
hash.merge! association.serialize_ids(associated_object, scope)
|
329
|
+
end
|
330
|
+
|
331
|
+
hash
|
332
|
+
end
|
333
|
+
|
334
|
+
# Returns a hash representation of the serializable
|
335
|
+
# object attributes.
|
336
|
+
def attributes
|
337
|
+
hash = {}
|
338
|
+
|
339
|
+
_attributes.each do |name,key|
|
340
|
+
hash[key] = @object.read_attribute_for_serialization(name)
|
341
|
+
end
|
342
|
+
|
343
|
+
hash
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
class Array
|
349
|
+
# Array uses ActiveModel::ArraySerializer.
|
350
|
+
def active_model_serializer
|
351
|
+
ActiveModel::ArraySerializer
|
352
|
+
end
|
353
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_support/core_ext/string/inflections"
|
3
|
+
require "active_model"
|
4
|
+
require "active_model/serializer"
|
5
|
+
|
6
|
+
module ActiveModel::SerializerSupport
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods #:nodoc:
|
10
|
+
if "".respond_to?(:safe_constantize)
|
11
|
+
def active_model_serializer
|
12
|
+
@active_model_serializer ||= "#{self.name}Serializer".safe_constantize
|
13
|
+
end
|
14
|
+
else
|
15
|
+
def active_model_serializer
|
16
|
+
return @active_model_serializer if defined?(@active_model_serializer)
|
17
|
+
|
18
|
+
begin
|
19
|
+
@active_model_serializer = "#{self.name}Serializer".constantize
|
20
|
+
rescue NameError => e
|
21
|
+
raise unless e.message =~ /uninitialized constant/
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a model serializer for this object considering its namespace.
|
28
|
+
def active_model_serializer
|
29
|
+
self.class.active_model_serializer
|
30
|
+
end
|
31
|
+
|
32
|
+
alias :read_attribute_for_serialization :send
|
33
|
+
end
|
34
|
+
|
35
|
+
ActiveSupport.on_load(:active_record) do
|
36
|
+
include ActiveModel::SerializerSupport
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
require 'action_controller'
|
41
|
+
require 'action_controller/serialization'
|
42
|
+
|
43
|
+
ActiveSupport.on_load(:action_controller) do
|
44
|
+
include ::ActionController::Serialization
|
45
|
+
end
|
46
|
+
rescue LoadError => ex
|
47
|
+
# rails on installed, continuing
|
48
|
+
end
|