active_model_serializers 0.1.0 → 0.5.0

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.
@@ -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