active_model_serializers 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +3 -3
- data/CHANGELOG.md +61 -0
- data/DESIGN.textile +2 -2
- data/Gemfile +3 -1
- data/README.md +44 -21
- data/Rakefile +5 -0
- data/active_model_serializers.gemspec +2 -1
- data/bench/perf.rb +43 -0
- data/lib/action_controller/serialization.rb +8 -25
- data/lib/active_model/array_serializer.rb +58 -20
- data/lib/active_model/serializer.rb +132 -16
- data/lib/active_model/serializer/associations.rb +17 -10
- data/lib/active_model/serializers/version.rb +1 -1
- data/lib/active_model_serializers.rb +20 -8
- data/lib/active_record/serializer_override.rb +16 -0
- data/lib/generators/serializer/serializer_generator.rb +3 -2
- data/lib/generators/serializer/templates/serializer.rb +11 -0
- data/test/array_serializer_test.rb +0 -1
- data/test/association_test.rb +103 -0
- data/test/caching_test.rb +96 -0
- data/test/generators_test.rb +16 -2
- data/test/no_serialization_scope_test.rb +1 -1
- data/test/serialization_scope_name_test.rb +67 -0
- data/test/serialization_test.rb +1 -1
- data/test/serializer_support_test.rb +10 -0
- data/test/serializer_test.rb +142 -3
- data/test/test_fakes.rb +12 -0
- data/test/test_helper.rb +8 -7
- metadata +25 -4
- data/RELEASE_NOTES.md +0 -15
@@ -66,7 +66,15 @@ module ActiveModel
|
|
66
66
|
self._embed = :objects
|
67
67
|
class_attribute :_root_embed
|
68
68
|
|
69
|
+
class_attribute :cache
|
70
|
+
class_attribute :perform_caching
|
71
|
+
|
69
72
|
class << self
|
73
|
+
# set perform caching like root
|
74
|
+
def cached(value = true)
|
75
|
+
self.perform_caching = value
|
76
|
+
end
|
77
|
+
|
70
78
|
# Define attributes to be used in the serialization.
|
71
79
|
def attributes(*attrs)
|
72
80
|
|
@@ -82,7 +90,9 @@ module ActiveModel
|
|
82
90
|
end
|
83
91
|
|
84
92
|
def attribute(attr, options={})
|
85
|
-
self._attributes = _attributes.merge(attr => options[:key] || attr.to_s.gsub(/\?$/, '').to_sym)
|
93
|
+
self._attributes = _attributes.merge(attr.is_a?(Hash) ? attr : {attr => options[:key] || attr.to_s.gsub(/\?$/, '').to_sym})
|
94
|
+
|
95
|
+
attr = attr.keys[0] if attr.is_a? Hash
|
86
96
|
|
87
97
|
unless method_defined?(attr)
|
88
98
|
define_method attr do
|
@@ -91,6 +101,15 @@ module ActiveModel
|
|
91
101
|
end
|
92
102
|
|
93
103
|
define_include_method attr
|
104
|
+
|
105
|
+
# protect inheritance chains and open classes
|
106
|
+
# if a serializer inherits from another OR
|
107
|
+
# attributes are added later in a classes lifecycle
|
108
|
+
# poison the cache
|
109
|
+
define_method :_fast_attributes do
|
110
|
+
raise NameError
|
111
|
+
end
|
112
|
+
|
94
113
|
end
|
95
114
|
|
96
115
|
def associate(klass, attrs) #:nodoc:
|
@@ -184,8 +203,12 @@ module ActiveModel
|
|
184
203
|
attrs[key] = column.type
|
185
204
|
else
|
186
205
|
# Computed attribute (method on serializer or model). We cannot
|
187
|
-
# infer the type, so we put nil
|
188
|
-
|
206
|
+
# infer the type, so we put nil, unless specified in the attribute declaration
|
207
|
+
if name != key
|
208
|
+
attrs[name] = key
|
209
|
+
else
|
210
|
+
attrs[key] = nil
|
211
|
+
end
|
189
212
|
end
|
190
213
|
end
|
191
214
|
|
@@ -229,19 +252,64 @@ module ActiveModel
|
|
229
252
|
self._root = name
|
230
253
|
end
|
231
254
|
alias_method :root=, :root
|
255
|
+
|
256
|
+
# Used internally to create a new serializer object based on controller
|
257
|
+
# settings and options for a given resource. These settings are typically
|
258
|
+
# set during the request lifecycle or by the controller class, and should
|
259
|
+
# not be manually defined for this method.
|
260
|
+
def build_json(controller, resource, options)
|
261
|
+
default_options = controller.send(:default_serializer_options) || {}
|
262
|
+
options = default_options.merge(options || {})
|
263
|
+
|
264
|
+
serializer = options.delete(:serializer) ||
|
265
|
+
(resource.respond_to?(:active_model_serializer) &&
|
266
|
+
resource.active_model_serializer)
|
267
|
+
|
268
|
+
return serializer unless serializer
|
269
|
+
|
270
|
+
if resource.respond_to?(:to_ary)
|
271
|
+
unless serializer <= ActiveModel::ArraySerializer
|
272
|
+
raise ArgumentError.new("#{serializer.name} is not an ArraySerializer. " +
|
273
|
+
"You may want to use the :each_serializer option instead.")
|
274
|
+
end
|
275
|
+
|
276
|
+
if options[:root] != false && serializer.root != false
|
277
|
+
# the serializer for an Array is ActiveModel::ArraySerializer
|
278
|
+
options[:root] ||= serializer.root || controller.controller_name
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
options[:scope] = controller.serialization_scope unless options.has_key?(:scope)
|
283
|
+
options[:scope_name] = controller._serialization_scope
|
284
|
+
options[:url_options] = controller.url_options
|
285
|
+
|
286
|
+
serializer.new(resource, options)
|
287
|
+
end
|
232
288
|
end
|
233
289
|
|
234
290
|
attr_reader :object, :options
|
235
291
|
|
236
292
|
def initialize(object, options={})
|
237
293
|
@object, @options = object, options
|
294
|
+
|
295
|
+
scope_name = @options[:scope_name]
|
296
|
+
if scope_name && !respond_to?(scope_name)
|
297
|
+
self.class.class_eval do
|
298
|
+
define_method scope_name, lambda { scope }
|
299
|
+
end
|
300
|
+
end
|
238
301
|
end
|
239
302
|
|
240
303
|
def root_name
|
241
304
|
return false if self._root == false
|
242
|
-
|
305
|
+
|
243
306
|
class_name = self.class.name.demodulize.underscore.sub(/_serializer$/, '').to_sym unless self.class.name.blank?
|
244
|
-
|
307
|
+
|
308
|
+
if self._root == true
|
309
|
+
class_name
|
310
|
+
else
|
311
|
+
self._root || class_name
|
312
|
+
end
|
245
313
|
end
|
246
314
|
|
247
315
|
def url_options
|
@@ -256,6 +324,16 @@ module ActiveModel
|
|
256
324
|
hash[meta_key] = @options[:meta] if @options.has_key?(:meta)
|
257
325
|
end
|
258
326
|
|
327
|
+
def to_json(*args)
|
328
|
+
if perform_caching?
|
329
|
+
cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'to-json']) do
|
330
|
+
super
|
331
|
+
end
|
332
|
+
else
|
333
|
+
super
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
259
337
|
# Returns a json representation of the serializable
|
260
338
|
# object including the root.
|
261
339
|
def as_json(options={})
|
@@ -274,13 +352,12 @@ module ActiveModel
|
|
274
352
|
# Returns a hash representation of the serializable
|
275
353
|
# object without the root.
|
276
354
|
def serializable_hash
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
instrument :associations do
|
281
|
-
include_associations! if _embed
|
355
|
+
if perform_caching?
|
356
|
+
cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'serializable-hash']) do
|
357
|
+
_serializable_hash
|
282
358
|
end
|
283
|
-
|
359
|
+
else
|
360
|
+
_serializable_hash
|
284
361
|
end
|
285
362
|
end
|
286
363
|
|
@@ -291,6 +368,8 @@ module ActiveModel
|
|
291
368
|
end
|
292
369
|
|
293
370
|
def include?(name)
|
371
|
+
return false if options.key?(:only) && !Array(options[:only]).include?(name)
|
372
|
+
return false if options.key?(:except) && Array(options[:except]).include?(name)
|
294
373
|
send INCLUDE_METHODS[name]
|
295
374
|
end
|
296
375
|
|
@@ -372,13 +451,19 @@ module ActiveModel
|
|
372
451
|
# Returns a hash representation of the serializable
|
373
452
|
# object attributes.
|
374
453
|
def attributes
|
375
|
-
|
454
|
+
_fast_attributes
|
455
|
+
rescue NameError
|
456
|
+
method = "def _fast_attributes\n"
|
376
457
|
|
377
|
-
|
378
|
-
hash[key] = read_attribute_for_serialization(name) if include?(name)
|
379
|
-
end
|
458
|
+
method << " h = {}\n"
|
380
459
|
|
381
|
-
|
460
|
+
_attributes.each do |name,key|
|
461
|
+
method << " h[:\"#{key}\"] = read_attribute_for_serialization(:\"#{name}\") if include?(:\"#{name}\")\n"
|
462
|
+
end
|
463
|
+
method << " h\nend"
|
464
|
+
|
465
|
+
self.class.class_eval method
|
466
|
+
_fast_attributes
|
382
467
|
end
|
383
468
|
|
384
469
|
# Returns options[:scope]
|
@@ -388,6 +473,21 @@ module ActiveModel
|
|
388
473
|
|
389
474
|
alias :read_attribute_for_serialization :send
|
390
475
|
|
476
|
+
def _serializable_hash
|
477
|
+
return nil if @object.nil?
|
478
|
+
@node = attributes
|
479
|
+
include_associations! if _embed
|
480
|
+
@node
|
481
|
+
end
|
482
|
+
|
483
|
+
def perform_caching?
|
484
|
+
perform_caching && cache && respond_to?(:cache_key)
|
485
|
+
end
|
486
|
+
|
487
|
+
def expand_cache_key(*args)
|
488
|
+
ActiveSupport::Cache.expand_cache_key(args)
|
489
|
+
end
|
490
|
+
|
391
491
|
# Use ActiveSupport::Notifications to send events to external systems.
|
392
492
|
# The event name is: name.class_name.serializer
|
393
493
|
def instrument(name, payload = {}, &block)
|
@@ -395,4 +495,20 @@ module ActiveModel
|
|
395
495
|
ActiveSupport::Notifications.instrument(event_name, payload, &block)
|
396
496
|
end
|
397
497
|
end
|
498
|
+
|
499
|
+
# DefaultSerializer
|
500
|
+
#
|
501
|
+
# Provides a constant interface for all items, particularly
|
502
|
+
# for ArraySerializer.
|
503
|
+
class DefaultSerializer
|
504
|
+
attr_reader :object, :options
|
505
|
+
|
506
|
+
def initialize(object, options={})
|
507
|
+
@object, @options = object, options
|
508
|
+
end
|
509
|
+
|
510
|
+
def serializable_hash
|
511
|
+
@object.as_json(@options)
|
512
|
+
end
|
513
|
+
end
|
398
514
|
end
|
@@ -17,6 +17,14 @@ module ActiveModel
|
|
17
17
|
end
|
18
18
|
|
19
19
|
self.options = class_options
|
20
|
+
|
21
|
+
# cache the root so we can reuse it without falling back on a per-instance basis
|
22
|
+
begin
|
23
|
+
self.options[:root] ||= self.new(name, nil).root
|
24
|
+
rescue
|
25
|
+
# this could fail if it needs a valid source, for example a polymorphic association
|
26
|
+
end
|
27
|
+
|
20
28
|
end
|
21
29
|
end
|
22
30
|
|
@@ -64,11 +72,11 @@ module ActiveModel
|
|
64
72
|
end
|
65
73
|
|
66
74
|
def embed_ids?
|
67
|
-
option(:embed, source_serializer._embed)
|
75
|
+
[:id, :ids].include? option(:embed, source_serializer._embed)
|
68
76
|
end
|
69
77
|
|
70
78
|
def embed_objects?
|
71
|
-
option(:embed, source_serializer._embed)
|
79
|
+
[:object, :objects].include? option(:embed, source_serializer._embed)
|
72
80
|
end
|
73
81
|
|
74
82
|
def embed_in_root?
|
@@ -124,12 +132,9 @@ module ActiveModel
|
|
124
132
|
end
|
125
133
|
|
126
134
|
def serialize_ids
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
if !option(:include) && !option(:embed_key) && source_serializer.object.respond_to?(ids_key)
|
132
|
-
source_serializer.object.send(ids_key)
|
135
|
+
ids_key = "#{@name.to_s.singularize}_ids".to_sym
|
136
|
+
if !option(:embed_key) && !source_serializer.respond_to?(@name.to_s) && source_serializer.object.respond_to?(ids_key)
|
137
|
+
source_serializer.object.read_attribute_for_serialization(ids_key)
|
133
138
|
else
|
134
139
|
associated_object.map do |item|
|
135
140
|
item.read_attribute_for_serialization(embed_key)
|
@@ -203,6 +208,8 @@ module ActiveModel
|
|
203
208
|
end
|
204
209
|
|
205
210
|
def serialize_ids
|
211
|
+
id_key = "#{@name}_id".to_sym
|
212
|
+
|
206
213
|
if polymorphic?
|
207
214
|
if associated_object
|
208
215
|
{
|
@@ -212,8 +219,8 @@ module ActiveModel
|
|
212
219
|
else
|
213
220
|
nil
|
214
221
|
end
|
215
|
-
elsif !option(:embed_key) && source_serializer.object.respond_to?(
|
216
|
-
source_serializer.object.
|
222
|
+
elsif !option(:embed_key) && !source_serializer.respond_to?(@name.to_s) && source_serializer.object.respond_to?(id_key)
|
223
|
+
source_serializer.object.read_attribute_for_serialization(id_key)
|
217
224
|
elsif associated_object
|
218
225
|
associated_object.read_attribute_for_serialization(embed_key)
|
219
226
|
else
|
@@ -15,14 +15,24 @@ if defined?(Rails)
|
|
15
15
|
|
16
16
|
Rails::Generators.configure!(app.config.generators)
|
17
17
|
Rails::Generators.hidden_namespaces.uniq!
|
18
|
-
|
18
|
+
require_relative "generators/resource_override"
|
19
19
|
end
|
20
20
|
|
21
21
|
initializer "include_routes.active_model_serializer" do |app|
|
22
22
|
ActiveSupport.on_load(:active_model_serializers) do
|
23
|
-
include
|
23
|
+
include AbstractController::UrlFor
|
24
|
+
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
|
25
|
+
include app.routes.mounted_helpers
|
24
26
|
end
|
25
27
|
end
|
28
|
+
|
29
|
+
initializer "caching.active_model_serializer" do |app|
|
30
|
+
ActiveModel::Serializer.perform_caching = app.config.action_controller.perform_caching
|
31
|
+
ActiveModel::ArraySerializer.perform_caching = app.config.action_controller.perform_caching
|
32
|
+
|
33
|
+
ActiveModel::Serializer.cache = Rails.cache
|
34
|
+
ActiveModel::ArraySerializer.cache = Rails.cache
|
35
|
+
end
|
26
36
|
end
|
27
37
|
end
|
28
38
|
end
|
@@ -54,10 +64,6 @@ module ActiveModel::SerializerSupport
|
|
54
64
|
alias :read_attribute_for_serialization :send
|
55
65
|
end
|
56
66
|
|
57
|
-
ActiveSupport.on_load(:active_record) do
|
58
|
-
include ActiveModel::SerializerSupport
|
59
|
-
end
|
60
|
-
|
61
67
|
module ActiveModel::ArraySerializerSupport
|
62
68
|
def active_model_serializer
|
63
69
|
ActiveModel::ArraySerializer
|
@@ -67,8 +73,14 @@ end
|
|
67
73
|
Array.send(:include, ActiveModel::ArraySerializerSupport)
|
68
74
|
Set.send(:include, ActiveModel::ArraySerializerSupport)
|
69
75
|
|
70
|
-
|
71
|
-
ActiveRecord::Relation
|
76
|
+
{
|
77
|
+
:active_record => 'ActiveRecord::Relation',
|
78
|
+
:mongoid => 'Mongoid::Criteria'
|
79
|
+
}.each do |orm, rel_class|
|
80
|
+
ActiveSupport.on_load(orm) do
|
81
|
+
include ActiveModel::SerializerSupport
|
82
|
+
rel_class.constantize.send(:include, ActiveModel::ArraySerializerSupport)
|
83
|
+
end
|
72
84
|
end
|
73
85
|
|
74
86
|
begin
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# We do not recommend that you use AM::S in this way, but if you must, here
|
2
|
+
# is a mixin that overrides ActiveRecord::Base#to_json and #as_json.
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
module SerializerOverride
|
6
|
+
def to_json options = {}
|
7
|
+
active_model_serializer.new(self).to_json options
|
8
|
+
end
|
9
|
+
|
10
|
+
def as_json options={}
|
11
|
+
active_model_serializer.new(self).as_json options
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Base.send(:include, SerializerOverride)
|
16
|
+
end
|
@@ -12,9 +12,10 @@ module Rails
|
|
12
12
|
template 'serializer.rb', File.join('app/serializers', class_path, "#{file_name}_serializer.rb")
|
13
13
|
end
|
14
14
|
|
15
|
-
# hook_for :test_framework
|
16
|
-
|
17
15
|
private
|
16
|
+
def generate_id_method
|
17
|
+
RUBY_VERSION =~ /1\.8/
|
18
|
+
end
|
18
19
|
|
19
20
|
def attributes_names
|
20
21
|
[:id] + attributes.select { |attr| !attr.reference? }.map { |a| a.name.to_sym }
|
@@ -4,5 +4,16 @@ class <%= class_name %>Serializer < <%= parent_class_name %>
|
|
4
4
|
<% association_names.each do |attribute| -%>
|
5
5
|
has_one :<%= attribute %>
|
6
6
|
<% end -%>
|
7
|
+
<% if generate_id_method %>
|
8
|
+
|
9
|
+
# due to the difference between 1.8 and 1.9 with respect to #id and
|
10
|
+
# #object_id, we recommend that if you wish to serialize id columns, you
|
11
|
+
# do this. Feel free to remove this if you don't feel that it's appropriate.
|
12
|
+
#
|
13
|
+
# For more: https://github.com/rails-api/active_model_serializers/issues/127
|
14
|
+
def id
|
15
|
+
object.read_attribute_for_serialization(:id)
|
16
|
+
end
|
17
|
+
<% end -%>
|
7
18
|
end
|
8
19
|
<% end -%>
|
data/test/association_test.rb
CHANGED
@@ -379,6 +379,109 @@ class AssociationTest < ActiveModel::TestCase
|
|
379
379
|
|
380
380
|
assert_equal 1, serialized_times
|
381
381
|
end
|
382
|
+
|
383
|
+
def test_include_with_read_association_id_for_serialization_hook
|
384
|
+
@post_serializer_class.class_eval do
|
385
|
+
has_one :comment, :embed => :ids, :include => true
|
386
|
+
end
|
387
|
+
|
388
|
+
association_name = nil
|
389
|
+
@post.class_eval do
|
390
|
+
define_method :read_attribute_for_serialization, lambda { |name|
|
391
|
+
association_name = name
|
392
|
+
send(name)
|
393
|
+
}
|
394
|
+
define_method :comment_id, lambda {
|
395
|
+
@attributes[:comment].id
|
396
|
+
}
|
397
|
+
end
|
398
|
+
|
399
|
+
include_bare! :comment
|
400
|
+
|
401
|
+
assert_equal({
|
402
|
+
:comment_id => 1
|
403
|
+
}, @hash)
|
404
|
+
end
|
405
|
+
|
406
|
+
def test_include_with_read_association_ids_for_serialization_hook
|
407
|
+
@post_serializer_class.class_eval do
|
408
|
+
has_many :comments, :embed => :ids, :include => false
|
409
|
+
end
|
410
|
+
|
411
|
+
association_name = nil
|
412
|
+
@post.class_eval do
|
413
|
+
define_method :read_attribute_for_serialization, lambda { |name|
|
414
|
+
association_name = name
|
415
|
+
send(name)
|
416
|
+
}
|
417
|
+
define_method :comment_ids, lambda {
|
418
|
+
@attributes[:comments].map(&:id)
|
419
|
+
}
|
420
|
+
end
|
421
|
+
|
422
|
+
include_bare! :comments
|
423
|
+
|
424
|
+
assert_equal({
|
425
|
+
:comment_ids => [1]
|
426
|
+
}, @hash)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
class RecursiveTest < AssociationTest
|
431
|
+
class BarSerializer < ActiveModel::Serializer; end
|
432
|
+
|
433
|
+
class FooSerializer < ActiveModel::Serializer
|
434
|
+
root :foos
|
435
|
+
attributes :id
|
436
|
+
has_many :bars, :serializer => BarSerializer, :root => :bars, :embed => :ids, :include => true
|
437
|
+
end
|
438
|
+
|
439
|
+
class BarSerializer < ActiveModel::Serializer
|
440
|
+
root :bars
|
441
|
+
attributes :id
|
442
|
+
has_many :foos, :serializer => FooSerializer, :root => :foos, :embed => :ids, :include => true
|
443
|
+
end
|
444
|
+
|
445
|
+
class Foo < Model
|
446
|
+
def active_model_serializer; FooSerializer; end
|
447
|
+
end
|
448
|
+
|
449
|
+
class Bar < Model
|
450
|
+
def active_model_serializer; BarSerializer; end
|
451
|
+
end
|
452
|
+
|
453
|
+
def setup
|
454
|
+
super
|
455
|
+
|
456
|
+
foo = Foo.new(:id => 1)
|
457
|
+
bar = Bar.new(:id => 2)
|
458
|
+
|
459
|
+
foo.bars = [ bar ]
|
460
|
+
bar.foos = [ foo ]
|
461
|
+
|
462
|
+
collection = [ foo ]
|
463
|
+
|
464
|
+
@serializer = collection.active_model_serializer.new(collection, :root => :foos)
|
465
|
+
end
|
466
|
+
|
467
|
+
def test_mutual_relation_result
|
468
|
+
assert_equal({
|
469
|
+
:foos => [{
|
470
|
+
:bar_ids => [ 2 ],
|
471
|
+
:id => 1
|
472
|
+
}],
|
473
|
+
:bars => [{
|
474
|
+
:foo_ids => [ 1 ],
|
475
|
+
:id => 2
|
476
|
+
}]
|
477
|
+
}, @serializer.as_json)
|
478
|
+
end
|
479
|
+
|
480
|
+
def test_mutual_relation_does_not_raise_error
|
481
|
+
assert_nothing_raised SystemStackError, 'stack level too deep' do
|
482
|
+
@serializer.as_json
|
483
|
+
end
|
484
|
+
end
|
382
485
|
end
|
383
486
|
|
384
487
|
class InclusionTest < AssociationTest
|