active_model_serializers 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|