active_model_serializers 0.8.4 → 0.9.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -45
  3. data/CONTRIBUTING.md +20 -0
  4. data/DESIGN.textile +4 -4
  5. data/{MIT-LICENSE.txt → MIT-LICENSE} +0 -0
  6. data/README.md +187 -113
  7. data/lib/action_controller/serialization.rb +30 -16
  8. data/lib/active_model/array_serializer.rb +36 -82
  9. data/lib/active_model/default_serializer.rb +22 -0
  10. data/lib/active_model/serializable.rb +25 -0
  11. data/lib/active_model/serializer.rb +126 -447
  12. data/lib/active_model/serializer/associations.rb +53 -211
  13. data/lib/active_model/serializer/config.rb +31 -0
  14. data/lib/active_model/serializer/generators/resource_override.rb +13 -0
  15. data/lib/{generators → active_model/serializer/generators}/serializer/USAGE +0 -0
  16. data/lib/active_model/serializer/generators/serializer/scaffold_controller_generator.rb +14 -0
  17. data/lib/active_model/serializer/generators/serializer/serializer_generator.rb +37 -0
  18. data/lib/active_model/serializer/generators/serializer/templates/controller.rb +93 -0
  19. data/lib/active_model/serializer/generators/serializer/templates/serializer.rb +8 -0
  20. data/lib/active_model/serializer/railtie.rb +10 -0
  21. data/lib/active_model/{serializers → serializer}/version.rb +1 -1
  22. data/lib/active_model/serializer_support.rb +5 -0
  23. data/lib/active_model_serializers.rb +7 -86
  24. data/test/coverage_setup.rb +15 -0
  25. data/test/fixtures/active_record.rb +92 -0
  26. data/test/fixtures/poro.rb +64 -0
  27. data/test/integration/action_controller/serialization_test.rb +234 -0
  28. data/test/integration/active_record/active_record_test.rb +77 -0
  29. data/test/integration/generators/resource_generator_test.rb +26 -0
  30. data/test/integration/generators/scaffold_controller_generator_test.rb +67 -0
  31. data/test/integration/generators/serializer_generator_test.rb +41 -0
  32. data/test/test_app.rb +11 -0
  33. data/test/test_helper.rb +7 -41
  34. data/test/tmp/app/serializers/account_serializer.rb +3 -0
  35. data/test/unit/active_model/array_serializer/meta_test.rb +53 -0
  36. data/test/unit/active_model/array_serializer/root_test.rb +102 -0
  37. data/test/unit/active_model/array_serializer/scope_test.rb +24 -0
  38. data/test/unit/active_model/array_serializer/serialization_test.rb +83 -0
  39. data/test/unit/active_model/default_serializer_test.rb +13 -0
  40. data/test/unit/active_model/serializer/associations/build_serializer_test.rb +21 -0
  41. data/test/unit/active_model/serializer/associations_test.rb +19 -0
  42. data/test/unit/active_model/serializer/attributes_test.rb +41 -0
  43. data/test/unit/active_model/serializer/config_test.rb +86 -0
  44. data/test/unit/active_model/serializer/filter_test.rb +49 -0
  45. data/test/unit/active_model/serializer/has_many_test.rb +173 -0
  46. data/test/unit/active_model/serializer/has_one_test.rb +151 -0
  47. data/test/unit/active_model/serializer/meta_test.rb +39 -0
  48. data/test/unit/active_model/serializer/root_test.rb +117 -0
  49. data/test/unit/active_model/serializer/scope_test.rb +49 -0
  50. metadata +78 -74
  51. data/.gitignore +0 -18
  52. data/.travis.yml +0 -34
  53. data/Gemfile +0 -38
  54. data/Rakefile +0 -22
  55. data/active_model_serializers.gemspec +0 -24
  56. data/appveyor.yml +0 -27
  57. data/bench/perf.rb +0 -43
  58. data/cruft.md +0 -19
  59. data/lib/active_record/serializer_override.rb +0 -16
  60. data/lib/generators/resource_override.rb +0 -13
  61. data/lib/generators/serializer/serializer_generator.rb +0 -42
  62. data/lib/generators/serializer/templates/serializer.rb +0 -19
  63. data/test/array_serializer_test.rb +0 -75
  64. data/test/association_test.rb +0 -592
  65. data/test/caching_test.rb +0 -177
  66. data/test/generators_test.rb +0 -85
  67. data/test/no_serialization_scope_test.rb +0 -34
  68. data/test/serialization_scope_name_test.rb +0 -67
  69. data/test/serialization_test.rb +0 -396
  70. data/test/serializer_support_test.rb +0 -51
  71. data/test/serializer_test.rb +0 -1466
  72. data/test/test_fakes.rb +0 -218
