active_model_serializers 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- attrs[key] = nil
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
- self._root || class_name
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
- return nil if @object.nil?
278
- instrument(:serialize, :serializer => self.class.name) do
279
- @node = attributes
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
- @node
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
- hash = {}
454
+ _fast_attributes
455
+ rescue NameError
456
+ method = "def _fast_attributes\n"
376
457
 
377
- _attributes.each do |name,key|
378
- hash[key] = read_attribute_for_serialization(name) if include?(name)
379
- end
458
+ method << " h = {}\n"
380
459
 
381
- hash
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) == :ids
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) == :objects
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
- # Use pluck or select_columns if available
128
- # return collection.ids if collection.respond_to?(:ids)
129
- ids_key = "#{key.to_s.singularize}_ids"
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?("#{name}_id")
216
- source_serializer.object.send("#{name}_id")
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
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
- VERSION = "0.7.0"
3
+ VERSION = "0.8.0"
4
4
  end
5
5
  end
@@ -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
- require "generators/resource_override"
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 app.routes.url_helpers
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
- ActiveSupport.on_load(:active_record) do
71
- ActiveRecord::Relation.send(:include, ActiveModel::ArraySerializerSupport)
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 -%>
@@ -51,5 +51,4 @@ class ArraySerializerTest < ActiveModel::TestCase
51
51
  { :title => "Post2" }
52
52
  ], serializer.as_json)
53
53
  end
54
-
55
54
  end
@@ -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