active_model_serializers 0.1.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,4 @@
1
- "!https://secure.travis-ci.org/josevalim/active_model_serializers.png!":http://travis-ci.org/josevalim/active_model_serializers
2
-
1
+ <strong>This was the original design document for serializers.</strong> It is useful mostly for historical purposes as the public API has changed.
3
2
 
4
3
  h2. Rails Serializers
5
4
 
@@ -456,7 +455,36 @@ The +association_ids+ helper will use the overridden version of the association,
456
455
  this case, +association_ids+ will only include the ids of the comments provided by the
457
456
  +comments+ method.
458
457
 
459
- h3. Authorization Scope
458
+
459
+ h3. Special Association Serializers
460
+
461
+ So far, associations defined in serializers use either the +as_json+ method on the model
462
+ or the defined serializer for the association type. Sometimes, you may want to serialize
463
+ associated models differently when they are requested as part of another resource than
464
+ when they are requested on their own.
465
+
466
+ For instance, we might want to provide the full comment when it is requested directly,
467
+ but only its title when requested as part of the post. To achieve this, you can define
468
+ a serializer for associated objects nested inside the main serializer.
469
+
470
+ <pre lang="ruby">
471
+ class PostSerializer < ActiveModel::Serializer
472
+ class CommentSerializer < ActiveModel::Serializer
473
+ attributes :id, :title
474
+ end
475
+
476
+ # same as before
477
+ # ...
478
+ end
479
+ </pre>
480
+
481
+ In other words, if a +PostSerializer+ is trying to serialize comments, it will first
482
+ look for +PostSerializer::CommentSerializer+ before falling back to +CommentSerializer+
483
+ and finally +comment.as_json+.
484
+
485
+ h3. Overriding the Defaults
486
+
487
+ h4. Authorization Scope
460
488
 
461
489
  By default, the authorization scope for serializers is +:current_user+. This means
462
490
  that when you call +render json: @post+, the controller will automatically call
data/Gemfile CHANGED
@@ -2,3 +2,6 @@ source 'http://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in active_model_serializers.gemspec
4
4
  gemspec
