active_model_serializers 0.9.0 → 0.9.8

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.
Files changed (61) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +113 -0
  3. data/README.md +112 -19
  4. data/lib/action_controller/serialization.rb +35 -8
  5. data/lib/action_controller/serialization_test_case.rb +4 -4
  6. data/lib/active_model/array_serializer.rb +13 -10
  7. data/lib/active_model/default_serializer.rb +2 -6
  8. data/lib/active_model/serializable.rb +30 -11
  9. data/lib/active_model/serializable/utils.rb +16 -0
  10. data/lib/active_model/serializer.rb +90 -40
  11. data/lib/active_model/serializer/{associations.rb → association.rb} +8 -52
  12. data/lib/active_model/serializer/association/has_many.rb +39 -0
  13. data/lib/active_model/serializer/association/has_one.rb +25 -0
  14. data/lib/active_model/serializer/generators/serializer/scaffold_controller_generator.rb +1 -1
  15. data/lib/active_model/serializer/railtie.rb +12 -0
  16. data/lib/active_model/serializer/version.rb +1 -1
  17. data/lib/active_model_serializers.rb +1 -1
  18. data/lib/active_model_serializers/model/caching.rb +25 -0
  19. data/test/benchmark/app.rb +60 -0
  20. data/test/benchmark/benchmarking_support.rb +67 -0
  21. data/test/benchmark/bm_active_record.rb +41 -0
  22. data/test/benchmark/setup.rb +75 -0
  23. data/test/benchmark/tmp/miniprofiler/mp_timers_6eqewtfgrhitvq5gqm25 +0 -0
  24. data/test/benchmark/tmp/miniprofiler/mp_timers_8083sx03hu72pxz1a4d0 +0 -0
  25. data/test/benchmark/tmp/miniprofiler/mp_timers_fyz2gsml4z0ph9kpoy1c +0 -0
  26. data/test/benchmark/tmp/miniprofiler/mp_timers_hjry5rc32imd42oxoi48 +0 -0
  27. data/test/benchmark/tmp/miniprofiler/mp_timers_m8fpoz2cvt3g9agz0bs3 +0 -0
  28. data/test/benchmark/tmp/miniprofiler/mp_timers_p92m2drnj1i568u3sta0 +0 -0
  29. data/test/benchmark/tmp/miniprofiler/mp_timers_qg52tpca3uesdfguee9i +0 -0
  30. data/test/benchmark/tmp/miniprofiler/mp_timers_s15t1a6mvxe0z7vjv790 +0 -0
  31. data/test/benchmark/tmp/miniprofiler/mp_timers_x8kal3d17nfds6vp4kcj +0 -0
  32. data/test/benchmark/tmp/miniprofiler/mp_views_127.0.0.1 +0 -0
  33. data/test/fixtures/active_record.rb +4 -0
  34. data/test/fixtures/poro.rb +149 -1
  35. data/test/fixtures/template.html.erb +1 -0
  36. data/test/integration/action_controller/namespaced_serialization_test.rb +105 -0
  37. data/test/integration/action_controller/serialization_test.rb +5 -5
  38. data/test/integration/action_controller/serialization_test_case_test.rb +10 -0
  39. data/test/integration/active_record/active_record_test.rb +19 -2
  40. data/test/test_app.rb +3 -0
  41. data/test/tmp/app/assets/javascripts/accounts.js +2 -0
  42. data/test/tmp/app/assets/stylesheets/accounts.css +4 -0
  43. data/test/tmp/app/controllers/accounts_controller.rb +2 -0
  44. data/test/tmp/app/helpers/accounts_helper.rb +2 -0
  45. data/test/tmp/app/serializers/account_serializer.rb +3 -0
  46. data/test/tmp/config/routes.rb +1 -0
  47. data/test/unit/active_model/array_serializer/options_test.rb +16 -0
  48. data/test/unit/active_model/array_serializer/serialization_test.rb +18 -1
  49. data/test/unit/active_model/serializer/associations/build_serializer_test.rb +15 -0
  50. data/test/unit/active_model/serializer/associations_test.rb +30 -0
  51. data/test/unit/active_model/serializer/attributes_test.rb +16 -0
  52. data/test/unit/active_model/serializer/config_test.rb +3 -0
  53. data/test/unit/active_model/serializer/has_many_polymorphic_test.rb +189 -0
  54. data/test/unit/active_model/serializer/has_many_test.rb +52 -17
  55. data/test/unit/active_model/serializer/has_one_and_has_many_test.rb +27 -0
  56. data/test/unit/active_model/serializer/has_one_polymorphic_test.rb +196 -0
  57. data/test/unit/active_model/serializer/has_one_test.rb +46 -0
  58. data/test/unit/active_model/serializer/options_test.rb +27 -0
  59. data/test/unit/active_model/serializer/url_helpers_test.rb +35 -0
  60. data/test/unit/active_model/serilizable_test.rb +50 -0
  61. metadata +98 -25