@@ -1,3 +1,5 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+
1
3
  module ActionController
2
4
  # Action Controller Serialization
3
5
  #
@@ -32,29 +34,41 @@ module ActionController
32
34
  self._serialization_scope = :current_user
33
35
  end
34
36
 
35
- def serialization_scope
36
- send(_serialization_scope) if _serialization_scope &&
37
- respond_to?(_serialization_scope, true)
37
+ module ClassMethods
38
+ def serialization_scope(scope)
39
+ self._serialization_scope = scope
40
+ end
38
41
  end
39
42
 
40
- def default_serializer_options
43
+ def _render_option_json(resource, options)
44
+ serializer = build_json_serializer(resource, options)
45
+
46
+ if serializer
47
+ super(serializer, options)
48
+ else
49
+ super
50
+ end
41
51
  end
42
52
 
43
- [:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
44
- define_method renderer_method do |resource, options|
45
- json = ActiveModel::Serializer.build_json(self, resource, options)
53
+ private
46
54
 
47
- if json
48
- super(json, options)
49
- else
50
- super(resource, options)
51
- end
52
- end
55
+ def default_serializer_options
56
+ {}
53
57
  end
54
58
 
55
- module ClassMethods
56
- def serialization_scope(scope)
57
- self._serialization_scope = scope
59
+ def serialization_scope
60
+ _serialization_scope = self.class._serialization_scope
61
+ send(_serialization_scope) if _serialization_scope && respond_to?(_serialization_scope, true)
62
+ end
63
+
64
+ def build_json_serializer(resource, options = {})
65
+ options = default_serializer_options.merge(options)
66
+
67
+ if serializer = options.fetch(:serializer, ActiveModel::Serializer.serializer_for(resource))
68
+ options[:scope] = serialization_scope unless options.has_key?(:scope)
69
+ options[:resource_name] = controller_name if resource.respond_to?(:to_ary)
70
+
71
+ serializer.new(resource, options)
58
72
  end
59
73
  end
60
74
  end
@@ -1,104 +1,58 @@
1
- require "active_support/core_ext/class/attribute"
2
- require 'active_support/dependencies'
3
- require 'active_support/descendants_tracker'
1
+ require 'active_model/default_serializer'
2
+ require 'active_model/serializable'
3
+ require 'active_model/serializer'
4
4
 
5
5
  module ActiveModel
6
- # Active Model Array Serializer
7
- #
8
- # Serializes an Array, checking if each element implements
9
- # the +active_model_serializer+ method.
10
- #
11
- # To disable serialization of root elements:
12
- #
13
- # ActiveModel::ArraySerializer.root = false
14
- #
15
6
  class ArraySerializer
16
- extend ActiveSupport::DescendantsTracker
17
-
18
- attr_reader :object, :options
19
-
20
- class_attribute :root
21
-
22
- class_attribute :cache
23
- class_attribute :perform_caching
7
+ include Serializable
24
8
 
25
9
  class << self
26
- # set perform caching like root
27
- def cached(value = true)
28
- self.perform_caching = value
29
- end
10
+ attr_accessor :_root
11
+ alias root _root=
12
+ alias root= _root=
30
13
  end
31
14
 
32
15
  def initialize(object, options={})
33
- @object, @options = object, options
34
- end
35
-
36
- def meta_key
37
- @options[:meta_key].try(:to_sym) || :meta
38
- end
39
-
40
- def include_meta(hash)
41
- hash[meta_key] = @options[:meta] if @options.has_key?(:meta)
42
- end
43
-
44
- def as_json(*args)
45
- @options[:hash] = hash = {}
46
- @options[:unique_values] = {}
47
-
48
- if root = @options[:root]
49
- hash.merge!(root => serializable_array)
50
- include_meta hash
51
- hash
16
+ @object = object
17
+ @scope = options[:scope]
18
+ @root = options.fetch(:root, self.class._root)
19
+ @meta_key = options[:meta_key] || :meta
20
+ @meta = options[@meta_key]
21
+ @each_serializer = options[:each_serializer]
22
+ @resource_name = options[:resource_name]
23
+ end
24
+ attr_accessor :object, :scope, :root, :meta_key, :meta
25
+
26
+ def json_key
27
+ if root.nil?
28
+ @resource_name
52
29
  else
53
- serializable_array
30
+ root
54
31
  end
55
32
  end
56
33
 
57
- def to_json(*args)
58
- if perform_caching?
59
- cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'to-json']) do
60
- super
61
- end
62
- else
63
- super
64
- end
34
+ def serializer_for(item)
35
+ serializer_class = @each_serializer || Serializer.serializer_for(item) || DefaultSerializer
36
+ serializer_class.new(item, scope: scope)
65
37
  end
