active_model_serializers 0.9.0 → 0.9.8

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