@@ -15,15 +15,18 @@ module ActiveModel
15
15
  @object = object
16
16
  @scope = options[:scope]
17
17
  @root = options.fetch(:root, self.class._root)
18
+ @polymorphic = options.fetch(:polymorphic, false)
18
19
  @meta_key = options[:meta_key] || :meta
19
20
  @meta = options[@meta_key]
20
21
  @each_serializer = options[:each_serializer]
21
22
  @resource_name = options[:resource_name]
22
23
  @only = options[:only] ? Array(options[:only]) : nil
23
24
  @except = options[:except] ? Array(options[:except]) : nil
25
+ @context = options[:context]
26
+ @namespace = options[:namespace]
24
27
  @key_format = options[:key_format] || options[:each_serializer].try(:key_format)
25
28
  end
26
- attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format
29
+ attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format, :context
27
30
 
28
31
  def json_key
29
32
  key = root.nil? ? @resource_name : root
@@ -32,13 +35,13 @@ module ActiveModel
32
35
  end
33
36
 
34
37
  def serializer_for(item)
35
- serializer_class = @each_serializer || Serializer.serializer_for(item) || DefaultSerializer
36
- serializer_class.new(item, scope: scope, key_format: key_format, only: @only, except: @except)
38
+ serializer_class = @each_serializer || Serializer.serializer_for(item, namespace: @namespace) || DefaultSerializer
39
+ serializer_class.new(item, scope: scope, key_format: key_format, context: @context, only: @only, except: @except, polymorphic: @polymorphic, namespace: @namespace)
37
40
  end
38
41
 
39
- def serializable_object
42
+ def serializable_object(options={})
40
43
  @object.map do |item|
41
- serializer_for(item).serializable_object
44
+ serializer_for(item).serializable_object_with_notification(options)
42
45
  end
43
46
  end
44
47
  alias_method :serializable_array, :serializable_object
@@ -49,7 +52,11 @@ module ActiveModel
49
52
  next if !objects || objects.flatten.empty?
50
53
 
51
54
  if hash.has_key?(type)
52
- hash[type].concat(objects).uniq!
55
+ case hash[type] when Hash
56
+ hash[type].deep_merge!(objects){ |key, old, new| (Array(old) + Array(new)).uniq }
57
+ else
58
+ hash[type].concat(objects).uniq!
59
+ end
53
60
  else
54
61
  hash[type] = objects
55
62
  end
@@ -57,9 +64,5 @@ module ActiveModel
57
64
  end
58
65
  end
59
66
 
60
- private
61
- def instrumentation_keys
62
- [:object, :scope, :root, :meta_key, :meta, :each_serializer, :resource_name, :key_format]
63
- end
64
67
  end
65
68
  end
@@ -15,18 +15,14 @@ module ActiveModel
15
15
  end
16
16
 
17
17
  def as_json(options={})
18
- instrument('!serialize') do
18
+ instrument do
19
19
  return [] if @object.nil? && @wrap_in_array
