active_model_serializers 0.8.3 → 0.10.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/ISSUE_TEMPLATE.md +29 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +105 -0
- data/.simplecov +110 -0
- data/.travis.yml +50 -24
- data/CHANGELOG.md +650 -6
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +105 -0
- data/Gemfile +69 -1
- data/{MIT-LICENSE.txt → MIT-LICENSE} +3 -2
- data/README.md +195 -545
- data/Rakefile +64 -8
- data/active_model_serializers.gemspec +62 -23
- data/appveyor.yml +28 -0
- data/bin/bench +171 -0
- data/bin/bench_regression +316 -0
- data/bin/rubocop +38 -0
- data/bin/serve_benchmark +39 -0
- data/docs/README.md +41 -0
- data/docs/STYLE.md +58 -0
- data/docs/general/adapters.md +269 -0
- data/docs/general/caching.md +58 -0
- data/docs/general/configuration_options.md +185 -0
- data/docs/general/deserialization.md +100 -0
- data/docs/general/fields.md +31 -0
- data/docs/general/getting_started.md +133 -0
- data/docs/general/instrumentation.md +40 -0
- data/docs/general/key_transforms.md +40 -0
- data/docs/general/logging.md +21 -0
- data/docs/general/rendering.md +293 -0
- data/docs/general/serializers.md +495 -0
- data/docs/how-open-source-maintained.jpg +0 -0
- data/docs/howto/add_pagination_links.md +138 -0
- data/docs/howto/add_relationship_links.md +140 -0
- data/docs/howto/add_root_key.md +62 -0
- data/docs/howto/grape_integration.md +42 -0
- data/docs/howto/outside_controller_use.md +66 -0
- data/docs/howto/passing_arbitrary_options.md +27 -0
- data/docs/howto/serialize_poro.md +73 -0
- data/docs/howto/test.md +154 -0
- data/docs/howto/upgrade_from_0_8_to_0_10.md +265 -0
- data/docs/integrations/ember-and-json-api.md +147 -0
- data/docs/integrations/grape.md +19 -0
- data/docs/jsonapi/errors.md +56 -0
- data/docs/jsonapi/schema/schema.json +366 -0
- data/docs/jsonapi/schema.md +151 -0
- data/docs/rfcs/0000-namespace.md +106 -0
- data/docs/rfcs/template.md +15 -0
- data/lib/action_controller/serialization.rb +43 -38
- data/lib/active_model/serializable_resource.rb +11 -0
- data/lib/active_model/serializer/adapter/attributes.rb +15 -0
- data/lib/active_model/serializer/adapter/base.rb +18 -0
- data/lib/active_model/serializer/adapter/json.rb +15 -0
- data/lib/active_model/serializer/adapter/json_api.rb +15 -0
- data/lib/active_model/serializer/adapter/null.rb +15 -0
- data/lib/active_model/serializer/adapter.rb +24 -0
- data/lib/active_model/serializer/array_serializer.rb +12 -0
- data/lib/active_model/serializer/association.rb +71 -0
- data/lib/active_model/serializer/attribute.rb +25 -0
- data/lib/active_model/serializer/belongs_to_reflection.rb +11 -0
- data/lib/active_model/serializer/collection_serializer.rb +88 -0
- data/lib/active_model/serializer/concerns/caching.rb +300 -0
- data/lib/active_model/serializer/error_serializer.rb +14 -0
- data/lib/active_model/serializer/errors_serializer.rb +32 -0
- data/lib/active_model/serializer/field.rb +90 -0
- data/lib/active_model/serializer/fieldset.rb +31 -0
- data/lib/active_model/serializer/has_many_reflection.rb +10 -0
- data/lib/active_model/serializer/has_one_reflection.rb +7 -0
- data/lib/active_model/serializer/lazy_association.rb +96 -0
- data/lib/active_model/serializer/link.rb +21 -0
- data/lib/active_model/serializer/lint.rb +150 -0
- data/lib/active_model/serializer/null.rb +17 -0
- data/lib/active_model/serializer/reflection.rb +210 -0
- data/lib/active_model/{serializers → serializer}/version.rb +1 -1
- data/lib/active_model/serializer.rb +343 -442
- data/lib/active_model_serializers/adapter/attributes.rb +13 -0
- data/lib/active_model_serializers/adapter/base.rb +83 -0
- data/lib/active_model_serializers/adapter/json.rb +21 -0
- data/lib/active_model_serializers/adapter/json_api/deserialization.rb +213 -0
- data/lib/active_model_serializers/adapter/json_api/error.rb +96 -0
- data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +49 -0
- data/lib/active_model_serializers/adapter/json_api/link.rb +83 -0
- data/lib/active_model_serializers/adapter/json_api/meta.rb +37 -0
- data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +88 -0
- data/lib/active_model_serializers/adapter/json_api/relationship.rb +104 -0
- data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +66 -0
- data/lib/active_model_serializers/adapter/json_api.rb +533 -0
- data/lib/active_model_serializers/adapter/null.rb +9 -0
- data/lib/active_model_serializers/adapter.rb +98 -0
- data/lib/active_model_serializers/callbacks.rb +55 -0
- data/lib/active_model_serializers/deprecate.rb +54 -0
- data/lib/active_model_serializers/deserialization.rb +15 -0
- data/lib/active_model_serializers/json_pointer.rb +14 -0
- data/lib/active_model_serializers/logging.rb +122 -0
- data/lib/active_model_serializers/lookup_chain.rb +80 -0
- data/lib/active_model_serializers/model.rb +130 -0
- data/lib/active_model_serializers/railtie.rb +50 -0
- data/lib/active_model_serializers/register_jsonapi_renderer.rb +78 -0
- data/lib/active_model_serializers/serializable_resource.rb +82 -0
- data/lib/active_model_serializers/serialization_context.rb +39 -0
- data/lib/active_model_serializers/test/schema.rb +138 -0
- data/lib/active_model_serializers/test/serializer.rb +125 -0
- data/lib/active_model_serializers/test.rb +7 -0
- data/lib/active_model_serializers.rb +47 -81
- data/lib/generators/rails/USAGE +6 -0
- data/lib/generators/rails/resource_override.rb +10 -0
- data/lib/generators/rails/serializer_generator.rb +36 -0
- data/lib/generators/rails/templates/serializer.rb.erb +8 -0
- data/lib/grape/active_model_serializers.rb +16 -0
- data/lib/grape/formatters/active_model_serializers.rb +32 -0
- data/lib/grape/helpers/active_model_serializers.rb +17 -0
- data/lib/tasks/rubocop.rake +53 -0
- data/test/action_controller/adapter_selector_test.rb +62 -0
- data/test/action_controller/explicit_serializer_test.rb +135 -0
- data/test/action_controller/json/include_test.rb +246 -0
- data/test/action_controller/json_api/deserialization_test.rb +112 -0
- data/test/action_controller/json_api/errors_test.rb +40 -0
- data/test/action_controller/json_api/fields_test.rb +66 -0
- data/test/action_controller/json_api/linked_test.rb +202 -0
- data/test/action_controller/json_api/pagination_test.rb +124 -0
- data/test/action_controller/json_api/transform_test.rb +189 -0
- data/test/action_controller/lookup_proc_test.rb +49 -0
- data/test/action_controller/namespace_lookup_test.rb +232 -0
- data/test/action_controller/serialization_scope_name_test.rb +235 -0
- data/test/action_controller/serialization_test.rb +478 -0
- data/test/active_model_serializers/adapter_for_test.rb +208 -0
- data/test/active_model_serializers/json_pointer_test.rb +22 -0
- data/test/active_model_serializers/logging_test.rb +77 -0
- data/test/active_model_serializers/model_test.rb +142 -0
- data/test/active_model_serializers/railtie_test_isolated.rb +68 -0
- data/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb +161 -0
- data/test/active_model_serializers/serialization_context_test_isolated.rb +71 -0
- data/test/active_model_serializers/test/schema_test.rb +131 -0
- data/test/active_model_serializers/test/serializer_test.rb +62 -0
- data/test/active_record_test.rb +9 -0
- data/test/adapter/attributes_test.rb +40 -0
- data/test/adapter/deprecation_test.rb +100 -0
- data/test/adapter/json/belongs_to_test.rb +45 -0
- data/test/adapter/json/collection_test.rb +104 -0
- data/test/adapter/json/has_many_test.rb +53 -0
- data/test/adapter/json/transform_test.rb +93 -0
- data/test/adapter/json_api/belongs_to_test.rb +155 -0
- data/test/adapter/json_api/collection_test.rb +96 -0
- data/test/adapter/json_api/errors_test.rb +76 -0
- data/test/adapter/json_api/fields_test.rb +96 -0
- data/test/adapter/json_api/has_many_explicit_serializer_test.rb +96 -0
- data/test/adapter/json_api/has_many_test.rb +173 -0
- data/test/adapter/json_api/has_one_test.rb +80 -0
- data/test/adapter/json_api/include_data_if_sideloaded_test.rb +213 -0
- data/test/adapter/json_api/json_api_test.rb +33 -0
- data/test/adapter/json_api/linked_test.rb +413 -0
- data/test/adapter/json_api/links_test.rb +110 -0
- data/test/adapter/json_api/pagination_links_test.rb +206 -0
- data/test/adapter/json_api/parse_test.rb +137 -0
- data/test/adapter/json_api/relationship_test.rb +397 -0
- data/test/adapter/json_api/resource_meta_test.rb +100 -0
- data/test/adapter/json_api/toplevel_jsonapi_test.rb +82 -0
- data/test/adapter/json_api/transform_test.rb +512 -0
- data/test/adapter/json_api/type_test.rb +193 -0
- data/test/adapter/json_test.rb +46 -0
- data/test/adapter/null_test.rb +22 -0
- data/test/adapter/polymorphic_test.rb +218 -0
- data/test/adapter_test.rb +67 -0
- data/test/array_serializer_test.rb +20 -73
- data/test/benchmark/app.rb +65 -0
- data/test/benchmark/benchmarking_support.rb +67 -0
- data/test/benchmark/bm_active_record.rb +81 -0
- data/test/benchmark/bm_adapter.rb +38 -0
- data/test/benchmark/bm_caching.rb +119 -0
- data/test/benchmark/bm_lookup_chain.rb +83 -0
- data/test/benchmark/bm_transform.rb +45 -0
- data/test/benchmark/config.ru +3 -0
- data/test/benchmark/controllers.rb +83 -0
- data/test/benchmark/fixtures.rb +219 -0
- data/test/cache_test.rb +651 -0
- data/test/collection_serializer_test.rb +127 -0
- data/test/fixtures/active_record.rb +113 -0
- data/test/fixtures/poro.rb +225 -0
- data/test/generators/scaffold_controller_generator_test.rb +24 -0
- data/test/generators/serializer_generator_test.rb +75 -0
- data/test/grape_test.rb +196 -0
- data/test/lint_test.rb +49 -0
- data/test/logger_test.rb +20 -0
- data/test/poro_test.rb +9 -0
- data/test/serializable_resource_test.rb +79 -0
- data/test/serializers/association_macros_test.rb +37 -0
- data/test/serializers/associations_test.rb +518 -0
- data/test/serializers/attribute_test.rb +153 -0
- data/test/serializers/attributes_test.rb +52 -0
- data/test/serializers/caching_configuration_test_isolated.rb +170 -0
- data/test/serializers/configuration_test.rb +32 -0
- data/test/serializers/fieldset_test.rb +14 -0
- data/test/serializers/meta_test.rb +202 -0
- data/test/serializers/options_test.rb +32 -0
- data/test/serializers/read_attribute_for_serialization_test.rb +79 -0
- data/test/serializers/reflection_test.rb +479 -0
- data/test/serializers/root_test.rb +21 -0
- data/test/serializers/serialization_test.rb +55 -0
- data/test/serializers/serializer_for_test.rb +136 -0
- data/test/serializers/serializer_for_with_namespace_test.rb +88 -0
- data/test/support/custom_schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
- data/test/support/isolated_unit.rb +84 -0
- data/test/support/rails5_shims.rb +53 -0
- data/test/support/rails_app.rb +38 -0
- data/test/support/schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
- data/test/support/schemas/active_model_serializers/test/schema_test/my/show.json +6 -0
- data/test/support/schemas/custom/show.json +7 -0
- data/test/support/schemas/hyper_schema.json +93 -0
- data/test/support/schemas/render_using_json_api.json +43 -0
- data/test/support/schemas/simple_json_pointers.json +10 -0
- data/test/support/serialization_testing.rb +79 -0
- data/test/test_helper.rb +59 -21
- metadata +529 -43
- data/DESIGN.textile +0 -586
- data/Gemfile.edge +0 -9
- data/bench/perf.rb +0 -43
- data/cruft.md +0 -19
- data/lib/active_model/array_serializer.rb +0 -104
- data/lib/active_model/serializer/associations.rb +0 -233
- data/lib/active_record/serializer_override.rb +0 -16
- data/lib/generators/resource_override.rb +0 -13
- data/lib/generators/serializer/USAGE +0 -9
- data/lib/generators/serializer/serializer_generator.rb +0 -42
- data/lib/generators/serializer/templates/serializer.rb +0 -19
- data/test/association_test.rb +0 -592
- data/test/caching_test.rb +0 -96
- data/test/generators_test.rb +0 -85
- data/test/no_serialization_scope_test.rb +0 -34
- data/test/serialization_scope_name_test.rb +0 -67
- data/test/serialization_test.rb +0 -392
- data/test/serializer_support_test.rb +0 -51
- data/test/serializer_test.rb +0 -1465
- data/test/test_fakes.rb +0 -217
data/DESIGN.textile
DELETED
@@ -1,586 +0,0 @@
|
|
1
|
-
<strong>This was the original design document for serializers.</strong> It is useful mostly for historical purposes as the public API has changed.
|
2
|
-
|
3
|
-
h2. Rails Serializers
|
4
|
-
|
5
|
-
This guide describes how to use Active Model serializers to build non-trivial JSON services in Rails. By reading this guide, you will learn:
|
6
|
-
|
7
|
-
* When to use the built-in Active Model serialization
|
8
|
-
* When to use a custom serializer for your models
|
9
|
-
* How to use serializers to encapsulate authorization concerns
|
10
|
-
* How to create serializer templates to describe the application-wide structure of your serialized JSON
|
11
|
-
* How to build resources not backed by a single database table for use with JSON services
|
12
|
-
|
13
|
-
This guide covers an intermediate topic and assumes familiarity with Rails conventions. It is suitable for applications that expose a
|
14
|
-
JSON API that may return different results based on the authorization status of the user.
|
15
|
-
|
16
|
-
h3. Serialization
|
17
|
-
|
18
|
-
By default, Active Record objects can serialize themselves into JSON by using the `to_json` method. This method takes a series of additional
|
19
|
-
parameter to control which properties and associations Rails should include in the serialized output.
|
20
|
-
|
21
|
-
When building a web application that uses JavaScript to retrieve JSON data from the server, this mechanism has historically been the primary
|
22
|
-
way that Rails developers prepared their responses. This works great for simple cases, as the logic for serializing an Active Record object
|
23
|
-
is neatly encapsulated in Active Record itself.
|
24
|
-
|
25
|
-
However, this solution quickly falls apart in the face of serialization requirements based on authorization. For instance, a web service
|
26
|
-
may choose to expose additional information about a resource only if the user is entitled to access it. In addition, a JavaScript front-end
|
27
|
-
may want information that is not neatly described in terms of serializing a single Active Record object, or in a different format than.
|
28
|
-
|
29
|
-
In addition, neither the controller nor the model seems like the correct place for logic that describes how to serialize an model object
|
30
|
-
*for the current user*.
|
31
|
-
|
32
|
-
Serializers solve these problems by encapsulating serialization in an object designed for this purpose. If the default +to_json+ semantics,
|
33
|
-
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
|
34
|
-
hash-driven-development in your controllers, juggling authorization logic and other concerns, serializers are for you!
|
35
|
-
|
36
|
-
h3. The Most Basic Serializer
|
37
|
-
|
38
|
-
A basic serializer is a simple Ruby object named after the model class it is serializing.
|
39
|
-
|
40
|
-
<pre lang="ruby">
|
41
|
-
class PostSerializer
|
42
|
-
def initialize(post, scope)
|
43
|
-
@post, @scope = post, scope
|
44
|
-
end
|
45
|
-
|
46
|
-
def as_json
|
47
|
-
{ post: { title: @post.name, body: @post.body } }
|
48
|
-
end
|
49
|
-
end
|
50
|
-
</pre>
|
51
|
-
|
52
|
-
A serializer is initialized with two parameters: the model object it should serialize and an authorization scope. By default, the
|
53
|
-
authorization scope is the current user (+current_user+) but you can use a different object if you want. The serializer also
|
54
|
-
implements an +as_json+ method, which returns a Hash that will be sent to the JSON encoder.
|
55
|
-
|
56
|
-
Rails will transparently use your serializer when you use +render :json+ in your controller.
|
57
|
-
|
58
|
-
<pre lang="ruby">
|
59
|
-
class PostsController < ApplicationController
|
60
|
-
def show
|
61
|
-
@post = Post.find(params[:id])
|
62
|
-
render json: @post
|
63
|
-
end
|
64
|
-
end
|
65
|
-
</pre>
|
66
|
-
|
67
|
-
Because +respond_with+ uses +render :json+ under the hood for JSON requests, Rails will automatically use your serializer when
|
68
|
-
you use +respond_with+ as well.
|
69
|
-
|
70
|
-
h4. +serializable_hash+
|
71
|
-
|
72
|
-
In general, you will want to implement +serializable_hash+ and +as_json+ to allow serializers to embed associated content
|
73
|
-
directly. The easiest way to implement these two methods is to have +as_json+ call +serializable_hash+ and insert the root.
|
74
|
-
|
75
|
-
<pre lang="ruby">
|
76
|
-
class PostSerializer
|
77
|
-
def initialize(post, scope)
|
78
|
-
@post, @scope = post, scope
|
79
|
-
end
|
80
|
-
|
81
|
-
def serializable_hash
|
82
|
-
{ title: @post.name, body: @post.body }
|
83
|
-
end
|
84
|
-
|
85
|
-
def as_json
|
86
|
-
{ post: serializable_hash }
|
87
|
-
end
|
88
|
-
end
|
89
|
-
</pre>
|
90
|
-
|
91
|
-
h4. Authorization
|
92
|
-
|
93
|
-
Let's update our serializer to include the email address of the author of the post, but only if the current user has superuser
|
94
|
-
access.
|
95
|
-
|
96
|
-
<pre lang="ruby">
|
97
|
-
class PostSerializer
|
98
|
-
def initialize(post, scope)
|
99
|
-
@post, @scope = post, scope
|
100
|
-
end
|
101
|
-
|
102
|
-
def as_json
|
103
|
-
{ post: serializable_hash }
|
104
|
-
end
|
105
|
-
|
106
|
-
def serializable_hash
|
107
|
-
hash = post
|
108
|
-
hash.merge!(super_data) if super?
|
109
|
-
hash
|
110
|
-
end
|
111
|
-
|
112
|
-
private
|
113
|
-
def post
|
114
|
-
{ title: @post.name, body: @post.body }
|
115
|
-
end
|
116
|
-
|
117
|
-
def super_data
|
118
|
-
{ email: @post.email }
|
119
|
-
end
|
120
|
-
|
121
|
-
def super?
|
122
|
-
@scope.superuser?
|
123
|
-
end
|
124
|
-
end
|
125
|
-
</pre>
|
126
|
-
|
127
|
-
h4. Testing
|
128
|
-
|
129
|
-
One benefit of encapsulating our objects this way is that it becomes extremely straight-forward to test the serialization
|
130
|
-
logic in isolation.
|
131
|
-
|
132
|
-
<pre lang="ruby">
|
133
|
-
require "ostruct"
|
134
|
-
|
135
|
-
class PostSerializerTest < ActiveSupport::TestCase
|
136
|
-
# For now, we use a very simple authorization structure. These tests will need
|
137
|
-
# refactoring if we change that.
|
138
|
-
plebe = OpenStruct.new(super?: false)
|
139
|
-
god = OpenStruct.new(super?: true)
|
140
|
-
|
141
|
-
post = OpenStruct.new(title: "Welcome to my blog!", body: "Blah blah blah", email: "tenderlove@gmail.com")
|
142
|
-
|
143
|
-
test "a regular user sees just the title and body" do
|
144
|
-
json = PostSerializer.new(post, plebe).to_json
|
145
|
-
hash = JSON.parse(json)
|
146
|
-
|
147
|
-
assert_equal post.title, hash.delete("title")
|
148
|
-
assert_equal post.body, hash.delete("body")
|
149
|
-
assert_empty hash
|
150
|
-
end
|
151
|
-
|
152
|
-
test "a superuser sees the title, body and email" do
|
153
|
-
json = PostSerializer.new(post, god).to_json
|
154
|
-
hash = JSON.parse(json)
|
155
|
-
|
156
|
-
assert_equal post.title, hash.delete("title")
|
157
|
-
assert_equal post.body, hash.delete("body")
|
158
|
-
assert_equal post.email, hash.delete("email")
|
159
|
-
assert_empty hash
|
160
|
-
end
|
161
|
-
end
|
162
|
-
</pre>
|
163
|
-
|
164
|
-
It's important to note that serializer objects define a clear interface specifically for serializing an existing object.
|
165
|
-
In this case, the serializer expects to receive a post object with +name+, +body+ and +email+ attributes and an authorization
|
166
|
-
scope with a +super?+ method.
|
167
|
-
|
168
|
-
By defining a clear interface, it's must easier to ensure that your authorization logic is behaving correctly. In this case,
|
169
|
-
the serializer doesn't need to concern itself with how the authorization scope decides whether to set the +super?+ flag, just
|
170
|
-
whether it is set. In general, you should document these requirements in your serializer files and programatically via tests.
|
171
|
-
The documentation library +YARD+ provides excellent tools for describing this kind of requirement:
|
172
|
-
|
173
|
-
<pre lang="ruby">
|
174
|
-
class PostSerializer
|
175
|
-
# @param [~body, ~title, ~email] post the post to serialize
|
176
|
-
# @param [~super] scope the authorization scope for this serializer
|
177
|
-
def initialize(post, scope)
|
178
|
-
@post, @scope = post, scope
|
179
|
-
end
|
180
|
-
|
181
|
-
# ...
|
182
|
-
end
|
183
|
-
</pre>
|
184
|
-
|
185
|
-
h3. Attribute Sugar
|
186
|
-
|
187
|
-
To simplify this process for a number of common cases, Rails provides a default superclass named +ActiveModel::Serializer+
|
188
|
-
that you can use to implement your serializers.
|
189
|
-
|
190
|
-
For example, you will sometimes want to simply include a number of existing attributes from the source model into the outputted
|
191
|
-
JSON. In the above example, the +title+ and +body+ attributes were always included in the JSON. Let's see how to use
|
192
|
-
+ActiveModel::Serializer+ to simplify our post serializer.
|
193
|
-
|
194
|
-
<pre lang="ruby">
|
195
|
-
class PostSerializer < ActiveModel::Serializer
|
196
|
-
attributes :title, :body
|
197
|
-
|
198
|
-
def serializable_hash
|
199
|
-
hash = attributes
|
200
|
-
hash.merge!(super_data) if super?
|
201
|
-
hash
|
202
|
-
end
|
203
|
-
|
204
|
-
private
|
205
|
-
def super_data
|
206
|
-
{ email: @post.email }
|
207
|
-
end
|
208
|
-
|
209
|
-
def super?
|
210
|
-
@scope.superuser?
|
211
|
-
end
|
212
|
-
end
|
213
|
-
</pre>
|
214
|
-
|
215
|
-
First, we specified the list of included attributes at the top of the class. This will create an instance method called
|
216
|
-
+attributes+ that extracts those attributes from the post model.
|
217
|
-
|
218
|
-
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.
|
219
|
-
|
220
|
-
Next, we use the attributes method in our +serializable_hash+ method, which allowed us to eliminate the +post+ method we hand-rolled
|
221
|
-
earlier. We could also eliminate the +as_json+ method, as +ActiveModel::Serializer+ provides a default +as_json+ method for
|
222
|
-
us that calls our +serializable_hash+ method and inserts a root. But we can go a step further!
|
223
|
-
|
224
|
-
<pre lang="ruby">
|
225
|
-
class PostSerializer < ActiveModel::Serializer
|
226
|
-
attributes :title, :body
|
227
|
-
|
228
|
-
private
|
229
|
-
def attributes
|
230
|
-
hash = super
|
231
|
-
hash.merge!(email: post.email) if super?
|
232
|
-
hash
|
233
|
-
end
|
234
|
-
|
235
|
-
def super?
|
236
|
-
@scope.superuser?
|
237
|
-
end
|
238
|
-
end
|
239
|
-
</pre>
|
240
|
-
|
241
|
-
The superclass provides a default +initialize+ method as well as a default +serializable_hash+ method, which uses
|
242
|
-
+attributes+. We can call +super+ to get the hash based on the attributes we declared, and then add in any additional
|
243
|
-
attributes we want to use.
|
244
|
-
|
245
|
-
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.
|
246
|
-
|
247
|
-
h3. Associations
|
248
|
-
|
249
|
-
In most JSON APIs, you will want to include associated objects with your serialized object. In this case, let's include
|
250
|
-
the comments with the current post.
|
251
|
-
|
252
|
-
<pre lang="ruby">
|
253
|
-
class PostSerializer < ActiveModel::Serializer
|
254
|
-
attributes :title, :body
|
255
|
-
has_many :comments
|
256
|
-
|
257
|
-
private
|
258
|
-
def attributes
|
259
|
-
hash = super
|
260
|
-
hash.merge!(email: post.email) if super?
|
261
|
-
hash
|
262
|
-
end
|
263
|
-
|
264
|
-
def super?
|
265
|
-
@scope.superuser?
|
266
|
-
end
|
267
|
-
end
|
268
|
-
</pre>
|
269
|
-
|
270
|
-
The default +serializable_hash+ method will include the comments as embedded objects inside the post.
|
271
|
-
|
272
|
-
<pre lang="json">
|
273
|
-
{
|
274
|
-
post: {
|
275
|
-
title: "Hello Blog!",
|
276
|
-
body: "This is my first post. Isn't it fabulous!",
|
277
|
-
comments: [
|
278
|
-
{
|
279
|
-
title: "Awesome",
|
280
|
-
body: "Your first post is great"
|
281
|
-
}
|
282
|
-
]
|
283
|
-
}
|
284
|
-
}
|
285
|
-
</pre>
|
286
|
-
|
287
|
-
Rails uses the same logic to generate embedded serializations as it does when you use +render :json+. In this case,
|
288
|
-
because you didn't define a +CommentSerializer+, Rails used the default +as_json+ on your comment object.
|
289
|
-
|
290
|
-
If you define a serializer, Rails will automatically instantiate it with the existing authorization scope.
|
291
|
-
|
292
|
-
<pre lang="ruby">
|
293
|
-
class CommentSerializer
|
294
|
-
def initialize(comment, scope)
|
295
|
-
@comment, @scope = comment, scope
|
296
|
-
end
|
297
|
-
|
298
|
-
def serializable_hash
|
299
|
-
{ title: @comment.title }
|
300
|
-
end
|
301
|
-
|
302
|
-
def as_json
|
303
|
-
{ comment: serializable_hash }
|
304
|
-
end
|
305
|
-
end
|
306
|
-
</pre>
|
307
|
-
|
308
|
-
If we define the above comment serializer, the outputted JSON will change to:
|
309
|
-
|
310
|
-
<pre lang="json">
|
311
|
-
{
|
312
|
-
post: {
|
313
|
-
title: "Hello Blog!",
|
314
|
-
body: "This is my first post. Isn't it fabulous!",
|
315
|
-
comments: [{ title: "Awesome" }]
|
316
|
-
}
|
317
|
-
}
|
318
|
-
</pre>
|
319
|
-
|
320
|
-
Let's imagine that our comment system allows an administrator to kill a comment, and we only want to allow
|
321
|
-
users to see the comments they're entitled to see. By default, +has_many :comments+ will simply use the
|
322
|
-
+comments+ accessor on the post object. We can override the +comments+ accessor to limit the comments used
|
323
|
-
to just the comments we want to allow for the current user.
|
324
|
-
|
325
|
-
<pre lang="ruby">
|
326
|
-
class PostSerializer < ActiveModel::Serializer
|
327
|
-
attributes :title. :body
|
328
|
-
has_many :comments
|
329
|
-
|
330
|
-
private
|
331
|
-
def attributes
|
332
|
-
hash = super
|
333
|
-
hash.merge!(email: post.email) if super?
|
334
|
-
hash
|
335
|
-
end
|
336
|
-
|
337
|
-
def comments
|
338
|
-
post.comments_for(scope)
|
339
|
-
end
|
340
|
-
|
341
|
-
def super?
|
342
|
-
@scope.superuser?
|
343
|
-
end
|
344
|
-
end
|
345
|
-
</pre>
|
346
|
-
|
347
|
-
+ActiveModel::Serializer+ will still embed the comments, but this time it will use just the comments
|
348
|
-
for the current user.
|
349
|
-
|
350
|
-
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.
|
351
|
-
|
352
|
-
h4. Modifying Associations
|
353
|
-
|
354
|
-
You can also rename associations if required. Say for example you have an association that
|
355
|
-
makes sense to be named one thing in your code, but another when data is serialized.
|
356
|
-
You can use the <code:key</code> option to specify a different name for an association.
|
357
|
-
Here is an example:
|
358
|
-
|
359
|
-
<pre lang="ruby">
|
360
|
-
class UserSerializer < ActiveModel::Serializer
|
361
|
-
has_many :followed_posts, :key => :posts
|
362
|
-
has_one :owned_account, :key => :account
|
363
|
-
end
|
364
|
-
</pre>
|
365
|
-
|
366
|
-
Using the <code>:key</code> without a <code>:serializer</code> option will use implicit detection
|
367
|
-
to determine a serializer. In this example, you'd have to define two classes: <code>PostSerializer</code>
|
368
|
-
and <code>AccountSerializer</code>. You can also add the <code>:serializer</code> option
|
369
|
-
to set it explicitly:
|
370
|
-
|
371
|
-
<pre lang="ruby">
|
372
|
-
class UserSerializer < ActiveModel::Serializer
|
373
|
-
has_many :followed_posts, :key => :posts, :serializer => CustomPostSerializer
|
374
|
-
has_one :owne_account, :key => :account, :serializer => PrivateAccountSerializer
|
375
|
-
end
|
376
|
-
</pre>
|
377
|
-
|
378
|
-
h3. Customizing Associations
|
379
|
-
|
380
|
-
Not all front-ends expect embedded documents in the same form. In these cases, you can override the
|
381
|
-
default +serializable_hash+, and use conveniences provided by +ActiveModel::Serializer+ to avoid having to
|
382
|
-
build up the hash manually.
|
383
|
-
|
384
|
-
For example, let's say our front-end expects the posts and comments in the following format:
|
385
|
-
|
386
|
-
<pre lang="json">
|
387
|
-
{
|
388
|
-
post: {
|
389
|
-
id: 1
|
390
|
-
title: "Hello Blog!",
|
391
|
-
body: "This is my first post. Isn't it fabulous!",
|
392
|
-
comments: [1,2]
|
393
|
-
},
|
394
|
-
comments: [
|
395
|
-
{
|
396
|
-
id: 1
|
397
|
-
title: "Awesome",
|
398
|
-
body: "Your first post is great"
|
399
|
-
},
|
400
|
-
{
|
401
|
-
id: 2
|
402
|
-
title: "Not so awesome",
|
403
|
-
body: "Why is it so short!"
|
404
|
-
}
|
405
|
-
]
|
406
|
-
}
|
407
|
-
</pre>
|
408
|
-
|
409
|
-
We could achieve this with a custom +as_json+ method. We will also need to define a serializer for comments.
|
410
|
-
|
411
|
-
<pre lang="ruby">
|
412
|
-
class CommentSerializer < ActiveModel::Serializer
|
413
|
-
attributes :id, :title, :body
|
414
|
-
|
415
|
-
# define any logic for dealing with authorization-based attributes here
|
416
|
-
end
|
417
|
-
|
418
|
-
class PostSerializer < ActiveModel::Serializer
|
419
|
-
attributes :title, :body
|
420
|
-
has_many :comments
|
421
|
-
|
422
|
-
def as_json
|
423
|
-
{ post: serializable_hash }.merge!(associations)
|
424
|
-
end
|
425
|
-
|
426
|
-
def serializable_hash
|
427
|
-
post_hash = attributes
|
428
|
-
post_hash.merge!(association_ids)
|
429
|
-
post_hash
|
430
|
-
end
|
431
|
-
|
432
|
-
private
|
433
|
-
def attributes
|
434
|
-
hash = super
|
435
|
-
hash.merge!(email: post.email) if super?
|
436
|
-
hash
|
437
|
-
end
|
438
|
-
|
439
|
-
def comments
|
440
|
-
post.comments_for(scope)
|
441
|
-
end
|
442
|
-
|
443
|
-
def super?
|
444
|
-
@scope.superuser?
|
445
|
-
end
|
446
|
-
end
|
447
|
-
</pre>
|
448
|
-
|
449
|
-
Here, we used two convenience methods: +associations+ and +association_ids+. The first,
|
450
|
-
+associations+, creates a hash of all of the define associations, using their defined
|
451
|
-
serializers. The second, +association_ids+, generates a hash whose key is the association
|
452
|
-
name and whose value is an Array of the association's keys.
|
453
|
-
|
454
|
-
The +association_ids+ helper will use the overridden version of the association, so in
|
455
|
-
this case, +association_ids+ will only include the ids of the comments provided by the
|
456
|
-
+comments+ method.
|
457
|
-
|
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
|
488
|
-
|
489
|
-
By default, the authorization scope for serializers is +:current_user+. This means
|
490
|
-
that when you call +render json: @post+, the controller will automatically call
|
491
|
-
its +current_user+ method and pass that along to the serializer's initializer.
|
492
|
-
|
493
|
-
If you want to change that behavior, simply use the +serialization_scope+ class
|
494
|
-
method.
|
495
|
-
|
496
|
-
<pre lang="ruby">
|
497
|
-
class PostsController < ApplicationController
|
498
|
-
serialization_scope :current_app
|
499
|
-
end
|
500
|
-
</pre>
|
501
|
-
|
502
|
-
You can also implement an instance method called (no surprise) +serialization_scope+,
|
503
|
-
which allows you to define a dynamic authorization scope based on the current request.
|
504
|
-
|
505
|
-
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.
|
506
|
-
|
507
|
-
h3. Using Serializers Outside of a Request
|
508
|
-
|
509
|
-
The serialization API encapsulates the concern of generating a JSON representation of
|
510
|
-
a particular model for a particular user. As a result, you should be able to easily use
|
511
|
-
serializers, whether you define them yourself or whether you use +ActiveModel::Serializer+
|
512
|
-
outside a request.
|
513
|
-
|
514
|
-
For instance, if you want to generate the JSON representation of a post for a user outside
|
515
|
-
of a request:
|
516
|
-
|
517
|
-
<pre lang="ruby">
|
518
|
-
user = get_user # some logic to get the user in question
|
519
|
-
PostSerializer.new(post, user).to_json # reliably generate JSON output
|
520
|
-
</pre>
|
521
|
-
|
522
|
-
If you want to generate JSON for an anonymous user, you should be able to use whatever
|
523
|
-
technique you use in your application to generate anonymous users outside of a request.
|
524
|
-
Typically, that means creating a new user and not saving it to the database:
|
525
|
-
|
526
|
-
<pre lang="ruby">
|
527
|
-
user = User.new # create a new anonymous user
|
528
|
-
PostSerializer.new(post, user).to_json
|
529
|
-
</pre>
|
530
|
-
|
531
|
-
In general, the better you encapsulate your authorization logic, the more easily you
|
532
|
-
will be able to use the serializer outside of the context of a request. For instance,
|
533
|
-
if you use an authorization library like Cancan, which uses a uniform +user.can?(action, model)+,
|
534
|
-
the authorization interface can very easily be replaced by a plain Ruby object for
|
535
|
-
testing or usage outside the context of a request.
|
536
|
-
|
537
|
-
h3. Collections
|
538
|
-
|
539
|
-
So far, we've talked about serializing individual model objects. By default, Rails
|
540
|
-
will serialize collections, including when using the +associations+ helper, by
|
541
|
-
looping over each element of the collection, calling +serializable_hash+ on the element,
|
542
|
-
and then grouping them by their type (using the plural version of their class name
|
543
|
-
as the root).
|
544
|
-
|
545
|
-
For example, an Array of post objects would serialize as:
|
546
|
-
|
547
|
-
<pre lang="json">
|
548
|
-
{
|
549
|
-
posts: [
|
550
|
-
{
|
551
|
-
title: "FIRST POST!",
|
552
|
-
body: "It's my first pooooost"
|
553
|
-
},
|
554
|
-
{ title: "Second post!",
|
555
|
-
body: "Zomg I made it to my second post"
|
556
|
-
}
|
557
|
-
]
|
558
|
-
}
|
559
|
-
</pre>
|
560
|
-
|
561
|
-
If you want to change the behavior of serialized Arrays, you need to create
|
562
|
-
a custom Array serializer.
|
563
|
-
|
564
|
-
<pre lang="ruby">
|
565
|
-
class ArraySerializer < ActiveModel::ArraySerializer
|
566
|
-
def serializable_array
|
567
|
-
serializers.map do |serializer|
|
568
|
-
serializer.serializable_hash
|
569
|
-
end
|
570
|
-
end
|
571
|
-
|
572
|
-
def as_json
|
573
|
-
hash = { root => serializable_array }
|
574
|
-
hash.merge!(associations)
|
575
|
-
hash
|
576
|
-
end
|
577
|
-
end
|
578
|
-
</pre>
|
579
|
-
|
580
|
-
When generating embedded associations using the +associations+ helper inside a
|
581
|
-
regular serializer, it will create a new <code>ArraySerializer</code> with the
|
582
|
-
associated content and call its +serializable_array+ method. In this case, those
|
583
|
-
embedded associations will not recursively include associations.
|
584
|
-
|
585
|
-
When generating an Array using +render json: posts+, the controller will invoke
|
586
|
-
the +as_json+ method, which will include its associations and its root.
|
data/Gemfile.edge
DELETED
data/bench/perf.rb
DELETED
@@ -1,43 +0,0 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
require "bundler/setup"
|
3
|
-
require "active_model_serializers"
|
4
|
-
require "active_support/json"
|
5
|
-
require "benchmark"
|
6
|
-
|
7
|
-
class User < Struct.new(:id,:name,:age,:about)
|
8
|
-
include ActiveModel::SerializerSupport
|
9
|
-
|
10
|
-
def fast_hash
|
11
|
-
h = {
|
12
|
-
id: read_attribute_for_serialization(:id),
|
13
|
-
name: read_attribute_for_serialization(:name),
|
14
|
-
about: read_attribute_for_serialization(:about)
|
15
|
-
}
|
16
|
-
h[:age] = read_attribute_for_serialization(:age) if age > 18
|
17
|
-
h
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
class UserSerializer < ActiveModel::Serializer
|
22
|
-
attributes :id, :name, :age, :about
|
23
|
-
|
24
|
-
def include_age?
|
25
|
-
object.age > 18
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
u = User.new(1, "sam", 10, "about")
|
32
|
-
s = UserSerializer.new(u)
|
33
|
-
|
34
|
-
n = 100000
|
35
|
-
|
36
|
-
Benchmark.bmbm {|x|
|
37
|
-
x.report("init") { n.times { UserSerializer.new(u) } }
|
38
|
-
x.report("fast_hash") { n.times { u.fast_hash } }
|
39
|
-
x.report("attributes") { n.times { UserSerializer.new(u).attributes } }
|
40
|
-
x.report("serializable_hash") { n.times { UserSerializer.new(u).serializable_hash } }
|
41
|
-
}
|
42
|
-
|
43
|
-
|
data/cruft.md
DELETED
@@ -1,19 +0,0 @@
|
|
1
|
-
As of Ruby 1.9.3, it is impossible to dynamically generate a Symbol
|
2
|
-
through interpolation without generating garbage. Theoretically, Ruby
|
3
|
-
should be able to take care of this by building up the String in C and
|
4
|
-
interning the C String.
|
5
|
-
|
6
|
-
Because of this, we avoid generating dynamic Symbols at runtime. For
|
7
|
-
example, instead of generating the instrumentation event dynamically, we
|
8
|
-
have a constant with a Hash of events:
|
9
|
-
|
10
|
-
```ruby
|
11
|
-
INSTRUMENT = {
|
12
|
-
:serialize => :"serialize.serializer",
|
13
|
-
:associations => :"associations.serializer"
|
14
|
-
}
|
15
|
-
```
|
16
|
-
|
17
|
-
If Ruby ever fixes this issue and avoids generating garbage with dynamic
|
18
|
-
symbols, this code can be removed.
|
19
|
-
|