active_model_serializers 0.8.4 → 0.9.0.alpha1

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 (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