66
38
 
67
- def serializable_array
68
- if perform_caching?
69
- cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'serializable-array']) do
70
- _serializable_array
71
- end
72
- else
73
- _serializable_array
39
+ def serializable_object
40
+ @object.map do |item|
41
+ serializer_for(item).serializable_object
74
42
  end
75
43
  end
44
+ alias_method :serializable_array, :serializable_object
76
45
 
77
- private
78
- def _serializable_array
79
- @object.map do |item|
80
- if @options.has_key? :each_serializer
81
- serializer = @options[:each_serializer]
82
- elsif item.respond_to?(:active_model_serializer)
83
- serializer = item.active_model_serializer
84
- end
85
-
86
- serializable = serializer ? serializer.new(item, @options) : DefaultSerializer.new(item, @options.merge(:root => false))
87
-
88
- if serializable.respond_to?(:serializable_hash)
89
- serializable.serializable_hash
90
- else
91
- serializable.as_json
46
+ def embedded_in_root_associations
47
+ @object.each_with_object({}) do |item, hash|
48
+ serializer_for(item).embedded_in_root_associations.each_pair do |type, objects|
49
+ if hash.has_key?(type)
50
+ hash[type].concat(objects).uniq!
51
+ else
52
+ hash[type] = objects
53
+ end
92
54
  end
93
55
  end
94
56
  end
95
-
96
- def expand_cache_key(*args)
97
- ActiveSupport::Cache.expand_cache_key(args)
98
- end
99
-
100
- def perform_caching?
101
- perform_caching && cache && respond_to?(:cache_key)
102
- end
103
57
  end
104
58
  end
