active_model_serializers 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - 1.9.3
5
+ - ree
6
+ - jruby
7
+ - rbx
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'http://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in active_model_serializers.gemspec
4
+ gemspec
@@ -0,0 +1,558 @@
1
+ "!https://secure.travis-ci.org/josevalim/active_model_serializers.png!":http://travis-ci.org/josevalim/active_model_serializers
2
+
3
+
4
+ h2. Rails Serializers
5
+
6
+ This guide describes how to use Active Model serializers to build non-trivial JSON services in Rails. By reading this guide, you will learn:
7
+
8
+ * When to use the built-in Active Model serialization
9
+ * When to use a custom serializer for your models
10
+ * How to use serializers to encapsulate authorization concerns
11
+ * How to create serializer templates to describe the application-wide structure of your serialized JSON
12
+ * How to build resources not backed by a single database table for use with JSON services
13
+
14
+ This guide covers an intermediate topic and assumes familiarity with Rails conventions. It is suitable for applications that expose a
15
+ JSON API that may return different results based on the authorization status of the user.
16
+
17
+ h3. Serialization
18
+
19
+ By default, Active Record objects can serialize themselves into JSON by using the `to_json` method. This method takes a series of additional
20
+ parameter to control which properties and associations Rails should include in the serialized output.
21
+
22
+ When building a web application that uses JavaScript to retrieve JSON data from the server, this mechanism has historically been the primary
23
+ way that Rails developers prepared their responses. This works great for simple cases, as the logic for serializing an Active Record object
24
+ is neatly encapsulated in Active Record itself.
25
+
26
+ However, this solution quickly falls apart in the face of serialization requirements based on authorization. For instance, a web service
27
+ may choose to expose additional information about a resource only if the user is entitled to access it. In addition, a JavaScript front-end
28
+ may want information that is not neatly described in terms of serializing a single Active Record object, or in a different format than.
29
+
30
+ In addition, neither the controller nor the model seems like the correct place for logic that describes how to serialize an model object
31
+ *for the current user*.
32
+
33
+ Serializers solve these problems by encapsulating serialization in an object designed for this purpose. If the default +to_json+ semantics,
34
+ with at most a few configuration options serve your needs, by all means continue to use the built-in +to_json+. If you find yourself doing
35
+ hash-driven-development in your controllers, juggling authorization logic and other concerns, serializers are for you!
36
+
37
+ h3. The Most Basic Serializer
38
+
39
+ A basic serializer is a simple Ruby object named after the model class it is serializing.
40
+
41
+ <pre lang="ruby">
42
+ class PostSerializer
43
+ def initialize(post, scope)
44
+ @post, @scope = post, scope
45
+ end
46
+
47
+ def as_json
48
+ { post: { title: @post.name, body: @post.body } }
49
+ end
50
+ end
51
+ </pre>
52
+
53
+ A serializer is initialized with two parameters: the model object it should serialize and an authorization scope. By default, the
54
+ authorization scope is the current user (+current_user+) but you can use a different object if you want. The serializer also
55
+ implements an +as_json+ method, which returns a Hash that will be sent to the JSON encoder.
56
+
57
+ Rails will transparently use your serializer when you use +render :json+ in your controller.
58
+
59
+ <pre lang="ruby">
60
+ class PostsController < ApplicationController
61
+ def show
62
+ @post = Post.find(params[:id])
63
+ render json: @post
64
+ end
65
+ end
66
+ </pre>
67
+
68
+ Because +respond_with+ uses +render :json+ under the hood for JSON requests, Rails will automatically use your serializer when
69
+ you use +respond_with+ as well.
70
+
71
+ h4. +serializable_hash+
72
+
73
+ In general, you will want to implement +serializable_hash+ and +as_json+ to allow serializers to embed associated content
74
+ directly. The easiest way to implement these two methods is to have +as_json+ call +serializable_hash+ and insert the root.
75
+
76
+ <pre lang="ruby">
77
+ class PostSerializer
78
+ def initialize(post, scope)
79
+ @post, @scope = post, scope
80
+ end
81
+
82
+ def serializable_hash
83
+ { title: @post.name, body: @post.body }
84
+ end
85
+
86
+ def as_json
87
+ { post: serializable_hash }
88
+ end
89
+ end
90
+ </pre>
91
+
92
+ h4. Authorization
93
+
94
+ Let's update our serializer to include the email address of the author of the post, but only if the current user has superuser
95
+ access.
96
+
97
+ <pre lang="ruby">
98
+ class PostSerializer
99
+ def initialize(post, scope)
100
+ @post, @scope = post, scope
101
+ end
102
+
103
+ def as_json
104
+ { post: serializable_hash }
105
+ end
106
+
107
+ def serializable_hash
108
+ hash = post
109
+ hash.merge!(super_data) if super?
110
+ hash
111
+ end
112
+
113
+ private
114
+ def post
115
+ { title: @post.name, body: @post.body }
116
+ end
117
+
118
+ def super_data
119
+ { email: @post.email }
120
+ end
121
+
122
+ def super?
123
+ @scope.superuser?
124
+ end
125
+ end
126
+ </pre>
127
+
128
+ h4. Testing
129
+
130
+ One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization
131
+ logic in isolation.
132
+
133
+ <pre lang="ruby">
134
+ require "ostruct"
135
+
136
+ class PostSerializerTest < ActiveSupport::TestCase
137
+ # For now, we use a very simple authorization structure. These tests will need
138
+ # refactoring if we change that.
139
+ plebe = OpenStruct.new(super?: false)
140
+ god = OpenStruct.new(super?: true)
141
+
142
+ post = OpenStruct.new(title: "Welcome to my blog!", body: "Blah blah blah", email: "tenderlove@gmail.com")
143
+
144
+ test "a regular user sees just the title and body" do
145
+ json = PostSerializer.new(post, plebe).to_json
146
+ hash = JSON.parse(json)
147
+
148
+ assert_equal post.title, hash.delete("title")
149
+ assert_equal post.body, hash.delete("body")
150
+ assert_empty hash
151
+ end
152
+
153
+ test "a superuser sees the title, body and email" do
154
+ json = PostSerializer.new(post, god).to_json
155
+ hash = JSON.parse(json)
156
+
157
+ assert_equal post.title, hash.delete("title")
158
+ assert_equal post.body, hash.delete("body")
159
+ assert_equal post.email, hash.delete("email")
160
+ assert_empty hash
161
+ end
162
+ end
163
+ </pre>
164
+
165
+ It's important to note that serializer objects define a clear interface specifically for serializing an existing object.
166
+ In this case, the serializer expects to receive a post object with +name+, +body+ and +email+ attributes and an authorization
167
+ scope with a +super?+ method.
168
+
169
+ By defining a clear interface, it's must easier to ensure that your authorization logic is behaving correctly. In this case,
170
+ the serializer doesn't need to concern itself with how the authorization scope decides whether to set the +super?+ flag, just
171
+ whether it is set. In general, you should document these requirements in your serializer files and programatically via tests.
172
+ The documentation library +YARD+ provides excellent tools for describing this kind of requirement:
173
+
174
+ <pre lang="ruby">
175
+ class PostSerializer
176
+ # @param [~body, ~title, ~email] post the post to serialize
177
+ # @param [~super] scope the authorization scope for this serializer
178
+ def initialize(post, scope)
179
+ @post, @scope = post, scope
180
+ end
181
+
182
+ # ...
183
+ end
184
+ </pre>
185
+
186
+ h3. Attribute Sugar
187
+
188
+ To simplify this process for a number of common cases, Rails provides a default superclass named +ActiveModel::Serializer+
189
+ that you can use to implement your serializers.
190
+
191
+ For example, you will sometimes want to simply include a number of existing attributes from the source model into the outputted
192
+ JSON. In the above example, the +title+ and +body+ attributes were always included in the JSON. Let's see how to use
193
+ +ActiveModel::Serializer+ to simplify our post serializer.
194
+
195
+ <pre lang="ruby">
196
+ class PostSerializer < ActiveModel::Serializer
197
+ attributes :title, :body
198
+
199
+ def serializable_hash
200
+ hash = attributes
201
+ hash.merge!(super_data) if super?
202
+ hash
203
+ end
204
+
205
+ private
206
+ def super_data
207
+ { email: @post.email }
208
+ end
209
+
210
+ def super?
211
+ @scope.superuser?
212
+ end
213
+ end
214
+ </pre>
215
+
216
+ First, we specified the list of included attributes at the top of the class. This will create an instance method called
217
+ +attributes+ that extracts those attributes from the post model.
218
+
219
+ NOTE: Internally, +ActiveModel::Serializer+ uses +read_attribute_for_serialization+, which defaults to +read_attribute+, which defaults to +send+. So if you're rolling your own models for use with the serializer, you can use simple Ruby accessors for your attributes if you like.
220
+
221
+ Next, we use the attributes methood in our +serializable_hash+ method, which allowed us to eliminate the +post+ method we hand-rolled
222
+ earlier. We could also eliminate the +as_json+ method, as +ActiveModel::Serializer+ provides a default +as_json+ method for
223
+ us that calls our +serializable_hash+ method and inserts a root. But we can go a step further!
224
+
225
+ <pre lang="ruby">
226
+ class PostSerializer < ActiveModel::Serializer
227
+ attributes :title, :body
228
+
229
+ private
230
+ def attributes
231
+ hash = super
232
+ hash.merge!(email: post.email) if super?
233
+ hash
234
+ end
235
+
236
+ def super?
237
+ @scope.superuser?
238
+ end
239
+ end
240
+ </pre>
241
+
242
+ The superclass provides a default +initialize+ method as well as a default +serializable_hash+ method, which uses
243
+ +attributes+. We can call +super+ to get the hash based on the attributes we declared, and then add in any additional
244
+ attributes we want to use.
245
+
246
+ NOTE: +ActiveModel::Serializer+ will create an accessor matching the name of the current class for the resource you pass in. In this case, because we have defined a PostSerializer, we can access the resource with the +post+ accessor.
247
+
248
+ h3. Associations
249
+
250
+ In most JSON APIs, you will want to include associated objects with your serialized object. In this case, let's include
251
+ the comments with the current post.
252
+
253
+ <pre lang="ruby">
254
+ class PostSerializer < ActiveModel::Serializer
255
+ attributes :title, :body
256
+ has_many :comments
257
+
258
+ private
259
+ def attributes
260
+ hash = super
261
+ hash.merge!(email: post.email) if super?
262
+ hash
263
+ end
264
+
265
+ def super?
266
+ @scope.superuser?
267
+ end
268
+ end
269
+ </pre>
270
+
271
+ The default +serializable_hash+ method will include the comments as embedded objects inside the post.
272
+
273
+ <pre lang="json">
274
+ {
275
+ post: {
276
+ title: "Hello Blog!",
277
+ body: "This is my first post. Isn't it fabulous!",
278
+ comments: [
279
+ {
280
+ title: "Awesome",
281
+ body: "Your first post is great"
282
+ }
283
+ ]
284
+ }
285
+ }
286
+ </pre>
287
+
288
+ Rails uses the same logic to generate embedded serializations as it does when you use +render :json+. In this case,
289
+ because you didn't define a +CommentSerializer+, Rails used the default +as_json+ on your comment object.
290
+
291
+ If you define a serializer, Rails will automatically instantiate it with the existing authorization scope.
292
+
293
+ <pre lang="ruby">
294
+ class CommentSerializer
295
+ def initialize(comment, scope)
296
+ @comment, @scope = comment, scope
297
+ end
298
+
299
+ def serializable_hash
300
+ { title: @comment.title }
301
+ end
302
+
303
+ def as_json
304
+ { comment: serializable_hash }
305
+ end
306
+ end
307
+ </pre>
308
+
309
+ If we define the above comment serializer, the outputted JSON will change to:
310
+
311
+ <pre lang="json">
312
+ {
313
+ post: {
314
+ title: "Hello Blog!",
315
+ body: "This is my first post. Isn't it fabulous!",
316
+ comments: [{ title: "Awesome" }]
317
+ }
318
+ }
319
+ </pre>
320
+
321
+ Let's imagine that our comment system allows an administrator to kill a comment, and we only want to allow
322
+ users to see the comments they're entitled to see. By default, +has_many :comments+ will simply use the
323
+ +comments+ accessor on the post object. We can override the +comments+ accessor to limit the comments used
324
+ to just the comments we want to allow for the current user.
325
+
326
+ <pre lang="ruby">
327
+ class PostSerializer < ActiveModel::Serializer
328
+ attributes :title. :body
329
+ has_many :comments
330
+
331
+ private
332
+ def attributes
333
+ hash = super
334
+ hash.merge!(email: post.email) if super?
335
+ hash
336
+ end
337
+
338
+ def comments
339
+ post.comments_for(scope)
340
+ end
341
+
342
+ def super?
343
+ @scope.superuser?
344
+ end
345
+ end
346
+ </pre>
347
+
348
+ +ActiveModel::Serializer+ will still embed the comments, but this time it will use just the comments
349
+ for the current user.
350
+
351
+ NOTE: The logic for deciding which comments a user should see still belongs in the model layer. In general, you should encapsulate concerns that require making direct Active Record queries in scopes or public methods on your models.
352
+
353
+ h4. Modifying Associations
354
+
355
+ You can also rename associations if required. Say for example you have an association that
356
+ makes sense to be named one thing in your code, but another when data is serialized.
357
+ You can use the <code:key</code> option to specify a different name for an association.
358
+ Here is an exmaple:
359
+
360
+ <pre lang="ruby">
361
+ class UserSerializer < ActiveModel::Serializer
362
+ has_many :followed_posts, :key => :posts
363
+ has_one :owned_account, :key => :account
364
+ end
365
+ </pre>
366
+
367
+ Using the <code>:key</code> without a <code>:serializer</code> option will use implicit detection
368
+ to determine a serializer. In this example, you'd have to define two classes: <code>PostSerializer</code>
369
+ and <code>AccountSerializer</code>. You can also add the <code>:serializer</code> option
370
+ to set it explicitly:
371
+
372
+ <pre lang="ruby">
373
+ class UserSerializer < ActiveModel::Serializer
374
+ has_many :followed_posts, :key => :posts, :serializer => CustomPostSerializer
375
+ has_one :owne_account, :key => :account, :serializer => PrivateAccountSerializer
376
+ end
377
+ </pre>
378
+
379
+ h3. Customizing Associations
380
+
381
+ Not all front-ends expect embedded documents in the same form. In these cases, you can override the
382
+ default +serializable_hash+, and use conveniences provided by +ActiveModel::Serializer+ to avoid having to
383
+ build up the hash manually.
384
+
385
+ For example, let's say our front-end expects the posts and comments in the following format:
386
+
387
+ <pre lang="json">
388
+ {
389
+ post: {
390
+ id: 1
391
+ title: "Hello Blog!",
392
+ body: "This is my first post. Isn't it fabulous!",
393
+ comments: [1,2]
394
+ },
395
+ comments: [
396
+ {
397
+ id: 1
398
+ title: "Awesome",
399
+ body: "Your first post is great"
400
+ },
401
+ {
402
+ id: 2
403
+ title: "Not so awesome",
404
+ body: "Why is it so short!"
405
+ }
406
+ ]
407
+ }
408
+ </pre>
409
+
410
+ We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments.
411
+
412
+ <pre lang="ruby">
413
+ class CommentSerializer < ActiveModel::Serializer
414
+ attributes :id, :title, :body
415
+
416
+ # define any logic for dealing with authorization-based attributes here
417
+ end
418
+
419
+ class PostSerializer < ActiveModel::Serializer
420
+ attributes :title, :body
421
+ has_many :comments
422
+
423
+ def as_json
424
+ { post: serializable_hash }.merge!(associations)
425
+ end
426
+
427
+ def serializable_hash
428
+ post_hash = attributes
429
+ post_hash.merge!(association_ids)
430
+ post_hash
431
+ end
432
+
433
+ private
434
+ def attributes
435
+ hash = super
436
+ hash.merge!(email: post.email) if super?
437
+ hash
438
+ end
439
+
440
+ def comments
441
+ post.comments_for(scope)
442
+ end
443
+
444
+ def super?
445
+ @scope.superuser?
446
+ end
447
+ end
448
+ </pre>
449
+
450
+ Here, we used two convenience methods: +associations+ and +association_ids+. The first,
451
+ +associations+, creates a hash of all of the define associations, using their defined
452
+ serializers. The second, +association_ids+, generates a hash whose key is the association
453
+ name and whose value is an Array of the association's keys.
454
+
455
+ The +association_ids+ helper will use the overridden version of the association, so in
456
+ this case, +association_ids+ will only include the ids of the comments provided by the
457
+ +comments+ method.
458
+
459
+ h3. Authorization Scope
460
+
461
+ By default, the authorization scope for serializers is +:current_user+. This means
462
+ that when you call +render json: @post+, the controller will automatically call
463
+ its +current_user+ method and pass that along to the serializer's initializer.
464
+
465
+ If you want to change that behavior, simply use the +serialization_scope+ class
466
+ method.
467
+
468
+ <pre lang="ruby">
469
+ class PostsController < ApplicationController
470
+ serialization_scope :current_app
471
+ end
472
+ </pre>
473
+
474
+ You can also implement an instance method called (no surprise) +serialization_scope+,
475
+ which allows you to define a dynamic authorization scope based on the current request.
476
+
477
+ WARNING: If you use different objects as authorization scopes, make sure that they all implement whatever interface you use in your serializers to control what the outputted JSON looks like.
478
+
479
+ h3. Using Serializers Outside of a Request
480
+
481
+ The serialization API encapsulates the concern of generating a JSON representation of
482
+ a particular model for a particular user. As a result, you should be able to easily use
483
+ serializers, whether you define them yourself or whether you use +ActiveModel::Serializer+
484
+ outside a request.
485
+
486
+ For instance, if you want to generate the JSON representation of a post for a user outside
487
+ of a request:
488
+
489
+ <pre lang="ruby">
490
+ user = get_user # some logic to get the user in question
491
+ PostSerializer.new(post, user).to_json # reliably generate JSON output
492
+ </pre>
493
+
494
+ If you want to generate JSON for an anonymous user, you should be able to use whatever
495
+ technique you use in your application to generate anonymous users outside of a request.
496
+ Typically, that means creating a new user and not saving it to the database:
497
+
498
+ <pre lang="ruby">
499
+ user = User.new # create a new anonymous user
500
+ PostSerializer.new(post, user).to_json
501
+ </pre>
502
+
503
+ In general, the better you encapsulate your authorization logic, the more easily you
504
+ will be able to use the serializer outside of the context of a request. For instance,
505
+ if you use an authorization library like Cancan, which uses a uniform +user.can?(action, model)+,
506
+ the authorization interface can very easily be replaced by a plain Ruby object for
507
+ testing or usage outside the context of a request.
508
+
509
+ h3. Collections
510
+
511
+ So far, we've talked about serializing individual model objects. By default, Rails
512
+ will serialize collections, including when using the +associations+ helper, by
513
+ looping over each element of the collection, calling +serializable_hash+ on the element,
514
+ and then grouping them by their type (using the plural version of their class name
515
+ as the root).
516
+
517
+ For example, an Array of post objects would serialize as:
518
+
519
+ <pre lang="json">
520
+ {
521
+ posts: [
522
+ {
523
+ title: "FIRST POST!",
524
+ body: "It's my first pooooost"
525
+ },
526
+ { title: "Second post!",
527
+ body: "Zomg I made it to my second post"
528
+ }
529
+ ]
530
+ }
531
+ </pre>
532
+
533
+ If you want to change the behavior of serialized Arrays, you need to create
534
+ a custom Array serializer.
535
+
536
+ <pre lang="ruby">
537
+ class ArraySerializer < ActiveModel::ArraySerializer
538
+ def serializable_array
539
+ serializers.map do |serializer|
540
+ serializer.serializable_hash
541
+ end
542
+ end
543
+
544
+ def as_json
545
+ hash = { root => serializable_array }
546
+ hash.merge!(associations)
547
+ hash
548
+ end
549
+ end
550
+ </pre>
551
+
552
+ When generating embedded associations using the +associations+ helper inside a
553
+ regular serializer, it will create a new <code>ArraySerializer</code> with the
554
+ associated content and call its +serializable_array+ method. In this case, those
555
+ embedded associations will not recursively include associations.
556
+
557
+ When generating an Array using +render json: posts+, the controller will invoke
558
+ the +as_json+ method, which will include its associations and its root.