20
20
  hash = @object.as_json
21
21
  @wrap_in_array ? [hash] : hash
22
22
  end
23
23
  end
24
+
24
25
  alias serializable_hash as_json
25
26
  alias serializable_object as_json
26
-
27
- private
28
- def instrumentation_keys
29
- [:object, :wrap_in_array]
30
- end
31
27
  end
32
28
  end
@@ -1,17 +1,29 @@
1
+ require 'active_model/serializable/utils'
2
+
1
3
  module ActiveModel
2
4
  module Serializable
5
+ INSTRUMENTATION_KEY = '!serialize.active_model_serializers'.freeze
6
+
7
+ def self.included(base)
8
+ base.extend Utils
9
+ end
10
+
3
11
  def as_json(options={})
4
- instrument('!serialize') do
12
+ instrument do
5
13
  if root = options.fetch(:root, json_key)
6
- hash = { root => serializable_object }
14
+ hash = { root => serializable_object(options) }
7
15
  hash.merge!(serializable_data)
8
16
  hash
9
17
  else
10
- serializable_object
18
+ serializable_object(options)
11
19
  end
12
20
  end
13
21
  end
14
22
 
23
+ def serializable_object_with_notification(options={})
24
+ instrument { serializable_object(options) }
25
+ end
26
+
15
27
  def serializable_data
16
28
  embedded_in_root_associations.tap do |hash|
17
29
  if respond_to?(:meta) && meta
@@ -20,21 +32,28 @@ module ActiveModel
20
32
  end
21
33
  end
22
34
 
35
+ def namespace
36
+ if module_name = get_namespace
37
+ Serializer.serializers_cache.fetch_or_store(module_name) do
38
+ Utils._const_get(module_name)
39
+ end
40
+ end
41
+ end
42
+
23
43
  def embedded_in_root_associations
24
44
  {}
25
45
  end
26
46
 
27
47
  private
28
- def instrument(action, &block)
29
- payload = instrumentation_keys.inject({ serializer: self.class.name }) do |payload, key|
30
- payload[:payload] = self.instance_variable_get(:"@#{key}")
31
- payload
32
- end
33
- ActiveSupport::Notifications.instrument("#{action}.active_model_serializers", payload, &block)
48
+
49
+ def get_namespace
50
+ modules = self.class.name.split('::')
51
+ modules[0..-2].join('::') if modules.size > 1
34
52
  end
35
53
 
36
- def instrumentation_keys
37
- [:object, :scope, :root, :meta_key, :meta, :wrap_in_array, :only, :except, :key_format]
54
+ def instrument(&block)
55
+ payload = { serializer: self.class.name }
56
+ ActiveSupport::Notifications.instrument(INSTRUMENTATION_KEY, payload, &block)
38
57
  end
39
58
  end
40
59
  end
@@ -0,0 +1,16 @@
1
+ module ActiveModel
2
+ module Serializable
3
+ module Utils
4
+ extend self
5
+
6
+ def _const_get(const)
7
+ begin
8
+ method = RUBY_VERSION >= '2.0' ? :const_get : :qualified_const_get
9
+ Object.send method, const
10
+ rescue NameError
11
+ const.safe_constantize
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,9 +1,10 @@
1
1
  require 'active_model/array_serializer'
2
2
  require 'active_model/serializable'
3
- require 'active_model/serializer/associations'
3
+ require 'active_model/serializer/association'
4
4
  require 'active_model/serializer/config'
5
5
 
6
6
  require 'thread'
7
+ require 'concurrent/map'
7
8
 
8
9
  module ActiveModel
9
10
  class Serializer
@@ -55,32 +56,19 @@ end
55
56
  end
56
57
  attr_reader :key_format
57
58
 