@@ -0,0 +1,22 @@
1
+ require 'active_model/serializable'
2
+
3
+ module ActiveModel
4
+ # DefaultSerializer
5
+ #
6
+ # Provides a constant interface for all items
7
+ class DefaultSerializer
8
+ include ActiveModel::Serializable
9
+
10
+ attr_reader :object
11
+
12
+ def initialize(object, options=nil)
13
+ @object = object
14
+ end
15
+
16
+ def as_json(options={})
17
+ @object.as_json
18
+ end
19
+ alias serializable_hash as_json
20
+ alias serializable_object as_json
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveModel
2
+ module Serializable
3
+ def as_json(options={})
4
+ if root = options.fetch(:root, json_key)
5
+ hash = { root => serializable_object }
6
+ hash.merge!(serializable_data)
7
+ hash
8
+ else
9
+ serializable_object
10
+ end
11
+ end
12
+
13
+ def serializable_data
14
+ embedded_in_root_associations.tap do |hash|
15
+ if respond_to?(:meta) && meta
16
+ hash[meta_key] = meta
17
+ end
18
+ end
19
+ end
20
+
21
+ def embedded_in_root_associations
22
+ {}
23
+ end
24
+ end
25
+ end
@@ -1,514 +1,193 @@
1
- require "active_support/core_ext/class/attribute"
2
- require "active_support/core_ext/module/anonymous"
3
- require 'active_support/dependencies'
4
- require 'active_support/descendants_tracker'
1
+ require 'active_model/array_serializer'
2
+ require 'active_model/serializable'
3
+ require 'active_model/serializer/associations'
4
+ require 'active_model/serializer/config'
5
+
6
+ require 'thread'
5
7
 
6
8
  module ActiveModel
7
- # Active Model Serializer
8
- #
9
- # Provides a basic serializer implementation that allows you to easily
10
- # control how a given object is going to be serialized. On initialization,
11
- # it expects two objects as arguments, a resource and options. For example,
12
- # one may do in a controller:
13
- #
14
- # PostSerializer.new(@post, :scope => current_user).to_json
15
- #
16
- # The object to be serialized is the +@post+ and the current user is passed
17
- # in for authorization purposes.
18
- #
19
- # We use the scope to check if a given attribute should be serialized or not.
20
- # For example, some attributes may only be returned if +current_user+ is the
21
- # author of the post:
22
- #
23
- # class PostSerializer < ActiveModel::Serializer
24
- # attributes :title, :body
25
- # has_many :comments
26
- #
27
- # private
28
- #
29
- # def attributes
30
- # hash = super
31
- # hash.merge!(:email => post.email) if author?
32
- # hash
33
- # end
34
- #
35
- # def author?
36
- # post.author == scope
37
- # end
38
- # end
39
- #
40
9
  class Serializer
41
- extend ActiveSupport::DescendantsTracker
42
-
43
- INCLUDE_METHODS = {}
44
- INSTRUMENT = { :serialize => :"serialize.serializer", :associations => :"associations.serializer" }
45
-
46
- class IncludeError < StandardError
47
- attr_reader :source, :association
48
-
49
- def initialize(source, association)
50
- @source, @association = source, association
51
- end
52
-
53
- def to_s
54
- "Cannot serialize #{association} when #{source} does not have a root!"
55
- end
56
- end
57
-
58
- class_attribute :_attributes
59
- self._attributes = {}
10
+ include Serializable
60
11
 
61
- class_attribute :_associations
62
- self._associations = {}
63
-
64
- class_attribute :_root
65
- class_attribute :_embed
66
- self._embed = :objects
67
- class_attribute :_root_embed
68
-
69
- class_attribute :cache
70
- class_attribute :perform_caching
12
+ @mutex = Mutex.new
71
13
 
72
14
  class << self
73
- # set perform caching like root
74
- def cached(value = true)
75
- self.perform_caching = value
76
- end
77
-
78
- # Define attributes to be used in the serialization.
79
- def attributes(*attrs)
80
-
81
- self._attributes = _attributes.dup
82
-
83
- attrs.each do |attr|
84
- if Hash === attr
85
- attr.each {|attr_real, key| attribute attr_real, :key => key }
86
- else
87
- attribute attr
88
- end
89
- end
90
- end
91
-
92
- def attribute(attr, options={})
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
96
-
97
- unless method_defined?(attr)
98
- define_method attr do
99
- object.read_attribute_for_serialization(attr.to_sym)
100
- end
101
- end
102
-
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
-
113
- end
114
-
115
- def associate(klass, attrs) #:nodoc:
116
- options = attrs.extract_options!
117
- self._associations = _associations.dup
118
-
119
- attrs.each do |attr|
120
- unless method_defined?(attr)
121
- define_method attr do
122
- object.send attr
123
- end
124
- end
125
-
126
- define_include_method attr
127
-
128
- self._associations[attr] = klass.refine(attr, options)
129
- end
15
+ def inherited(base)
16
+ base._root = _root
17
+ base._attributes = (_attributes || []).dup
18
+ base._associations = (_associations || {}).dup
130
19
  end