5
+
6
+ gem "pry"
7
+ gem "simplecov", :require => false
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2011-2012 José Valim & Yehuda Katz
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
@@ -0,0 +1,270 @@
1
+ [![Build Status](https://secure.travis-ci.org/josevalim/active_model_serializers.png)](http://travis-ci.org/josevalim/active_model_serializers)
2
+
3
+ # Purpose
4
+
5
+ The purpose of `ActiveModel::Serializers` is to provide an object to
6
+ encapsulate serialization of `ActiveModel` objects, including `ActiveRecord`
7
+ objects.
8
+
9
+ Serializers know about both a model and the `current_user`, so you can
10
+ customize serialization based upon whether a user is authorized to see the
11
+ content.
12
+
13
+ In short, **serializers replaces hash-driven development with object-oriented
14
+ development.**
15
+
16
+ # Installing Serializers
17
+
18
+ For now, the easiest way to install `ActiveModel::Serializers` is to add this
19
+ to your `Gemfile`:
20
+
21
+ ```ruby
22
+ gem "active_model_serializers", :git => "git://github.com/josevalim/active_model_serializers.git"
23
+ ```
24
+
25
+ Then, install it on the command line:
26
+
27
+ ```
28
+ $ bundle install
29
+ ```
30
+
31
+ # Creating a Serializer
32
+
33
+ The easiest way to create a new serializer is to generate a new resource, which
34
+ will generate a serializer at the same time:
35
+
36
+ ```
37
+ $ rails g resource post title:string body:string
38
+ ```
39
+
40
+ This will generate a serializer in `app/serializers/post_serializer.rb` for
41
+ your new model. You can also generate a serializer for an existing model with
42
+ the `serializer generator`:
43
+
44
+ ```
45
+ $ rails g serializer post
46
+ ```
47
+
48
+ # ActiveModel::Serializer
49
+
50
+ All new serializers descend from ActiveModel::Serializer
51
+
52
+ # render :json
53
+
54
+ In your controllers, when you use `render :json`, Rails will now first search
55
+ for a serializer for the object and use it if available.
56
+
57
+ ```ruby
58
+ class PostsController < ApplicationController
59
+ def show
60
+ @post = Post.find(params[:id])
61
+ render :json => @post
62
+ end
63
+ end
64
+ ```
65
+
66
+ In this case, Rails will look for a serializer named `PostSerializer`, and if
67
+ it exists, use it to serialize the `Post`.
68
+
69
+ This also works with `render_with`, which uses `to_json` under the hood. Also
70
+ note that any options passed to `render :json` will be passed to your
71
+ serializer and available as `@options` inside.
72
+
73
+ ## Getting the old version
74
+
75
+ If you find that your project is already relying on the old rails to_json
76
+ change `render :json` to `render :json => @your_object.to_json`.
77
+
78
+ # Attributes and Associations
79
+
80
+ Once you have a serializer, you can specify which attributes and associations
81
+ you would like to include in the serialized form.
82
+
83
+ ```ruby
84
+ class PostSerializer < ActiveModel::Serializer
85
+ attributes :id, :title, :body
86
+ has_many :comments
87
+ end
88
+ ```
89
+
90
+ ## Attributes
91
+
92
+ For specified attributes, the serializer will look up the attribute on the
93
+ object you passed to `render :json`. It uses
94
+ `read_attribute_for_serialization`, which `ActiveRecord` objects implement as a
95
+ regular attribute lookup.
96
+
97
+ If you would like the key in the outputted JSON to be different from its name
98
+ in ActiveRecord, you can use the `:key` option to customize it:
99
+
100
+ ```ruby
101
+ class PostSerializer < ActiveModel::Serializer
102
+ attributes :id, :body
103
+
104
+ # look up :subject on the model, but use +title+ in the JSON
105
+ attribute :subject, :key => :title
106
+ has_many :comments
107
+ end
108
+ ```
109
+
110
+ ## Associations
111
+
112
+ For specified associations, the serializer will look up the association and
113
+ then serialize each element of the association. For instance, a `has_many
114
+ :comments` association will create a new `CommentSerializer` for each comment
115
+ and use it to serialize the comment.
116
+
117
+ By default, serializers simply look up the association on the original object.
118
+ You can customize this behavior by implementing a method with the name of the
119
+ association and returning a different Array. Often, you will do this to
120
+ customize the objects returned based on the current user.
121
+
122
+ ```ruby
123
+ class PostSerializer < ActiveModel::Serializer
124
+ attributes :id, :title, :body
125
+ has_many :comments
126
+
127
+ # only let the user see comments he created.
128
+ def comments
129
+ post.comments.where(:created_by => @scope)
130
+ end
131
+ end
132
+ ```
133
+
134
+ In a serializer, `@scope` is the current authorization scope (usually
135
+ `current_user`), which the controller gives to the serializer when you call
136
+ `render :json`
137
+
138
+ As with attributes, you can also change the JSON key that the serializer should
139
+ use for a particular association.
140
+
141
+ ```ruby
142
+ class PostSerializer < ActiveModel::Serializer
143
+ attributes :id, :title, :body
144
+
145
+ # look up comments, but use +my_comments+ as the key in JSON
146
+ has_many :comments, :key => :my_comments
147
+ end
148
+ ```
149
+
150
+ ## Embedding Associations
151
+
152
+ By default, associations will be embedded inside the serialized object. So if
153
+ you have a post, the outputted JSON will look like:
154
+
155
+ ```json
156
+ {
157
+ "post": {
158
+ "id": 1,
159
+ "title": "New post",
160
+ "body": "A body!",
161
+ "comments": [
162
+ { "id": 1, "body": "what a dumb post" }
163
+ ]
164
+ }
165
+ }
166
+ ```
167
+
168
+ This is convenient for simple use-cases, but for more complex clients, it is
169
+ better to supply an Array of IDs for the association. This makes your API more
170
+ flexible from a performance standpoint and avoids wasteful duplication.
171
+
172
+ To embed IDs instead of associations, simply use the `embed` class method:
173
+
174
+ ```ruby
175
+ class PostSerializer < ActiveModel::Serializer
176
+ embed :ids
177
+
178
+ attributes :id, :title, :body
179
+ has_many :comments
180
+ end
181
+ ```
182
+
183
+ Now, any associations will be supplied as an Array of IDs:
184
+
185
+ ```json
186
+ {
187
+ "post": {
188
+ "id": 1,
189
+ "title": "New post",
190
+ "body": "A body!",
191
+ "comments": [ 1, 2, 3 ]
192
+ }
193
+ }
194
+ ```
195
+
196
+ In addition to supplying an Array of IDs, you may want to side-load the data
197
+ alongside the main object. This makes it easier to process the entire package
198
+ of data without having to recursively scan the tree looking for embedded
199
+ information. It also ensures that associations that are shared between several
200
+ objects (like tags), are only delivered once for the entire payload.
201
+
202
+ You can specify that the data be included like this:
203
+
204
+ ```ruby
205
+ class PostSerializer < ActiveModel::Serializer
206
+ embed :ids, :include => true
207
+
208
+ attributes :id, :title, :body
209
+ has_many :comments
210
+ end
211
+ ```
212
+
213
+ Assuming that the comments also `has_many :tags`, you will get a JSON like
214
+ this:
215
+
216
+ ```json
217
+ {
218
+ "post": {
219
+ "id": 1,
220
+ "title": "New post",
221
+ "body": "A body!",
222
+ "comments": [ 1 ]
223
+ },
224
+ "comments": [
225
+ { "id": 1, "body": "what a dumb post", "tags": [ 1, 2 ] },
226
+ { "id": 1, "body": "i liked it", "tags": [ 1, 3 ] },
227
+ ],
228
+ "tags": [
229
+ { "id": 1, "name": "short" },
230
+ { "id": 2, "name": "whiny" },
231
+ { "id": 3, "name": "happy" }
232
+ ]
233
+ }
234
+ ```
235
+
236
+ You can also specify a different root for the embedded objects than the key
237
+ used to reference them:
238
+
239
+ ```ruby
240
+ class PostSerializer < ActiveModel::Serializer
241
+ embed :ids, :include => true
242
+
243
+ attributes :id, :title, :body
244
+ has_many :comments, :key => :comment_ids, :root => :comment_objects
245
+ end
246
+ ```
247
+
248
+ This would generate JSON that would look like this:
249
+
250
+ ```json
251
+ {
252
+ "post": {
253
+ "id": 1,
254
+ "title": "New post",
255
+ "body": "A body!",
256
+ "comment_ids": [ 1 ]
257
+ },
258
+ "comment_objects": [
259
+ { "id": 1, "body": "what a dumb post" }
260
+ ]
261
+ }
262
+ ```
263
+
264
+ **NOTE**: The `embed :ids` mechanism is primary useful for clients that process
265
+ data in bulk and load it into a local store. For these clients, the ability to
266
+ easily see all of the data per type, rather than having to recursively scan the
267
+ data looking for information, is extremely useful.
268
+
269
+ If you are mostly working with the data in simple scenarios and manually making
270
+ Ajax requests, you probably just want to use the default embedded behavior.
@@ -0,0 +1,4 @@
1
+ # VERSION 0.5 (May 16, 2012)
2
+
3
+ * First tagged version
4
+ * Changes generators to always generate an ApplicationSerializer
@@ -1,4 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
+
3
+ $:.unshift File.expand_path("../lib", __FILE__)
4
+ require "active_model/serializers/version"
5
+
2
6
  Gem::Specification.new do |gem|
3
7
  gem.authors = ["José Valim", "Yehuda Katz"]
4
8
  gem.email = ["jose.valim@gmail.com", "wycats@gmail.com"]
@@ -11,7 +15,7 @@ Gem::Specification.new do |gem|
11
15
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
12
16
  gem.name = "active_model_serializers"
13
17
  gem.require_paths = ["lib"]
14
- gem.version = "0.1.0"
18
+ gem.version = ActiveModel::Serializer::VERSION
15
19
 
16
20
  gem.add_dependency 'activemodel', '~> 3.0'
17
21
  gem.add_development_dependency "rails", "~> 3.0"
@@ -33,12 +33,23 @@ module ActionController
33
33
  end
34
34
 
35
35
  def serialization_scope
36
- send(_serialization_scope)
36
+ send(_serialization_scope) if respond_to?(_serialization_scope)
37
+ end
38
+
39
+ def default_serializer_options
37
40
  end
38
41
 
39
42
  def _render_option_json(json, options)
40
- if json.respond_to?(:active_model_serializer) && (serializer = json.active_model_serializer)
41
- json = serializer.new(json, serialization_scope, options)
43
+ if json.respond_to?(:to_ary)
44
+ options[:root] ||= controller_name
45
+ end
46
+
47
+ serializer = options.delete(:serializer) ||
48
+ (json.respond_to?(:active_model_serializer) && json.active_model_serializer)
49
+
50
+ if serializer
51
+ options[:scope] = serialization_scope
52
+ json = serializer.new(json, options.merge(default_serializer_options || {}))
42
53
  end
43
54
  super
44
55
  end
@@ -1,23 +1,47 @@
1
1
  require "active_support/core_ext/class/attribute"
2
2
  require "active_support/core_ext/module/anonymous"
3
+ require "set"
3
4
 
4
5
  module ActiveModel
6
+ class OrderedSet
7
+ def initialize(array)
8
+ @array = array
9
+ @hash = {}
10
+
11
+ array.each do |item|
12
+ @hash[item] = true
13
+ end
14
+ end
15
+
16
+ def merge!(other)
17
+ other.each do |item|
18
+ next if @hash.key?(item)
19
+
20
+ @hash[item] = true
21
+ @array.push item
22
+ end
23
+ end
24
+
25
+ def to_a
26
+ @array
27
+ end
28
+ end
29
+
5
30
  # Active Model Array Serializer
6
31
  #
7
32
  # It serializes an array checking if each element that implements
8
- # the +active_model_serializer+ method passing down the current scope.
33
+ # the +active_model_serializer+ method.
9
34
  class ArraySerializer
10
- attr_reader :object, :scope
35
+ attr_reader :object, :options
11
36
 
12
- def initialize(object, scope, options={})
13
- @object, @scope, @options = object, scope, options
14
- @hash = options[:hash]
37
+ def initialize(object, options={})
38
+ @object, @options = object, options
15
39
  end
16
40
 
17
41
  def serializable_array
18
42
  @object.map do |item|
19
43
  if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer)
20
- serializer.new(item, scope, :hash => @hash)
44
+ serializer.new(item, @options)
21
45
  else
22
46
  item
23
47
  end
@@ -25,11 +49,13 @@ module ActiveModel
25
49
  end
26
50
 
27
51
  def as_json(*args)
28
- @hash = {}
29
- array = serializable_array.as_json(*args)
52
+ @options[:hash] = hash = {}
53
+ @options[:unique_values] = {}
54
+
55
+ array = serializable_array.map(&:serializable_hash)
30
56
 
31
57
  if root = @options[:root]
32
- @hash.merge!(root => array)
58
+ hash.merge!(root => array)
33
59
  else
34
60
  array
35
61
  end
@@ -40,12 +66,13 @@ module ActiveModel
40
66
  #
41
67
  # Provides a basic serializer implementation that allows you to easily
42
68
  # control how a given object is going to be serialized. On initialization,
43
- # it expects to object as arguments, a resource and a scope. For example,
69
+ # it expects to object as arguments, a resource and options. For example,
44
70
  # one may do in a controller:
45
71
  #
46
- # PostSerializer.new(@post, current_user).to_json
72
+ # PostSerializer.new(@post, :scope => current_user).to_json
47
73
  #
48
- # The object to be serialized is the +@post+ and the scope is +current_user+.
74
+ # The object to be serialized is the +@post+ and the current user is passed
75
+ # in for authorization purposes.
49
76
  #
50
77
  # We use the scope to check if a given attribute should be serialized or not.
51
78
  # For example, some attributes maybe only be returned if +current_user+ is the
@@ -70,22 +97,86 @@ module ActiveModel
70
97
  #
71
98
  class Serializer
72
99
  module Associations #:nodoc:
73
- class Config < Struct.new(:name, :options) #:nodoc:
74
- def serializer
75
- options[:serializer]
100
+ class Config #:nodoc:
101
+ class_attribute :options
102
+
103
+ def self.refine(name, class_options)
104
+ current_class = self
105
+
106
+ Class.new(self) do
107
+ singleton_class.class_eval do
108
+ define_method(:to_s) do
109
+ "(subclass of #{current_class.name})"
110
+ end
111
+
112
+ alias inspect to_s
113
+ end
114
+
115
+ self.options = class_options
116
+ end
117
+ end
118
+
119
+ self.options = {}
120
+
121
+ def initialize(name, source, options={})
122
+ @name = name
123
+ @source = source
124
+ @options = options
125
+ end
126
+
127
+ def option(key, default=nil)
128
+ if @options.key?(key)
129
+ @options[key]
130
+ elsif self.class.options.key?(key)
131
+ self.class.options[key]
132
+ else
133
+ default
134
+ end
135
+ end
136
+
137
+ def target_serializer
138
+ option(:serializer)
139
+ end
140
+
141
+ def source_serializer
142
+ @source
76
143
  end
77
144
 
78
145
  def key
79
- options[:key] || name
146
+ option(:key) || @name
147
+ end
148
+
149
+ def root
150
+ option(:root) || plural_key
151
+ end
152
+
153
+ def name
154
+ option(:name) || @name
155
+ end
156
+
157
+ def associated_object
158
+ option(:value) || source_serializer.send(name)
159
+ end
160
+
161
+ def embed_ids?
162
+ option(:embed, source_serializer._embed) == :ids
163
+ end
164
+
165
+ def embed_objects?
166
+ option(:embed, source_serializer._embed) == :objects
167
+ end
168
+
169
+ def embed_in_root?
170
+ option(:include, source_serializer._root_embed)
80
171
  end
81
172
 
82
- protected
173
+ protected
83
174
 
84
- def find_serializable(object, scope, context, options)
85
- if serializer
86
- serializer.new(object, scope, options)
175
+ def find_serializable(object)
176
+ if target_serializer
177
+ target_serializer.new(object, source_serializer.options)
87
178
  elsif object.respond_to?(:active_model_serializer) && (ams = object.active_model_serializer)
88
- ams.new(object, scope, options)
179
+ ams.new(object, source_serializer.options)
89
180
  else
90
181
  object
91
182
  end
@@ -93,32 +184,47 @@ module ActiveModel
93
184
  end
94
185
 
95
186
  class HasMany < Config #:nodoc:
96
- def serialize(collection, scope, context, options)
97
- array = collection.map do |item|
98
- find_serializable(item, scope, context, options).as_json(:root => false)
187
+ alias plural_key key
188
+
189
+ def serialize
190
+ associated_object.map do |item|
191
+ find_serializable(item).serializable_hash
99
192
  end
100
- { key => array }
101
193
  end
194
+ alias serialize_many serialize
102
195
 
103
- def serialize_ids(collection, scope)
196
+ def serialize_ids
104
197
  # Use pluck or select_columns if available
105
198
  # return collection.ids if collection.respond_to?(:ids)
106
199
 
107
- array = collection.map do |item|
200
+ associated_object.map do |item|
108
201
  item.read_attribute_for_serialization(:id)
109
202
  end
110
-
111
- { key => array }
112
203
  end
113
204
  end
114
205
 
115
206
  class HasOne < Config #:nodoc:
116
- def serialize(object, scope, context, options)
117
- { key => object && find_serializable(object, scope, context, options).as_json(:root => false) }
207
+ def plural_key
208
+ key.to_s.pluralize.to_sym
209
+ end
210
+
211
+ def serialize
212
+ object = associated_object
213
+ object && find_serializable(object).serializable_hash
118
214
  end
119
215
 
120
- def serialize_ids(object, scope)
121
- { key => object && object.read_attribute_for_serialization(:id) }
216
+ def serialize_many
217
+ object = associated_object
218
+ value = object && find_serializable(object).serializable_hash
219
+ value ? [value] : []
220
+ end
221
+
222
+ def serialize_ids
223
+ if object = associated_object
224
+ object.read_attribute_for_serialization(:id)
225
+ else
226
+ nil
227
+ end
122
228
  end
123
229
  end
124
230
  end
@@ -127,7 +233,7 @@ module ActiveModel
127
233
  self._attributes = {}
128
234
 
129
235
  class_attribute :_associations
130
- self._associations = []
236
+ self._associations = {}
131
237
 
132
238
  class_attribute :_root
133
239
  class_attribute :_embed
@@ -150,11 +256,14 @@ module ActiveModel
150
256
 
151
257
  def associate(klass, attrs) #:nodoc:
152
258
  options = attrs.extract_options!
153
- self._associations += attrs.map do |attr|
259
+ self._associations = _associations.dup
260
+
261
+ attrs.each do |attr|
154
262
  unless method_defined?(attr)
155
263
  class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__
156
264
  end
157
- klass.new(attr, options)
265
+
266
+ self._associations[attr] = klass.refine(attr, options)
158
267
  end
159
268
  end
160
269
 
@@ -188,7 +297,6 @@ module ActiveModel
188
297
  # { :name => :string, :age => :integer }
189
298
  #
190
299
  # The +associations+ hash looks like this:
191
- #
192
300
  # { :posts => { :has_many => :posts } }
193
301
  #
194
302
  # If :key is used:
@@ -220,7 +328,9 @@ module ActiveModel
220
328
  hash.merge key => column.type
221
329
  end
222
330
 
223
- associations = _associations.inject({}) do |hash, association|
331
+ associations = _associations.inject({}) do |hash, (attr,association_class)|
332
+ association = association_class.new(attr, self)
333
+
224
334
  model_association = klass.reflect_on_association(association.name)
225
335
  hash.merge association.key => { model_association.macro => model_association.name }
226
336
  end
@@ -260,11 +370,10 @@ module ActiveModel
260
370
  end
261
371
  end
262
372
 
263
- attr_reader :object, :scope
373
+ attr_reader :object, :options
264
374
 
265
- def initialize(object, scope, options={})
266
- @object, @scope, @options = object, scope, options
267
- @hash = options[:hash]
375
+ def initialize(object, options={})
376
+ @object, @options = object, options
268
377
  end
269
378
 
270
379
  # Returns a json representation of the serializable
@@ -272,63 +381,107 @@ module ActiveModel
272
381
  def as_json(options=nil)
273
382
  options ||= {}
274
383
  if root = options.fetch(:root, @options.fetch(:root, _root))
275
- @hash = hash = {}
384
+ @options[:hash] = hash = {}
385
+ @options[:unique_values] = {}
386
+
276
387
  hash.merge!(root => serializable_hash)
277
388
  hash
278
389
  else
279
- @hash = serializable_hash
390
+ serializable_hash
280
391
  end
281
392
  end
282
393
 
283
394
  # Returns a hash representation of the serializable
284
395
  # object without the root.
285
396
  def serializable_hash
286
- if _embed == :ids
287
- merge_associations(@hash, associations) if _root_embed
288
- attributes.merge(association_ids)
289
- elsif _embed == :objects
290
- attributes.merge(associations)
291
- else
292
- attributes
293
- end
397
+ node = attributes
398
+ include_associations!(node) if _embed
399
+ node
294
400
  end
295
401
 
296
- # Merge associations for embed case by always adding
297
- # root associations to the given hash.
298
- def merge_associations(hash, associations)
299
- associations.each do |key, value|
300
- if hash[key]
301
- hash[key] |= value
302
- elsif value
303
- hash[key] = value
402
+ def include_associations!(node)
403
+ _associations.each do |attr, klass|
404
+ opts = { :node => node }
405
+
406
+ if options.include?(:include) || options.include?(:exclude)
407
+ opts[:include] = included_association?(attr)
304
408
  end
409
+
410
+ include! attr, opts
305
411
  end
306
412
  end
307
413
 
308
- # Returns a hash representation of the serializable
309
- # object associations.
310
- def associations
311
- hash = {}
312
-
313
- _associations.each do |association|
314
- associated_object = send(association.name)
315
- hash.merge! association.serialize(associated_object, scope, self, :hash => @hash)
414
+ def included_association?(name)
415
+ if options.key?(:include)
416
+ options[:include].include?(name)
417
+ elsif options.key?(:exclude)
418
+ !options[:exclude].include?(name)
419
+ else
420
+ true
316
421
  end
317
-
318
- hash
319
422
  end
320
423
 
321
- # Returns a hash representation of the serializable
322
- # object associations ids.
323
- def association_ids
324
- hash = {}
424
+ def include!(name, options={})
425
+ # Make sure that if a special options[:hash] was passed in, we generate
426
+ # a new unique values hash and don't clobber the original. If the hash
427
+ # passed in is the same as the current options hash, use the current
428
+ # unique values.
429
+ #
430
+ # TODO: Should passing in a Hash even be public API here?
431
+ unique_values =
432
+ if hash = options[:hash]
433
+ if @options[:hash] == hash
434
+ @options[:unique_values] ||= {}
435
+ else
436
+ {}
437
+ end
438
+ else
439
+ hash = @options[:hash]
440
+ @options[:unique_values] ||= {}
441
+ end
325
442
 
326
- _associations.each do |association|
327
- associated_object = send(association.name)
328
- hash.merge! association.serialize_ids(associated_object, scope)
443
+ node = options[:node]
444
+ value = options[:value]
445
+
446
+ association_class =
447
+ if klass = _associations[name]
448
+ klass
449
+ elsif value.respond_to?(:to_ary)
450
+ Associations::HasMany
451
+ else
452
+ Associations::HasOne
453
+ end
454
+
455
+ association = association_class.new(name, self, options)
456
+
457
+ if association.embed_ids?
458
+ node[association.key] = association.serialize_ids
459
+
460
+ if association.embed_in_root?
461
+ merge_association hash, association.root, association.serialize_many, unique_values
462
+ end
463
+ elsif association.embed_objects?
464
+ node[association.key] = association.serialize
329
465
  end
466
+ end
330
467
 
331
- hash
468
+ # In some cases, an Array of associations is built by merging the associated
469
+ # content for all of the children. For instance, if a Post has_many comments,
470
+ # which has_many tags, the top-level :tags key will contain the merged list
471
+ # of all tags for all comments of the post.
472
+ #
473
+ # In order to make this efficient, we store a :unique_values hash containing
474
+ # a unique list of all of the objects that are already in the Array. This
475
+ # avoids the need to scan through the Array looking for entries every time
476
+ # we want to merge a new list of values.
477
+ def merge_association(hash, key, value, unique_values)
478
+ if current_value = unique_values[key]
479
+ current_value.merge! value
480
+ hash[key] = current_value.to_a
481
+ elsif value
482
+ hash[key] = value
483
+ unique_values[key] = OrderedSet.new(value)
484
+ end
332
485
  end
333
486
 
334
487
  # Returns a hash representation of the serializable