58
- if RUBY_VERSION >= '2.0'
59
- def serializer_for(resource)
60
- if resource.respond_to?(:to_ary)
61
- if Object.constants.include?(:ArraySerializer)
62
- ::ArraySerializer
63
- else
64
- ArraySerializer
65
- end
59
+ def serializer_for(resource, options = {})
60
+ if resource.respond_to?(:serializer_class)
61
+ resource.serializer_class
62
+ elsif resource.respond_to?(:to_ary)
63
+ if Object.constants.include?(:ArraySerializer)
64
+ ::ArraySerializer
66
65
  else
67
- begin
68
- Object.const_get "#{resource.class.name}Serializer"
69
- rescue NameError
70
- nil
71
- end
66
+ ArraySerializer
72
67
  end
73
- end
74
- else
75
- def serializer_for(resource)
76
- if resource.respond_to?(:to_ary)
77
- if Object.constants.include?(:ArraySerializer)
78
- ::ArraySerializer
79
- else
80
- ArraySerializer
81
- end
82
- else
83
- "#{resource.class.name}Serializer".safe_constantize
68
+ else
69
+ klass_name = build_serializer_class(resource, options)
70
+ Serializer.serializers_cache.fetch_or_store(klass_name) do
71
+ _const_get(klass_name)
84
72
  end
85
73
  end
86
74
  end
@@ -90,14 +78,19 @@ end
90
78
  alias root= _root=
91
79
 
92
80
  def root_name
93
- name.demodulize.underscore.sub(/_serializer$/, '') if name
81
+ if name
82
+ root_name = name.demodulize.underscore.sub(/_serializer$/, '')
83
+ CONFIG.plural_default_root ? root_name.pluralize : root_name
84
+ end
94
85
  end
95
86
 
96
87
  def attributes(*attrs)
97
- @_attributes.concat attrs
98
-
99
88
  attrs.each do |attr|
100
- define_method attr do
89
+ striped_attr = strip_attribute attr
90
+
91
+ @_attributes << striped_attr
92
+
93
+ define_method striped_attr do
101
94
  object.read_attribute_for_serialization attr
102
95
  end unless method_defined?(attr)
103
96
  end
@@ -111,8 +104,28 @@ end
111
104
  associate(Association::HasMany, *attrs)
112
105
  end
113
106
 
107
+ def serializers_cache
108
+ @serializers_cache ||= Concurrent::Map.new
109
+ end
110
+
114
111
  private
115
112
 
113
+ def strip_attribute(attr)
114
+ symbolized = attr.is_a?(Symbol)
115
+
116
+ attr = attr.to_s.gsub(/\?\Z/, '')
117
+ attr = attr.to_sym if symbolized
118
+ attr
119
+ end
120
+
121
+ def build_serializer_class(resource, options)
122
+ "".tap do |klass_name|
123
+ klass_name << "#{options[:namespace]}::" if options[:namespace]
124
+ klass_name << options[:prefix].to_s.classify if options[:prefix]
125
+ klass_name << "#{resource.class.name}Serializer"
126
+ end
127
+ end
128
+
116
129
  def associate(klass, *attrs)
117
130
  options = attrs.extract_options!
118
131
 
@@ -130,6 +143,7 @@ end
130
143
  @object = object
131
144
  @scope = options[:scope]
132
145
  @root = options.fetch(:root, self.class._root)
146
+ @polymorphic = options.fetch(:polymorphic, false)
133
147
  @meta_key = options[:meta_key] || :meta
134
148
  @meta = options[@meta_key]
135
149
  @wrap_in_array = options[:_wrap_in_array]
@@ -137,8 +151,9 @@ end
137
151
  @except = options[:except] ? Array(options[:except]) : nil
138
152
  @key_format = options[:key_format]
139
153
  @context = options[:context]
154
+ @namespace = options[:namespace]
140
155
  end
141
- attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format, :context
156
+ attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format, :context, :polymorphic
142
157
 
143
158
  def json_key
144
159
  key = if root == true || root.nil?
@@ -156,7 +171,7 @@ end
156
171
  end
157
172
  end
158
173
 