131
20
 
132
- def define_include_method(name)
133
- method = "include_#{name}?".to_sym
134
-
135
- INCLUDE_METHODS[name] = method
136
-
137
- unless method_defined?(method)
138
- define_method method do
139
- true
140
- end
21
+ def setup
22
+ @mutex.synchronize do
23
+ yield CONFIG
141
24
  end
142
25
  end
143
26
 
144
- # Defines an association in the object should be rendered.
145
- #
146
- # The serializer object should implement the association name
147
- # as a method which should return an array when invoked. If a method
148
- # with the association name does not exist, the association name is
149
- # dispatched to the serialized object.
150
- def has_many(*attrs)
151
- associate(Associations::HasMany, attrs)
152
- end
153
-
154
- # Defines an association in the object should be rendered.
155
- #
156
- # The serializer object should implement the association name
157
- # as a method which should return an object when invoked. If a method
158
- # with the association name does not exist, the association name is
159
- # dispatched to the serialized object.
160
- def has_one(*attrs)
161
- associate(Associations::HasOne, attrs)
27
+ def embed(type, options={})
28
+ CONFIG.embed = type
29
+ CONFIG.embed_in_root = true if options[:embed_in_root] || options[:include]
30
+ ActiveSupport::Deprecation.warn <<-WARN
31
+ ** Notice: embed is deprecated. **
32
+ The use of .embed method on a Serializer will be soon removed, as this should have a global scope and not a class scope.
33
+ Please use the global .setup method instead:
34
+ ActiveModel::Serializer.setup do |config|
35
+ config.embed = :#{type}
36
+ config.embed_in_root = #{CONFIG.embed_in_root || false}
37
+ end
38
+ WARN
162
39
  end
163
40
 
164
- # Return a schema hash for the current serializer. This information
165
- # can be used to generate clients for the serialized output.
166
- #
167
- # The schema hash has two keys: +attributes+ and +associations+.
168
- #
169
- # The +attributes+ hash looks like this:
170
- #
171
- # { :name => :string, :age => :integer }
172
- #
173
- # The +associations+ hash looks like this:
174
- # { :posts => { :has_many => :posts } }
175
- #
176
- # If :key is used:
177
- #
178
- # class PostsSerializer < ActiveModel::Serializer
179
- # has_many :posts, :key => :my_posts
180
- # end
181
- #
182
- # the hash looks like this:
183
- #
184
- # { :my_posts => { :has_many => :posts }
185
- #
186
- # This information is extracted from the serializer's model class,
187
- # which is provided by +SerializerClass.model_class+.
188
- #
189
- # The schema method uses the +columns_hash+ and +reflect_on_association+
190
- # methods, provided by default by ActiveRecord. You can implement these
191
- # methods on your custom models if you want the serializer's schema method
192
- # to work.
193
- #
194
- # TODO: This is currently coupled to Active Record. We need to
195
- # figure out a way to decouple those two.
196
- def schema
197
- klass = model_class
198
- columns = klass.columns_hash
199
-
200
- attrs = {}
201
- _attributes.each do |name, key|
202
- if column = columns[name.to_s]
203
- attrs[key] = column.type
41
+ if RUBY_VERSION >= '2.0'
42
+ def serializer_for(resource)
43
+ if resource.respond_to?(:to_ary)
44
+ ArraySerializer
204
45
  else
205
- # Computed attribute (method on serializer or model). We cannot
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
46
+ begin
47
+ Object.const_get "#{resource.class.name}Serializer"
48
+ rescue NameError
49
+ nil
211
50
  end
212
51
  end
213
52
  end
214
-
215
- associations = {}
216
- _associations.each do |attr, association_class|
217
- association = association_class.new(attr, self)
218
-
219
- if model_association = klass.reflect_on_association(association.name)
220
- # Real association.
221
- associations[association.key] = { model_association.macro => model_association.name }
53
+ else
54
+ def serializer_for(resource)
55
+ if resource.respond_to?(:to_ary)
56
+ ArraySerializer
222
57
  else
223
- # Computed association. We could infer has_many vs. has_one from
224
- # the association class, but that would make it different from
225
- # real associations, which read has_one vs. belongs_to from the
226
- # model.
227
- associations[association.key] = nil
58
+ "#{resource.class.name}Serializer".safe_constantize
228
59
  end
229
60
  end
230
-
231
- { :attributes => attrs, :associations => associations }
232
- end
233
-
234
- # The model class associated with this serializer.
235
- def model_class
236
- name.sub(/Serializer$/, '').constantize
237
61
  end
238
62
 
239
- # Define how associations should be embedded.
240
- #
241
- # embed :objects # Embed associations as full objects
242
- # embed :ids # Embed only the association ids
243
- # embed :ids, :include => true # Embed the association ids and include objects in the root
244
- #
245
- def embed(type, options={})
246
- self._embed = type
247
- self._root_embed = true if options[:include]
248
- end
63
+ attr_accessor :_root, :_attributes, :_associations
64
+ alias root _root=
65
+ alias root= _root=
249
66
 
250
- # Defines the root used on serialization. If false, disables the root.
251
- def root(name)
252
- self._root = name
67
+ def root_name
68
+ name.demodulize.underscore.sub(/_serializer$/, '') if name
253
69
  end
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
70
 
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
71
+ def attributes(*attrs)
72
+ @_attributes.concat attrs
275
73
 
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
74
+ attrs.each do |attr|
75
+ define_method attr do
76
+ object.read_attribute_for_serialization attr
77
+ end unless method_defined?(attr)
280
78
  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
79
  end
288
- end
289
80
 
290
- attr_reader :object, :options
291
-
292
- def initialize(object, options={})
293
- @object, @options = object, options
294
-
295
- scope_name = @options[:scope_name]
296
- if scope_name && !respond_to?(scope_name)
297
- self.singleton_class.send :alias_method, scope_name, :scope
81
+ def has_one(*attrs)
82
+ associate(Association::HasOne, *attrs)
298
83
  end
299
- end
300
-
301
- def root_name
302
- return false if self._root == false
303
84
 
304
- class_name = self.class.name.demodulize.underscore.sub(/_serializer$/, '').to_sym unless self.class.name.blank?
305
-
306
- if self._root == true
307
- class_name
308
- else
309
- self._root || class_name
85
+ def has_many(*attrs)
86
+ associate(Association::HasMany, *attrs)
310
87
  end
311
- end
312
88
 
313
- def url_options
314
- @options[:url_options] || {}
315
- end
89
+ private
316
90
 
317
- def meta_key
318
- @options[:meta_key].try(:to_sym) || :meta
319
- end
91
+ def associate(klass, *attrs)
92
+ options = attrs.extract_options!
320
93
 
321
- def include_meta(hash)
322
- hash[meta_key] = @options[:meta] if @options.has_key?(:meta)
323
- end
94
+ attrs.each do |attr|
95
+ define_method attr do
96
+ object.send attr
97
+ end unless method_defined?(attr)
324
98
 
325
- def to_json(*args)
326
- if perform_caching?
327
- cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'to-json']) do
328
- super
99
+ @_associations[attr] = klass.new(attr, options)
329
100
  end