159
- def associations
174
+ def associations(options={})
160
175
  associations = self.class._associations
161
176
  included_associations = filter(associations.keys)
162
177
  associations.each_with_object({}) do |(name, association), hash|
@@ -173,7 +188,7 @@ end
173
188
  if association.embed_namespace?
174
189
  hash = hash[association.embed_namespace] ||= {}
175
190
  end
176
- hash[association.embedded_key] = serialize association
191
+ hash[association.embedded_key] = serialize association, options
177
192
  end
178
193
  end
179
194
  end
@@ -194,12 +209,21 @@ end
194
209
  included_associations = filter(associations.keys)
195
210
  associations.each_with_object({}) do |(name, association), hash|
196
211
  if included_associations.include? name
212
+ association_serializer = build_serializer(association)
213
+ # we must do this always because even if the current association is not
214
+ # embedded in root, it might have its own associations that are embedded in root
215
+ hash.merge!(association_serializer.embedded_in_root_associations) do |key, oldval, newval|
216
+ if oldval.respond_to?(:to_ary)
217
+ [oldval, newval].flatten.uniq
218
+ else
219
+ oldval.merge(newval) { |_, oldval, newval| [oldval, newval].flatten.uniq }
220
+ end
221
+ end
222
+
197
223
  if association.embed_in_root?
198
224
  if association.embed_in_root_key?
199
225
  hash = hash[association.embed_in_root_key] ||= {}
200
226
  end
201
- association_serializer = build_serializer(association)
202
- hash.merge!(association_serializer.embedded_in_root_associations) {|key, oldval, newval| [newval, oldval].flatten }
203
227
 
204
228
  serialized_data = association_serializer.serializable_object
205
229
  key = association.root_key
@@ -215,19 +239,29 @@ end
215
239
 
216
240
  def build_serializer(association)
217
241
  object = send(association.name)
218
- association.build_serializer(object, scope: scope)
242
+ association.build_serializer(object, association_options_for_serializer(association))
243
+ end
244
+
245
+ def association_options_for_serializer(association)
246
+ prefix = association.options[:prefix]
247
+ namespace = association.options[:namespace] || @namespace || self.namespace
248
+
249
+ { scope: scope }.tap do |opts|
250
+ opts[:namespace] = namespace if namespace
251
+ opts[:prefix] = prefix if prefix
252
+ end
219
253
  end
220
254
 
221
- def serialize(association)
222
- build_serializer(association).serializable_object
255
+ def serialize(association,options={})
256
+ build_serializer(association).serializable_object(options)
223
257
  end
224
258
 
225
259
  def serialize_ids(association)
226
260
  associated_data = send(association.name)
227
261
  if associated_data.respond_to?(:to_ary)
228
- associated_data.map { |elem| elem.read_attribute_for_serialization(association.embed_key) }
262
+ associated_data.map { |elem| serialize_id(elem, association) }
229
263
  else
230
- associated_data.read_attribute_for_serialization(association.embed_key) if associated_data
264
+ serialize_id(associated_data, association) if associated_data
231
265
  end
232
266
  end
233
267
 
@@ -255,14 +289,30 @@ end
255
289
  end]
256
290
  end
257
291
 
292
+ attr_writer :serialization_options
293
+ def serialization_options
294
+ @serialization_options || {}
295
+ end
296
+
258
297
  def serializable_object(options={})
298
+ self.serialization_options = options
259
299
  return @wrap_in_array ? [] : nil if @object.nil?
260
300
  hash = attributes
261
- hash.merge! associations
301
+ hash.merge! associations(options)
262
302
  hash = convert_keys(hash) if key_format.present?
303
+ hash = { :type => type_name(@object), type_name(@object) => hash } if @polymorphic
263
304
  @wrap_in_array ? [hash] : hash
264
305
  end
265
306
  alias_method :serializable_hash, :serializable_object
307
+
308
+ def serialize_id(elem, association)
309
+ id = elem.read_attribute_for_serialization(association.embed_key)
310
+ association.polymorphic? ? { id: id, type: type_name(elem) } : id
311
+ end
312
+
313
+ def type_name(elem)
314
+ elem.class.to_s.demodulize.underscore.to_sym
315
+ end
266
316
  end
267
317
 
268
318
  end
@@ -1,4 +1,6 @@
1
1
  require 'active_model/default_serializer'
2
+ require 'active_model/serializer/association/has_one'
3
+ require 'active_model/serializer/association/has_many'
2
4
 
3
5
  module ActiveModel
4
6
  class Serializer
@@ -13,6 +15,7 @@ module ActiveModel
13
15
  @name = name.to_s
14
16
  @options = options
15
17
  self.embed = options.fetch(:embed) { CONFIG.embed }
18
+ @polymorphic = options.fetch(:polymorphic, false)
16
19
  @embed_in_root = options.fetch(:embed_in_root) { options.fetch(:include) { CONFIG.embed_in_root } }
17
20
  @key_format = options.fetch(:key_format) { CONFIG.key_format }
18
21
  @embed_key = options[:embed_key] || :id
@@ -25,21 +28,22 @@ module ActiveModel
25
28
  @serializer_from_options = serializer.is_a?(String) ? serializer.constantize : serializer
26
29
  end
27
30
 
28
- attr_reader :name, :embed_ids, :embed_objects
31
+ attr_reader :name, :embed_ids, :embed_objects, :polymorphic
29
32
  attr_accessor :embed_in_root, :embed_key, :key, :embedded_key, :root_key, :serializer_from_options, :options, :key_format, :embed_in_root_key, :embed_namespace
30
33
  alias embed_ids? embed_ids
31
34
  alias embed_objects? embed_objects
32
35
  alias embed_in_root? embed_in_root
33
36
  alias embed_in_root_key? embed_in_root_key
34
37
  alias embed_namespace? embed_namespace
38
+ alias polymorphic? polymorphic
35
39
 
36
40
  def embed=(embed)
37
41
  @embed_ids = embed == :id || embed == :ids
38
42
  @embed_objects = embed == :object || embed == :objects
39
43
  end
40
44
 
41
- def serializer_from_object(object)
42
- Serializer.serializer_for(object)
45
+ def serializer_from_object(object, options = {})
46
+ Serializer.serializer_for(object, options)
43
47
  end
44
48
 
45
49
  def default_serializer
@@ -47,55 +51,7 @@ module ActiveModel
47
51
  end
48
52
 
49
53
  def build_serializer(object, options = {})
50
- serializer_class(object).new(object, options.merge(self.options))
51
- end
52
-
53
- class HasOne < Association
54
- def initialize(name, *args)
55
- super
56
- @root_key = @embedded_key.to_s.pluralize
57
- @key ||= "#{name}_id"
58
- end
59
-
60
- def serializer_class(object)
61
- serializer_from_options || serializer_from_object(object) || default_serializer
62
- end
63
-
64
- def build_serializer(object, options = {})
65
- options[:_wrap_in_array] = embed_in_root?
66
- super
67
- end
68
- end
69
-
70
- class HasMany < Association
71
- def initialize(name, *args)
72
- super
73
- @root_key = @embedded_key
74
- @key ||= "#{name.to_s.singularize}_ids"
75
- end
76
-
77
- def serializer_class(object)
78
- if use_array_serializer?
79
- ArraySerializer
80
- else
81
- serializer_from_options
82
- end
83
- end
84
-
85
- def options
86
- if use_array_serializer?
87
- { each_serializer: serializer_from_options }.merge! super
88
- else
89
- super
90
- end
91
- end
92
-
93
- private
94
-
95
- def use_array_serializer?
96
- !serializer_from_options ||
97
- serializer_from_options && !(serializer_from_options <= ArraySerializer)
98
- end
54
+ serializer_class(object, options).new(object, options.merge(self.options))
99
55
  end
100
56
  end
101
57
  end