330
- else
331
- super
332
- end
333
- end
334
-
335
- # Returns a json representation of the serializable
336
- # object including the root.
337
- def as_json(options={})
338
- options ||= {}
339
- if root = options.fetch(:root, @options.fetch(:root, root_name))
340
- @options[:hash] = hash = {}
341
- @options[:unique_values] = {}
342
-
343
- hash.merge!(root => serializable_hash)
344
- include_meta hash
345
- hash
346
- else
347
- serializable_hash
348
101
  end
349
102
  end
350
103
 
351
- # Returns a hash representation of the serializable
352
- # object without the root.
353
- def serializable_hash
354
- @node = if perform_caching?
355
- cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'serializable-hash']) do
356
- _serializable_hash
357
- end
104
+ def initialize(object, options={})
105
+ @object = object
106
+ @scope = options[:scope]
107
+ @root = options.fetch(:root, self.class._root)
108
+ @meta_key = options[:meta_key] || :meta
109
+ @meta = options[@meta_key]
110
+ @wrap_in_array = options[:_wrap_in_array]
111
+ end
112
+ attr_accessor :object, :scope, :root, :meta_key, :meta
113
+
114
+ def json_key
115
+ if root == true || root.nil?
116
+ self.class.root_name
358
117
  else
359
- _serializable_hash
118
+ root
360
119
  end
361
-
362
- include_associations! if @object.present? && _embed
363
- @node
364
120
  end
365
121
 
366
- def include_associations!
367
- _associations.each_key do |name|
368
- include!(name) if include?(name)
122
+ def attributes
123
+ filter(self.class._attributes.dup).each_with_object({}) do |name, hash|
124
+ hash[name] = send(name)
369
125
  end
370
126
  end
371
127
 
372
- def include?(name)
373
- return false if @options.key?(:only) && !Array(@options[:only]).include?(name)
374
- return false if @options.key?(:except) && Array(@options[:except]).include?(name)
375
- send INCLUDE_METHODS[name]
376
- end
377
-
378
- def include!(name, options={})
379
- # Make sure that if a special options[:hash] was passed in, we generate
380
- # a new unique values hash and don't clobber the original. If the hash
381
- # passed in is the same as the current options hash, use the current
382
- # unique values.
383
- #
384
- # TODO: Should passing in a Hash even be public API here?
385
- unique_values =
386
- if hash = options[:hash]
387
- if @options[:hash] == hash
388
- @options[:unique_values] ||= {}
389
- else
390
- {}
128
+ def associations
129
+ associations = self.class._associations
130
+ included_associations = filter(associations.keys)
131
+ associations.each_with_object({}) do |(name, association), hash|
132
+ if included_associations.include? name
133
+ if association.embed_ids?
134
+ hash[association.key] = serialize_ids association
135
+ elsif association.embed_objects?
136
+ hash[association.embedded_key] = serialize association
391
137
  end
392
- else
393
- hash = @options[:hash]
394
- @options[:unique_values] ||= {}
395
- end
396
-
397
- node = options[:node] ||= @node
398
- value = options[:value]
399
-
400
- if options[:include] == nil
401
- if @options.key?(:include)
402
- options[:include] = @options[:include].include?(name)
403
- elsif @options.include?(:exclude)
404
- options[:include] = !@options[:exclude].include?(name)
405
138
  end
406
139
  end
407
-
408
- association_class =
409
- if klass = _associations[name]
410
- klass
411
- elsif value.respond_to?(:to_ary)
412
- Associations::HasMany
413
- else
414
- Associations::HasOne
415
- end
416
-
417
- association = association_class.new(name, self, options)
418
-
419
- if association.embed_ids?
420
- node[association.key] = association.serialize_ids
421
-
422
- if association.embed_in_root? && hash.nil?
423
- raise IncludeError.new(self.class, association.name)
424
- elsif association.embed_in_root? && association.embeddable?
425
- merge_association hash, association.root, association.serializables, unique_values
426
- end
427
- elsif association.embed_objects?
428
- node[association.key] = association.serialize
429
- end
430
140
  end
431
141
 
432
- # In some cases, an Array of associations is built by merging the associated
433
- # content for all of the children. For instance, if a Post has_many comments,
434
- # which has_many tags, the top-level :tags key will contain the merged list
435
- # of all tags for all comments of the post.
436
- #
437
- # In order to make this efficient, we store a :unique_values hash containing
438
- # a unique list of all of the objects that are already in the Array. This
439
- # avoids the need to scan through the Array looking for entries every time
440
- # we want to merge a new list of values.
441
- def merge_association(hash, key, serializables, unique_values)
442
- already_serialized = (unique_values[key] ||= {})
443
- serializable_hashes = (hash[key] ||= [])
444
-
445
- serializables.each do |serializable|
446
- unless already_serialized.include? serializable.object
447
- already_serialized[serializable.object] = true
448
- serializable_hashes << serializable.serializable_hash
449
- end
450
- end
142
+ def filter(keys)
143
+ keys
451
144
  end
452
145
 
453
- # Returns a hash representation of the serializable
454
- # object attributes.
455
- def attributes
456
- _fast_attributes
457
- rescue NameError
458
- method = "def _fast_attributes\n"
459
-
460
- method << " h = {}\n"
146
+ def embedded_in_root_associations
147
+ associations = self.class._associations
148
+ included_associations = filter(associations.keys)
149
+ associations.each_with_object({}) do |(name, association), hash|
150
+ if included_associations.include? name
151
+ if association.embed_in_root?
152
+ association_serializer = build_serializer(association)
153
+ hash.merge! association_serializer.embedded_in_root_associations
461
154
 
462
- _attributes.each do |name,key|
463
- method << " h[:\"#{key}\"] = read_attribute_for_serialization(:\"#{name}\") if include?(:\"#{name}\")\n"
155
+ serialized_data = association_serializer.serializable_object
156
+ key = association.root_key
157
+ if hash.has_key?(key)
158
+ hash[key].concat(serialized_data).uniq!
159
+ else
160
+ hash[key] = serialized_data
161
+ end
162
+ end
464
163
  end
465
- method << " h\nend"
466
-
467
- self.class.class_eval method
468
- _fast_attributes
469
- end
470
-
471
- # Returns options[:scope]
472
- def scope
473
- @options[:scope]
474
- end
475
-
476
- alias :read_attribute_for_serialization :send
477
-
478
- def _serializable_hash
479
- return nil if @object.nil?
480
- attributes
481
- end
482
-
483
- def perform_caching?
484
- perform_caching && cache && respond_to?(:cache_key)
164
+ end
485
165
  end
486
166
 
487
- def expand_cache_key(*args)
488
- ActiveSupport::Cache.expand_cache_key(args)
167
+ def build_serializer(association)
168
+ object = send(association.name)
169
+ association.build_serializer(object, scope: scope)
489
170
  end
490
171
 
491
- # Use ActiveSupport::Notifications to send events to external systems.
492
- # The event name is: name.class_name.serializer
493
- def instrument(name, payload = {}, &block)
494
- event_name = INSTRUMENT[name]
495
- ActiveSupport::Notifications.instrument(event_name, payload, &block)
172
+ def serialize(association)
173
+ build_serializer(association).serializable_object
496
174
  end
497
- end
498
175
 
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
176
+ def serialize_ids(association)
177
+ associated_data = send(association.name)
178
+ if associated_data.respond_to?(:to_ary)
179
+ associated_data.map { |elem| elem.read_attribute_for_serialization(association.embed_key) }
180
+ else
181
+ associated_data.read_attribute_for_serialization(association.embed_key) if associated_data
182
+ end
508
183
  end
509
184
 
510
- def serializable_hash
511
- @object.as_json(@options)
185
+ def serializable_object(options={})
186
+ return nil if object.nil?
187
+ hash = attributes
188
+ hash.merge! associations
189
+ @wrap_in_array ? [hash] : hash
512
190
  end
191
+ alias_method :serializable_hash, :serializable_object
513
192
  end
514
193
  end