serega 0.42.0 → 0.50.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.
- checksums.yaml +4 -4
- data/README.md +124 -1207
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +186 -131
- data/lib/serega/attribute_normalizer.rb +164 -345
- data/lib/serega/attribute_normalizers/batch.rb +45 -0
- data/lib/serega/attribute_normalizers/block.rb +81 -0
- data/lib/serega/attribute_normalizers/delegate.rb +34 -0
- data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
- data/lib/serega/attribute_normalizers/preloads.rb +46 -0
- data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
- data/lib/serega/attribute_value_resolvers/const.rb +2 -24
- data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
- data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
- data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
- data/lib/serega/attribute_values.rb +46 -0
- data/lib/serega/batch_loader.rb +37 -0
- data/lib/serega/conditions.rb +56 -0
- data/lib/serega/config.rb +105 -144
- data/lib/serega/engine/pulls.rb +64 -0
- data/lib/serega/engine/run.rb +106 -0
- data/lib/serega/errors.rb +36 -8
- data/lib/serega/formatter.rb +98 -0
- data/lib/serega/plan/cache.rb +96 -0
- data/lib/serega/plan/per_ractor_cache.rb +23 -0
- data/lib/serega/plan.rb +101 -128
- data/lib/serega/plan_point.rb +44 -151
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
- data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
- data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
- data/lib/serega/plugins/metadata/check_params.rb +80 -0
- data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
- data/lib/serega/plugins/metadata/metadata.rb +40 -168
- data/lib/serega/plugins/root/root.rb +91 -235
- data/lib/serega/plugins.rb +16 -49
- data/lib/serega/presenter.rb +61 -19
- data/lib/serega/result_builder.rb +68 -0
- data/lib/serega/result_code.rb +203 -0
- data/lib/serega/shareable.rb +52 -0
- data/lib/serega/source_group.rb +187 -0
- data/lib/serega/utils/collection_detector.rb +19 -13
- data/lib/serega/utils/deep_merge.rb +26 -0
- data/lib/serega/utils/enum_deep_dup.rb +10 -16
- data/lib/serega/utils/enum_deep_freeze.rb +8 -14
- data/lib/serega/utils/method_signature.rb +6 -15
- data/lib/serega/utils/parse_string_modifiers.rb +63 -0
- data/lib/serega/utils/ractor_local.rb +36 -0
- data/lib/serega/utils/serialized_attribute_error.rb +60 -20
- data/lib/serega/utils/to_hash.rb +5 -26
- data/lib/serega/validations/attribute/check_block.rb +3 -15
- data/lib/serega/validations/attribute/check_name.rb +4 -15
- data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
- data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
- data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
- data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
- data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
- data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
- data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
- data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
- data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
- data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
- data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
- data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
- data/lib/serega/validations/check_attribute_params.rb +92 -71
- data/lib/serega/validations/check_batch_loader_params.rb +43 -62
- data/lib/serega/validations/check_formatter.rb +80 -0
- data/lib/serega/validations/check_serialize_keywords.rb +23 -0
- data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
- data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
- data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
- data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
- data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
- data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
- data/lib/serega/version.rb +1 -4
- data/lib/serega.rb +459 -388
- data/sig/serega/plugins.rbs +49 -0
- data/sig/serega.rbs +142 -0
- metadata +42 -38
- data/lib/serega/data_builder.rb +0 -54
- data/lib/serega/engine/level.rb +0 -59
- data/lib/serega/engine/level_queue.rb +0 -56
- data/lib/serega/engine/loader.rb +0 -70
- data/lib/serega/helpers/serializer_class_helper.rb +0 -19
- data/lib/serega/object_serializer.rb +0 -125
- data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
- data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
- data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
- data/lib/serega/plugins/formatters/formatters.rb +0 -322
- data/lib/serega/plugins/if/if.rb +0 -287
- data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
- data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
- data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
- data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
- data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
- data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
- data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
- data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
- data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
- data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
- data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
- data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
- data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
- data/lib/serega/utils/symbol_name.rb +0 -39
- data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
- data/lib/serega/validations/check_initiate_params.rb +0 -57
- data/lib/serega/validations/check_serialize_params.rb +0 -54
data/README.md
CHANGED
|
@@ -1,1307 +1,224 @@
|
|
|
1
|
-
# Serega
|
|
1
|
+
# Serega
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
📌 Serega does not depend on any gem and works with any framework
|
|
13
|
-
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
It has some great features:
|
|
3
|
+
<!-- TODO: logo (SVG, light and dark variants)
|
|
4
|
+
<p align="center">
|
|
5
|
+
<picture>
|
|
6
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
|
|
7
|
+
<img src="assets/logo.svg" alt="Serega" width="240">
|
|
8
|
+
</picture>
|
|
9
|
+
</p>
|
|
10
|
+
-->
|
|
17
11
|
|
|
18
|
-
|
|
19
|
-
- Secure from malicious queries with [depth_limit][depth_limit] plugin
|
|
20
|
-
- Solutions for N+1 problem (via built-in [batch loading](#batch-loading), [preloads][preloads] or
|
|
21
|
-
[activerecord_preloads][activerecord_preloads] plugin)
|
|
22
|
-
- Load serialized objects by ids ([prepare_initial_objects](#prepare-initial-objects))
|
|
23
|
-
- Built-in object presenter ([presenter][presenter])
|
|
24
|
-
- Adding custom metadata (via [metadata][metadata] or
|
|
25
|
-
[context_metadata][context_metadata] plugins)
|
|
26
|
-
- Value formatters ([formatters][formatters] plugin) helps to transform
|
|
27
|
-
time, date, money, percentage, and any other values in the same way
|
|
28
|
-
keeping the code dry
|
|
29
|
-
- Conditional attributes - ([if][if] plugin)
|
|
30
|
-
- Auto camelCase keys - [camel_case][camel_case] plugin
|
|
31
|
-
- Serializing Hash records - [hash_access][hash_access] attribute option
|
|
32
|
-
|
|
33
|
-
## Table of Contents
|
|
34
|
-
|
|
35
|
-
<!-- toc -->
|
|
36
|
-
|
|
37
|
-
- [Installation](#installation)
|
|
38
|
-
- [Usage](#usage)
|
|
39
|
-
- [Define serializers](#define-serializers)
|
|
40
|
-
- [Adding attributes](#adding-attributes)
|
|
41
|
-
- [Defining a nested serializer with a block](#defining-a-nested-serializer-with-a-block)
|
|
42
|
-
- [Serializing](#serializing)
|
|
43
|
-
- [Selecting Fields](#selecting-fields)
|
|
44
|
-
- [Using Context](#using-context)
|
|
45
|
-
- [Batch Loading](#batch-loading)
|
|
46
|
-
- [Prepare Initial Objects](#prepare-initial-objects)
|
|
47
|
-
- [Configuration](#configuration)
|
|
48
|
-
- [Preloads](#preloads)
|
|
49
|
-
- [Serializing the same object in association](#serializing-the-same-object-in-association)
|
|
50
|
-
- [Custom preloading](#custom-preloading)
|
|
51
|
-
- [Serializing Hash records](#serializing-hash-records)
|
|
52
|
-
- [Presenter](#presenter)
|
|
53
|
-
- [Plugins](#plugins)
|
|
54
|
-
- [Plugin :activerecord_preloads](#plugin-activerecord_preloads)
|
|
55
|
-
- [Plugin :root](#plugin-root)
|
|
56
|
-
- [Plugin :metadata](#plugin-metadata)
|
|
57
|
-
- [Plugin :context_metadata](#plugin-context_metadata)
|
|
58
|
-
- [Plugin :formatters](#plugin-formatters)
|
|
59
|
-
- [Plugin :string_modifiers](#plugin-string_modifiers)
|
|
60
|
-
- [Plugin :if](#plugin-if)
|
|
61
|
-
- [Plugin :camel_case](#plugin-camel_case)
|
|
62
|
-
- [Plugin :depth_limit](#plugin-depth_limit)
|
|
63
|
-
- [Plugin :explicit_many_option](#plugin-explicit_many_option)
|
|
64
|
-
- [Errors](#errors)
|
|
65
|
-
- [Release](#release)
|
|
66
|
-
- [Development](#development)
|
|
67
|
-
- [Contributing](#contributing)
|
|
68
|
-
- [License](#license)
|
|
69
|
-
|
|
70
|
-
<!-- tocstop -->
|
|
71
|
-
|
|
72
|
-
## Installation
|
|
12
|
+
**Serialize exactly what was asked. Load exactly what is needed.**
|
|
73
13
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
14
|
+
[](https://badge.fury.io/rb/serega)
|
|
15
|
+
[![GitHub Actions][build-badge]][build]
|
|
16
|
+

|
|
17
|
+

|
|
18
|
+

|
|
77
19
|
|
|
78
|
-
|
|
20
|
+
Serega is a Ruby serializer that turns your objects into Hashes ready for JSON.
|
|
21
|
+
Clients choose the fields they need, and Serega loads only the data those
|
|
22
|
+
fields use — in a few queries, with no N+1.
|
|
79
23
|
|
|
80
|
-
|
|
81
|
-
|
|
24
|
+
- ⚡ **Fast** — serialization code is generated per field selection and cached.
|
|
25
|
+
[See benchmarks](#benchmarks).
|
|
26
|
+
- 🎯 **Clients choose fields** — `?fields=name,posts(title)` works out of the
|
|
27
|
+
box, nested at any depth.
|
|
28
|
+
- 🧺 **No N+1** — associations are preloaded and batch loaders run once per
|
|
29
|
+
relation, only for the requested fields.
|
|
30
|
+
- 🪶 **No dependencies** — plain Ruby, works with Rails, Hanami, Roda, Sinatra
|
|
31
|
+
or no framework at all.
|
|
82
32
|
|
|
83
|
-
|
|
84
|
-
superclasses.
|
|
33
|
+
## Show me
|
|
85
34
|
|
|
86
35
|
```ruby
|
|
87
36
|
class AppSerializer < Serega
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
# config.one = :one
|
|
92
|
-
# config.two = :two
|
|
37
|
+
plugin :activerecord_preloads # preloads associations used by requested fields
|
|
38
|
+
config.auto_preload = true
|
|
39
|
+
config.hide_by_default = :auto # fields that need extra queries are sent on request
|
|
93
40
|
end
|
|
94
41
|
|
|
95
42
|
class UserSerializer < AppSerializer
|
|
96
|
-
|
|
97
|
-
# attribute :two
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
class CommentSerializer < AppSerializer
|
|
101
|
-
# attribute :one
|
|
102
|
-
# attribute :two
|
|
103
|
-
end
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Adding attributes
|
|
107
|
-
|
|
108
|
-
⚠️ Attribute names are checked to include only "a-z", "A-Z", "0-9", "\_", "-",
|
|
109
|
-
"~" characters.
|
|
110
|
-
|
|
111
|
-
We allow ONLY these characters as we want to be able to use attribute names in
|
|
112
|
-
URLs without escaping.
|
|
113
|
-
|
|
114
|
-
This rule can be turned off with simple config option: `config.check_attribute_name = false`
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
class UserSerializer < Serega
|
|
118
|
-
# Regular attribute
|
|
119
|
-
attribute :first_name
|
|
120
|
-
|
|
121
|
-
# Option :method specifies the name used to read the attribute's value —
|
|
122
|
-
# a method called on the serialized object by default, or a Hash key when
|
|
123
|
-
# combined with :hash_access
|
|
124
|
-
attribute :first_name, method: :old_first_name
|
|
125
|
-
|
|
126
|
-
# Attribute blocks below require a base serializer for nested serializers.
|
|
127
|
-
# See the "Defining a nested serializer with a block" section below.
|
|
128
|
-
config.base_serializer = Serega
|
|
129
|
-
|
|
130
|
-
# Method :itself is handy to serialize the same object with a nested set of
|
|
131
|
-
# attributes. Note: with a custom presenter the serialized value will be
|
|
132
|
-
# the presenter instance itself; use `method: :__getobj__` to serialize the
|
|
133
|
-
# original unwrapped object instead.
|
|
134
|
-
attribute :statistics, method: :itself do
|
|
135
|
-
attribute :likes_count
|
|
136
|
-
attribute :comments_count
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Block defines a nested serializer for the attribute value.
|
|
140
|
-
# See the "Defining a nested serializer with a block" section below.
|
|
141
|
-
attribute :author do
|
|
142
|
-
attribute :name
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Option :value can be used with a Proc or callable object to define
|
|
146
|
-
# attribute value
|
|
147
|
-
attribute :first_name, value: UserProfile.new # must have #call method
|
|
148
|
-
attribute :first_name, value: proc { |user| user.profile&.first_name }
|
|
149
|
-
|
|
150
|
-
# Option :delegate can be used to define attribute value.
|
|
151
|
-
# Sub-option :allow_nil by default is false
|
|
152
|
-
attribute :first_name, delegate: { to: :profile, allow_nil: true }
|
|
153
|
-
|
|
154
|
-
# Option :delegate can be used with :method sub-option, so method chain here
|
|
155
|
-
# is user.profile.fname
|
|
156
|
-
attribute :first_name, delegate: { to: :profile, method: :fname }
|
|
157
|
-
|
|
158
|
-
# Option :default can be used to replace possible nil values.
|
|
159
|
-
attribute :first_name, default: ''
|
|
160
|
-
attribute :is_active, default: false
|
|
161
|
-
attribute :comments_count, default: 0
|
|
162
|
-
|
|
163
|
-
# Option :const specifies attribute with a specific constant value
|
|
164
|
-
attribute(:type, const: 'user')
|
|
165
|
-
|
|
166
|
-
# Option :hide specifies attributes that should not be serialized by default
|
|
167
|
-
attribute :tags, hide: true
|
|
168
|
-
|
|
169
|
-
# Option :serializer specifies nested serializer for attribute
|
|
170
|
-
# We can define the `:serializer` value as a Class, String, or Proc.
|
|
171
|
-
# Use String or Proc if you have cross-references in serializers.
|
|
172
|
-
attribute :posts, serializer: PostSerializer
|
|
43
|
+
attribute :name
|
|
173
44
|
attribute :posts, serializer: "PostSerializer"
|
|
174
|
-
attribute :posts, serializer: -> { PostSerializer }
|
|
175
|
-
|
|
176
|
-
# Option `:many` specifies a has_many relationship. It is optional.
|
|
177
|
-
# If not specified, it is defined during serialization by checking
|
|
178
|
-
# `object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)`
|
|
179
|
-
# Also the `:many` changes the default value from `nil` to `[]`.
|
|
180
|
-
attribute :posts, serializer: PostSerializer, many: true
|
|
181
|
-
|
|
182
|
-
# Option `:preload` allows to specify associations to preload to
|
|
183
|
-
# attribute value
|
|
184
|
-
attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
|
|
185
|
-
|
|
186
|
-
# Options `:if, :unless, :if_value and :unless_value` can be specified
|
|
187
|
-
# when `:if` plugin is enabled. They hide the attribute key and value from the
|
|
188
|
-
# response.
|
|
189
|
-
# See more usage examples in the `:if` plugin section.
|
|
190
|
-
attribute :email, if: proc { |user, ctx| user == ctx[:current_user] }
|
|
191
|
-
attribute :email, if_value: :present?
|
|
192
|
-
|
|
193
|
-
# Option `:format` can be specified when enabled `:formatters` plugin
|
|
194
|
-
# It changes the attribute value
|
|
195
|
-
attribute :created_at, format: :iso_time
|
|
196
|
-
attribute :updated_at, format: :iso_time
|
|
197
|
-
|
|
198
|
-
# Option `:format` also can be used as Proc
|
|
199
|
-
attribute :created_at, format: proc { |time| time.strftime("%Y-%m-%d")}
|
|
200
|
-
end
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### Defining a nested serializer with a block
|
|
204
|
-
|
|
205
|
-
An attribute block defines an anonymous nested serializer. The attribute value
|
|
206
|
-
is found as usual — by the attribute name or the `:method`, `:value`,
|
|
207
|
-
`:delegate`, `:batch` option — and is serialized by this nested serializer.
|
|
208
|
-
|
|
209
|
-
The nested serializer is a regular subclass of an explicitly chosen **base
|
|
210
|
-
serializer** — usually a settings-only serializer holding your plugins and
|
|
211
|
-
configuration. Choose it with the `base_serializer:` attribute option or the
|
|
212
|
-
`config.base_serializer` setting (attribute option wins; an error is raised
|
|
213
|
-
when none is set):
|
|
214
|
-
|
|
215
|
-
```ruby
|
|
216
|
-
class AppSerializer < Serega
|
|
217
|
-
plugin :activerecord_preloads
|
|
218
|
-
|
|
219
|
-
# Nested serializers defined with attribute blocks will inherit from
|
|
220
|
-
# AppSerializer
|
|
221
|
-
config.base_serializer = self
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
class UserSerializer < AppSerializer
|
|
225
|
-
attribute :first_name
|
|
226
|
-
|
|
227
|
-
# Serializes user.author with a nested serializer inherited from AppSerializer
|
|
228
|
-
attribute :author do
|
|
229
|
-
attribute :name
|
|
230
|
-
end
|
|
231
|
-
|
|
232
|
-
# Serializes the user itself, exposing only counters
|
|
233
|
-
attribute :statistics, method: :itself, base_serializer: StatsBaseSerializer do
|
|
234
|
-
attribute :likes_count
|
|
235
|
-
attribute :comments_count
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
UserSerializer.to_h(user)
|
|
240
|
-
# => {first_name: "Bruce", author: {name: "Bob"}, statistics: {likes_count: 10, comments_count: 3}}
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
The nested serializer inherits everything the base serializer has — plugins,
|
|
244
|
-
config, attributes, batch loaders, the preload handler — through the regular
|
|
245
|
-
inheritance mechanism. Any serializer can be used as a base: if it has
|
|
246
|
-
attributes, they are serialized by the nested serializer too, together with
|
|
247
|
-
the attributes defined in the block. Anything the base does not provide can
|
|
248
|
-
be declared inside the block:
|
|
249
|
-
|
|
250
|
-
```ruby
|
|
251
|
-
attribute :statistics, method: :itself do
|
|
252
|
-
batch(:stats) { |users| Stats.for_users(users) } # => { user_id => stat }
|
|
253
|
-
|
|
254
|
-
attribute :likes_count, batch: :stats, value: proc { |user, batches:| batches[:stats][user.id].likes }
|
|
255
|
-
end
|
|
256
|
-
```
|
|
257
|
-
|
|
258
|
-
Things to keep in mind:
|
|
259
|
-
|
|
260
|
-
- The nested serializer is created at the moment the attribute is defined.
|
|
261
|
-
Changes made to the base serializer later do not affect already defined
|
|
262
|
-
nested serializers.
|
|
263
|
-
- Errors raised while serializing nested attributes are reported with a
|
|
264
|
-
readable serializer label, for example:
|
|
265
|
-
`(when serializing 'comments_count' attribute in UserSerializer.<statistics>)`.
|
|
266
|
-
|
|
267
|
-
### Serializing
|
|
268
|
-
|
|
269
|
-
We can serialize objects using class method `.call` aliased as `.to_h` and
|
|
270
|
-
same instance methods `#call` and its alias `#to_h`.
|
|
271
|
-
|
|
272
|
-
```ruby
|
|
273
|
-
user = OpenStruct.new(username: 'serega')
|
|
274
|
-
|
|
275
|
-
class UserSerializer < Serega
|
|
276
|
-
attribute :username
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
UserSerializer.to_h(user) # => {username: "serega"}
|
|
280
|
-
UserSerializer.to_h([user]) # => [{username: "serega"}]
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
Use `.to_data` / `#to_data` to get the same result as Ruby `Data` objects
|
|
284
|
-
(immutable value objects, Ruby 3.2+). Nested serialized relations are also
|
|
285
|
-
converted to `Data` objects.
|
|
286
|
-
|
|
287
|
-
```ruby
|
|
288
|
-
result = UserSerializer.to_data(user)
|
|
289
|
-
result # => #<data username="serega">
|
|
290
|
-
result.username # => "serega"
|
|
291
|
-
|
|
292
|
-
UserSerializer.to_data([user]) # => [#<data username="serega">]
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
If serialized fields are constant, then it's a good idea to initiate the
|
|
296
|
-
serializer and reuse it.
|
|
297
|
-
It will be a bit faster (the serialization plan will be prepared only once).
|
|
298
|
-
|
|
299
|
-
```ruby
|
|
300
|
-
# Example with all fields
|
|
301
|
-
serializer = UserSerializer.new
|
|
302
|
-
serializer.to_h(user1)
|
|
303
|
-
serializer.to_h(user2)
|
|
304
|
-
|
|
305
|
-
# Example with custom fields
|
|
306
|
-
serializer = UserSerializer.new(only: [:username, :avatar])
|
|
307
|
-
serializer.to_h(user1)
|
|
308
|
-
serializer.to_h(user2)
|
|
309
|
-
```
|
|
310
|
-
|
|
311
|
-
### Selecting Fields
|
|
312
|
-
|
|
313
|
-
By default, all attributes are serialized (except marked as `hide: true`).
|
|
314
|
-
|
|
315
|
-
We can provide **modifiers** to select serialized attributes:
|
|
316
|
-
|
|
317
|
-
- *only* - lists specific attributes to serialize;
|
|
318
|
-
- *except* - lists attributes to not serialize;
|
|
319
|
-
- *with* - lists attributes to serialize additionally (By default all attributes
|
|
320
|
-
are exposed and will be serialized, but some attributes can be hidden when
|
|
321
|
-
they are defined with the `hide: true` option, more on this below. `with`
|
|
322
|
-
modifier can be used to expose such attributes).
|
|
323
|
-
|
|
324
|
-
Modifiers can be provided as Hash, Array, String, Symbol, or their combinations.
|
|
325
|
-
|
|
326
|
-
With plugin [string_modifiers][string_modifiers] we can provide modifiers as
|
|
327
|
-
single `String` with attributes split by comma `,` and nested values inside
|
|
328
|
-
brackets `()`, like: `first_name,last_name,addresses(line1,line2)`. This can be very useful
|
|
329
|
-
to accept the list of fields in **GET** requests.
|
|
330
|
-
|
|
331
|
-
When a non-existing attribute is provided, the `Serega::AttributeNotExist` error
|
|
332
|
-
will be raised. This error can be muted with the `check_initiate_params: false`
|
|
333
|
-
option.
|
|
334
|
-
|
|
335
|
-
```ruby
|
|
336
|
-
class UserSerializer < Serega
|
|
337
|
-
plugin :string_modifiers # to send all modifiers in one string
|
|
338
|
-
|
|
339
|
-
attribute :username
|
|
340
|
-
attribute :first_name
|
|
341
|
-
attribute :last_name
|
|
342
|
-
attribute :email, hide: true
|
|
343
|
-
attribute :enemies, serializer: UserSerializer, hide: true
|
|
344
45
|
end
|
|
345
46
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
first_name: 'jack',
|
|
349
|
-
last_name: 'Oswald White',
|
|
350
|
-
email: 'joker@mail.com',
|
|
351
|
-
enemies: []
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
bruce = OpenStruct.new(
|
|
355
|
-
username: 'Batman',
|
|
356
|
-
first_name: 'Bruce',
|
|
357
|
-
last_name: 'Wayne',
|
|
358
|
-
email: 'bruce@wayneenterprises.com',
|
|
359
|
-
enemies: []
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
joker.enemies << bruce
|
|
363
|
-
bruce.enemies << joker
|
|
364
|
-
|
|
365
|
-
# Default
|
|
366
|
-
UserSerializer.to_h(bruce)
|
|
367
|
-
# => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
|
|
368
|
-
|
|
369
|
-
# With `:only` modifier
|
|
370
|
-
fields = [:username, { enemies: [:username, :email] }]
|
|
371
|
-
fields_as_string = 'username,enemies(username,email)'
|
|
372
|
-
|
|
373
|
-
UserSerializer.to_h(bruce, only: fields)
|
|
374
|
-
UserSerializer.new(only: fields).to_h(bruce)
|
|
375
|
-
UserSerializer.new(only: fields_as_string).to_h(bruce)
|
|
376
|
-
# =>
|
|
377
|
-
# {
|
|
378
|
-
# :username=>"Batman",
|
|
379
|
-
# :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]
|
|
380
|
-
# }
|
|
381
|
-
|
|
382
|
-
# With `:except` modifier
|
|
383
|
-
fields = %i[first_name last_name]
|
|
384
|
-
fields_as_string = 'first_name,last_name'
|
|
385
|
-
UserSerializer.new(except: fields).to_h(bruce)
|
|
386
|
-
UserSerializer.to_h(bruce, except: fields)
|
|
387
|
-
UserSerializer.to_h(bruce, except: fields_as_string)
|
|
388
|
-
# => {:username=>"Batman"}
|
|
389
|
-
|
|
390
|
-
# With `:with` modifier
|
|
391
|
-
fields = %i[email enemies]
|
|
392
|
-
fields_as_string = 'email,enemies'
|
|
393
|
-
UserSerializer.new(with: fields).to_h(bruce)
|
|
394
|
-
UserSerializer.to_h(bruce, with: fields)
|
|
395
|
-
UserSerializer.to_h(bruce, with: fields_as_string)
|
|
396
|
-
# =>
|
|
397
|
-
# {
|
|
398
|
-
# :username=>"Batman",
|
|
399
|
-
# :first_name=>"Bruce",
|
|
400
|
-
# :last_name=>"Wayne",
|
|
401
|
-
# :email=>"bruce@wayneenterprises.com",
|
|
402
|
-
# :enemies=>[
|
|
403
|
-
# {:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}
|
|
404
|
-
# ]
|
|
405
|
-
# }
|
|
406
|
-
|
|
407
|
-
# With no existing attribute
|
|
408
|
-
fields = %i[first_name enemy]
|
|
409
|
-
fields_as_string = 'first_name,enemy'
|
|
410
|
-
UserSerializer.new(only: fields).to_h(bruce)
|
|
411
|
-
UserSerializer.to_h(bruce, only: fields)
|
|
412
|
-
UserSerializer.to_h(bruce, only: fields_as_string)
|
|
413
|
-
# => raises Serega::AttributeNotExist
|
|
414
|
-
|
|
415
|
-
# With no existing attribute and disabled validation
|
|
416
|
-
fields = %i[first_name enemy]
|
|
417
|
-
fields_as_string = 'first_name,enemy'
|
|
418
|
-
UserSerializer.new(only: fields, check_initiate_params: false).to_h(bruce)
|
|
419
|
-
UserSerializer.to_h(bruce, only: fields, check_initiate_params: false)
|
|
420
|
-
UserSerializer.to_h(bruce, only: fields_as_string, check_initiate_params: false)
|
|
421
|
-
# => {:first_name=>"Bruce"}
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
### Using Context
|
|
425
|
-
|
|
426
|
-
Sometimes it can be required to use the context during serialization, like
|
|
427
|
-
current_user or any.
|
|
428
|
-
|
|
429
|
-
```ruby
|
|
430
|
-
class UserSerializer < Serega
|
|
431
|
-
attribute :email, value: proc { |user, ctx|
|
|
432
|
-
user.email if ctx[:current_user] == user
|
|
433
|
-
}
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
user = OpenStruct.new(email: 'email@example.com')
|
|
437
|
-
UserSerializer.(user, context: {current_user: user})
|
|
438
|
-
# => {:email=>"email@example.com"}
|
|
439
|
-
|
|
440
|
-
UserSerializer.new.to_h(user, context: {current_user: user}) # same
|
|
441
|
-
# => {:email=>"email@example.com"}
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### Batch Loading
|
|
445
|
-
|
|
446
|
-
Serega includes built-in batch loading functionality to efficiently load data on demand and solve N+1 problems.
|
|
447
|
-
|
|
448
|
-
#### Defining Named Batch Loaders
|
|
47
|
+
class PostSerializer < AppSerializer
|
|
48
|
+
batch :comments_count, ->(posts) { Comment.where(post: posts).group(:post_id).count }
|
|
449
49
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
```ruby
|
|
453
|
-
class UserSerializer < Serega
|
|
454
|
-
# Define named loaders
|
|
455
|
-
batch :comments_count, ->(users) { Comment.where(user: users).group(:user_id).count }
|
|
456
|
-
batch :comments_count, CommentsCountLoader # Example with callable class
|
|
457
|
-
|
|
458
|
-
# Use a loader by its name
|
|
459
|
-
attribute :comments_count, batch: :comments_count
|
|
460
|
-
|
|
461
|
-
# Equivalent — when the attribute name matches the loader name
|
|
50
|
+
attribute :title
|
|
462
51
|
attribute :comments_count, batch: true
|
|
463
|
-
|
|
464
|
-
# Equivalent — the default value resolution spelled out
|
|
465
|
-
attribute :comments_count, batch: :comments_count,
|
|
466
|
-
value: proc { |user, batches:| batches[:comments_count][user.id] }
|
|
467
|
-
|
|
468
|
-
# Hash form — needed only for sub-options, for example a custom `:id` method
|
|
469
|
-
attribute :comments_count, batch: { use: :comments_count, id: :uuid }
|
|
470
52
|
end
|
|
471
|
-
```
|
|
472
53
|
|
|
473
|
-
|
|
54
|
+
UserSerializer.to_h(User.all)
|
|
55
|
+
# => [{name: "Bruce Wayne"}, ...] 1 query
|
|
474
56
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
class UserSerializer < Serega
|
|
479
|
-
# Inline proc loader
|
|
480
|
-
attribute :followers_count,
|
|
481
|
-
batch: { use: ->(users) { Follow.where(user: users).group(:user_id).count } }
|
|
482
|
-
|
|
483
|
-
# Callable class loader
|
|
484
|
-
attribute :likes_count, batch: { use: LikesCountLoader }
|
|
485
|
-
|
|
486
|
-
# Method reference
|
|
487
|
-
attribute :views_count, batch: { use: ViewsCounter.method(:batch_load) }
|
|
488
|
-
end
|
|
57
|
+
UserSerializer.to_h(User.all, only: "name,posts(title,comments_count)")
|
|
58
|
+
# => [{name: "Bruce Wayne", posts: [{title: "Gotham at night", comments_count: 12}, ...]}, ...]
|
|
59
|
+
# 3 queries, for any number of users
|
|
489
60
|
```
|
|
490
61
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
Custom loaders can be provided directly using the `:use` option with any callable object:
|
|
494
|
-
|
|
495
|
-
```ruby
|
|
496
|
-
class UserSerializer < Serega
|
|
497
|
-
# Define named loaders
|
|
498
|
-
batch :facebook_likes, FacebookLikesLoader
|
|
499
|
-
batch :twitter_likes, TwitterLikesLoader
|
|
500
|
-
|
|
501
|
-
# Summarize likes
|
|
502
|
-
attribute :likes_count,
|
|
503
|
-
batch: { use: [:facebook_likes, :twitter_likes] },
|
|
504
|
-
value: proc { |user, batches:| batches[:facebook_likes][user.id] + batches[:twitter_likes][user.id] }
|
|
505
|
-
end
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
### Prepare Initial Objects
|
|
509
|
-
|
|
510
|
-
`prepare_initial_objects` replaces the serialized objects before serialization
|
|
511
|
-
starts, so a serializer can accept ids or other references and load the records
|
|
512
|
-
itself.
|
|
513
|
-
|
|
514
|
-
```ruby
|
|
515
|
-
class UserSerializer < Serega
|
|
516
|
-
# OccamsRecord returns read-only records — faster and with a much lower
|
|
517
|
-
# memory footprint than ActiveRecord models
|
|
518
|
-
prepare_initial_objects { |user_ids| OccamsRecord.query(User.where(id: user_ids)).run }
|
|
519
|
-
|
|
520
|
-
attribute :first_name
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
UserSerializer.to_h(["17", "42"]) # => [{first_name: "Ann"}, {first_name: "Bob"}]
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
The handler accepts the serialization context as a second positional or a
|
|
527
|
-
`:ctx` keyword argument, and can be provided as a callable value:
|
|
528
|
-
|
|
529
|
-
```ruby
|
|
530
|
-
class UserSerializer < Serega
|
|
531
|
-
prepare_initial_objects { |ids, ctx| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }
|
|
532
|
-
# or
|
|
533
|
-
prepare_initial_objects { |ids, ctx:| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }
|
|
534
|
-
# or
|
|
535
|
-
prepare_initial_objects UsersLoader
|
|
536
|
-
end
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
The handler runs once per serialization, before the `:many` option is detected,
|
|
540
|
-
so it can turn a single object into a collection and back:
|
|
541
|
-
|
|
542
|
-
```ruby
|
|
543
|
-
class UserSerializer < Serega
|
|
544
|
-
prepare_initial_objects { |user_id| OccamsRecord.query(User.where(id: user_id)).run }
|
|
545
|
-
|
|
546
|
-
attribute :first_name
|
|
547
|
-
end
|
|
548
|
-
|
|
549
|
-
UserSerializer.to_h("17") # => [{first_name: "Ann"}] - an array, as the handler returned a collection
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
A provided `:many` option is used as is. A handler returning `nil` serializes
|
|
553
|
-
`nil`, and errors raised inside the handler are not wrapped.
|
|
554
|
-
|
|
555
|
-
Declared preloads are applied to the prepared objects, so the handler pairs
|
|
556
|
-
with [preloads][preloads] and the
|
|
557
|
-
[activerecord_preloads][activerecord_preloads] plugin:
|
|
558
|
-
|
|
559
|
-
```ruby
|
|
560
|
-
class UserSerializer < Serega
|
|
561
|
-
plugin :activerecord_preloads
|
|
562
|
-
|
|
563
|
-
prepare_initial_objects { |user_ids| User.where(id: user_ids) }
|
|
564
|
-
|
|
565
|
-
attribute :albums_count, preload: :albums, value: proc { |user| user.albums.size }
|
|
566
|
-
end
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
⚠️ The `:preload` option needs ActiveRecord objects. When the handler returns
|
|
570
|
-
anything else — OccamsRecord read-only records, Structs, Hashes, plain objects
|
|
571
|
-
— preloading raises `Serega::SeregaError` (`Can't preload ... to ...`) during
|
|
572
|
-
serialization. Load that data with [batch loading](#batch-loading) instead,
|
|
573
|
-
which works with any objects.
|
|
574
|
-
|
|
575
|
-
The handler runs for the serialized objects only, and not for objects of nested
|
|
576
|
-
serializers. It is inherited by subclasses, so declare it on concrete
|
|
577
|
-
serializers rather than on a base serializer shared by all of them.
|
|
578
|
-
|
|
579
|
-
## Configuration
|
|
580
|
-
|
|
581
|
-
Here are the default options. Other options can be added with plugins.
|
|
582
|
-
|
|
583
|
-
⚠️ Attributes are prepared at the moment they are defined. If a config option
|
|
584
|
-
influences attributes (`auto_preload`, `hide_by_default`, `batch_id_option`,
|
|
585
|
-
formatters, etc.), changing it affects only attributes defined **after** the
|
|
586
|
-
change — including nested serializers defined with attribute blocks, which
|
|
587
|
-
are created at their definition point. Configure the serializer before
|
|
588
|
-
defining attributes.
|
|
589
|
-
|
|
590
|
-
```ruby
|
|
591
|
-
class AppSerializer < Serega
|
|
592
|
-
# With `activerecord_preloads` plugin it automatically adds `preload` option
|
|
593
|
-
# to attributes with `:delegate` or `:serializer` option.
|
|
594
|
-
# It helps to preload associations automatically, omitting N+1 requests.
|
|
595
|
-
config.auto_preload = false
|
|
596
|
-
|
|
597
|
-
# Hides attributes by default. Accepts:
|
|
598
|
-
# false - nothing hidden (default)
|
|
599
|
-
# true - all attributes hidden
|
|
600
|
-
# [:preload] - attributes with :preload option hidden
|
|
601
|
-
# [:batch] - attributes with :batch option hidden
|
|
602
|
-
# [:preload, :batch] - either option triggers hiding
|
|
603
|
-
# Useful to avoid extra DB requests for attributes that were not requested.
|
|
604
|
-
config.hide_by_default = false
|
|
605
|
-
|
|
606
|
-
# Default method used on serialized object to resolve batch value
|
|
607
|
-
# For example:
|
|
608
|
-
# attribute :counter, batch: CounterBatchLoader
|
|
609
|
-
# # Attribute values will be resolved as:
|
|
610
|
-
# proc { |object, batches:| batches[:counter][object.id] }
|
|
611
|
-
config.batch_id_option = :id
|
|
612
|
-
|
|
613
|
-
# Defaults for the `hash_access:` attribute option — `default_mode` is
|
|
614
|
-
# what `hash_access: true` (and a Hash form omitting :mode) resolves to.
|
|
615
|
-
# See "Serializing Hash records".
|
|
616
|
-
config.hash_access.default_mode = :symbol # the default
|
|
617
|
-
config.hash_access.default_allow_missing_key = false # the default
|
|
618
|
-
|
|
619
|
-
# Parent class for nested serializers defined with attribute blocks.
|
|
620
|
-
# Usually a settings-only serializer, e.g. `config.base_serializer = self`
|
|
621
|
-
# in an application base serializer class. There is no default — an
|
|
622
|
-
# attribute block raises an error when no base serializer is chosen (it can
|
|
623
|
-
# also be provided per-attribute with the `base_serializer:` option).
|
|
624
|
-
config.base_serializer = self
|
|
625
|
-
|
|
626
|
-
# Disable/enable validation of modifiers (`:with, :except, :only`)
|
|
627
|
-
# By default, this validation is enabled.
|
|
628
|
-
# After disabling, all requested incorrect attributes will be skipped.
|
|
629
|
-
config.check_initiate_params = false # default is true, enabled
|
|
630
|
-
|
|
631
|
-
# Stores in memory prepared `plans` - list of serialized attributes.
|
|
632
|
-
# Next time serialization happens with the same modifiers (`only, except, with`),
|
|
633
|
-
# we will reuse already prepared `plans`.
|
|
634
|
-
# This defines storage size (count of stored `plans` with different modifiers).
|
|
635
|
-
config.max_cached_plans_per_serializer_count = 50 # default is 0, disabled
|
|
636
|
-
end
|
|
637
|
-
```
|
|
638
|
-
|
|
639
|
-
## Preloads
|
|
640
|
-
|
|
641
|
-
Serega includes built-in preloads functionality that lets you declare
|
|
642
|
-
`:preload` on attributes. It is used by the `:activerecord_preloads` plugin:
|
|
643
|
-
enable it and every association you declare is loaded once during
|
|
644
|
-
serialization, automatically, with no N+1.
|
|
645
|
-
|
|
646
|
-
Configuration options:
|
|
647
|
-
|
|
648
|
-
- `config.auto_preload` - default `false` (use `true` or `{ has_delegate_option: true, has_serializer_option: true }`)
|
|
649
|
-
- `config.hide_by_default` - default `false` (use `:auto` to hide attributes with preloads or batch loaders)
|
|
650
|
-
|
|
651
|
-
These options are extremely useful if you want to forget about finding
|
|
652
|
-
preloads manually.
|
|
653
|
-
|
|
654
|
-
The `:preload` value is passed to the preload handler exactly as written — a
|
|
655
|
-
Symbol, Array, Hash, or any custom value your ORM understands. Preloads can be
|
|
656
|
-
disabled with `preload: false` (or `preload: nil`); `preload: true` is not a
|
|
657
|
-
valid value. Automatically added preloads can be overwritten with the manually
|
|
658
|
-
specified `preload: :xxx` option.
|
|
659
|
-
|
|
660
|
-
The actual loading is performed by a handler registered with `preload_with`. The
|
|
661
|
-
`:activerecord_preloads` plugin registers one for you; to preload with another
|
|
662
|
-
ORM — or to attach data to plain non-ORM objects by your own rules — register
|
|
663
|
-
your own (see [Custom preloading](#custom-preloading)). Declaring `:preload`
|
|
664
|
-
without a registered handler raises an error, so a preload never silently does
|
|
665
|
-
nothing.
|
|
666
|
-
|
|
667
|
-
For some examples, **please read the comments in the code below**
|
|
668
|
-
|
|
669
|
-
```ruby
|
|
670
|
-
class AppSerializer < Serega
|
|
671
|
-
config.auto_preload = true
|
|
672
|
-
config.hide_by_default = :auto
|
|
673
|
-
end
|
|
674
|
-
|
|
675
|
-
class UserSerializer < AppSerializer
|
|
676
|
-
# No preloads
|
|
677
|
-
attribute :username
|
|
678
|
-
|
|
679
|
-
# `preload: :user_stats` added manually
|
|
680
|
-
attribute :followers_count, preload: :user_stats,
|
|
681
|
-
value: proc { |user| user.user_stats.followers_count }
|
|
682
|
-
|
|
683
|
-
# `preload: :user_stats` added automatically, as
|
|
684
|
-
# `auto_preload` includes `has_delegate_option: true`
|
|
685
|
-
attribute :comments_count, delegate: { to: :user_stats }
|
|
686
|
-
|
|
687
|
-
# `preload: :albums` added automatically as
|
|
688
|
-
# `auto_preload` includes `has_serializer_option: true`
|
|
689
|
-
attribute :albums, serializer: 'AlbumSerializer'
|
|
690
|
-
end
|
|
691
|
-
|
|
692
|
-
class AlbumSerializer < AppSerializer
|
|
693
|
-
attribute :images_count, delegate: { to: :album_stats }
|
|
694
|
-
end
|
|
62
|
+
## Installation
|
|
695
63
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
# are preloaded automatically. For example:
|
|
699
|
-
#
|
|
700
|
-
# UserSerializer.to_h(users, with: [:followers_count, { albums: :images_count }])
|
|
701
|
-
#
|
|
702
|
-
# preloads `:user_stats` and `:albums` on the users, and `:album_stats` on the
|
|
703
|
-
# albums.
|
|
64
|
+
```sh
|
|
65
|
+
bundle add serega
|
|
704
66
|
```
|
|
705
67
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
### Serializing the same object in association
|
|
68
|
+
## Quick start
|
|
709
69
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
other user associations. Specify `preload: nil` so that nothing is preloaded for
|
|
713
|
-
this attribute on the "user" object — the associations declared in
|
|
714
|
-
`UserStatSerializer` are still preloaded onto the same user object.
|
|
70
|
+
Define a base serializer with the setup your app uses. Every serializer
|
|
71
|
+
inherits it.
|
|
715
72
|
|
|
716
73
|
```ruby
|
|
74
|
+
# app/serializers/app_serializer.rb
|
|
717
75
|
class AppSerializer < Serega
|
|
76
|
+
plugin :activerecord_preloads
|
|
718
77
|
config.auto_preload = true
|
|
719
|
-
config.hide_by_default = :auto
|
|
720
78
|
end
|
|
721
79
|
|
|
80
|
+
# app/serializers/user_serializer.rb
|
|
722
81
|
class UserSerializer < AppSerializer
|
|
723
|
-
attribute :
|
|
724
|
-
attribute :user_stats,
|
|
725
|
-
serializer: 'UserStatSerializer',
|
|
726
|
-
value: proc { |user| user },
|
|
727
|
-
preload: nil
|
|
728
|
-
end
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
---
|
|
732
|
-
|
|
733
|
-
### Custom preloading
|
|
734
|
-
|
|
735
|
-
The `:activerecord_preloads` plugin works out of the box, but the preload
|
|
736
|
-
mechanism itself is not tied to ActiveRecord — or to ORMs at all. Register a
|
|
737
|
-
handler with `preload_with` and it is called once per preloaded attribute with
|
|
738
|
-
the gathered objects and that attribute's `:preload` value, before the
|
|
739
|
-
attribute is serialized:
|
|
740
|
-
|
|
741
|
-
```ruby
|
|
742
|
-
class UserSerializer < Serega
|
|
82
|
+
attribute :id
|
|
743
83
|
attribute :name
|
|
744
|
-
attribute :
|
|
745
|
-
|
|
746
|
-
# `objects` is the gathered users; `preloads` is this attribute's
|
|
747
|
-
# `:preload` value (`:posts`). `MyORM.preload` does the eager loading.
|
|
748
|
-
preload_with { |objects, preloads| MyORM.preload(objects, preloads) }
|
|
84
|
+
attribute :email, value: proc { |user, ctx| user.email if user == ctx[:current_user] }
|
|
85
|
+
attribute :avatar_url, hide: true
|
|
749
86
|
end
|
|
750
|
-
```
|
|
751
87
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
88
|
+
# app/controllers/users_controller.rb
|
|
89
|
+
class UsersController < ApplicationController
|
|
90
|
+
def index
|
|
91
|
+
users = UserSerializer.to_json(
|
|
92
|
+
User.all,
|
|
93
|
+
only: params[:fields], # "id,name" from GET /users?fields=id,name
|
|
94
|
+
context: {current_user: current_user}
|
|
95
|
+
)
|
|
759
96
|
|
|
760
|
-
|
|
761
|
-
`:preload` value. If the same `:preload` is declared on several attributes, the
|
|
762
|
-
handler is invoked once for each of them over the same objects, so it should
|
|
763
|
-
check whether the data is already loaded before loading it again. (The
|
|
764
|
-
`:activerecord_preloads` handler relies on `ActiveRecord::Associations::Preloader`,
|
|
765
|
-
which already skips associations that are loaded.)
|
|
766
|
-
|
|
767
|
-
Because you choose the `:preload` value, it doubles as a **discriminator**: give
|
|
768
|
-
two attributes different `:preload` values and branch on `preloads` inside the
|
|
769
|
-
handler — no need for the value to be a real association name.
|
|
770
|
-
|
|
771
|
-
```ruby
|
|
772
|
-
class PostSerializer < Serega
|
|
773
|
-
attribute :owner, serializer: UserSerializer, preload: :owner
|
|
774
|
-
attribute :author, serializer: UserSerializer, preload: :author
|
|
775
|
-
|
|
776
|
-
# Both attributes load the same person, but from different sources.
|
|
777
|
-
preload_with do |objects, preloads|
|
|
778
|
-
case preloads
|
|
779
|
-
when :owner then load_owners(objects) # e.g. from the DB
|
|
780
|
-
when :author then load_authors(objects) # e.g. from an external API
|
|
781
|
-
else MyORM.preload(objects, preloads)
|
|
782
|
-
end
|
|
97
|
+
render json: users
|
|
783
98
|
end
|
|
784
99
|
end
|
|
785
100
|
```
|
|
786
101
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
## Serializing Hash records
|
|
790
|
-
|
|
791
|
-
(objects should implement `#[]`, `#fetch`, and `#key?` - Hash already has them)
|
|
102
|
+
## Benchmarks
|
|
792
103
|
|
|
793
|
-
|
|
794
|
-
|
|
104
|
+
Serega is on par with the fastest Ruby serializers in speed and allocations,
|
|
105
|
+
and in some cases beats them. Results, the details of each case and how to run
|
|
106
|
+
them: [benchmarks/README.md](https://github.com/andreyruby/serega/blob/master/benchmarks/README.md).
|
|
795
107
|
|
|
796
|
-
|
|
108
|
+
## Features
|
|
797
109
|
|
|
798
|
-
|
|
799
|
-
- `:string` reads record["name"]
|
|
800
|
-
- `true` shorthand for `config.hash_access.default_mode` (`:symbol` by default)
|
|
110
|
+
### Select fields
|
|
801
111
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
`config.hash_access.default_allow_missing_key = true` will change `KeyError`
|
|
805
|
-
to return `nil` value instead. `config.hash_access.default_mode = <mode>`
|
|
806
|
-
changes what `true` (and a Hash form omitting `:mode`) resolves to.
|
|
112
|
+
`only`, `except` and `with` accept Symbols, Arrays, Hashes or a string like
|
|
113
|
+
`"name,posts(title)"`. Unknown fields raise `Serega::AttributeNotExist`.
|
|
807
114
|
|
|
808
115
|
```ruby
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
attribute :name, hash_access: true # reads record[:name] (config.hash_access.default_mode)
|
|
814
|
-
attribute :name, hash_access: :symbol # reads record[:name]
|
|
815
|
-
attribute :name, hash_access: :string # reads record["name"]
|
|
816
|
-
|
|
817
|
-
# Use long form if you need to override `allow_missing_key`
|
|
818
|
-
attribute :name, hash_access: { allow_missing_key: false }
|
|
819
|
-
attribute :name, hash_access: { mode: :symbol, allow_missing_key: false }
|
|
820
|
-
attribute :name, hash_access: { mode: :string, allow_missing_key: false }
|
|
821
|
-
end
|
|
116
|
+
UserSerializer.to_h(user, only: [:name, {posts: [:title]}])
|
|
117
|
+
UserSerializer.to_h(user, except: :email)
|
|
118
|
+
UserSerializer.to_h(user, with: :avatar_url) # adds hidden fields
|
|
822
119
|
```
|
|
823
120
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
- `to_hash_access` configures reading `<mode>` for intermediate object
|
|
827
|
-
- `hash_access` configures reading `<mode>` for final key
|
|
828
|
-
|
|
829
|
-
```ruby
|
|
830
|
-
# reads `record.address.city` (no hash access)
|
|
831
|
-
attribute :city, delegate: { to: :address } # record.address.city
|
|
832
|
-
|
|
833
|
-
# reads record.address[:city]
|
|
834
|
-
attribute :city, delegate: { to: :address, hash_access: :symbol }
|
|
121
|
+
→ [Selecting fields guide](docs/selecting-fields.md)
|
|
835
122
|
|
|
836
|
-
|
|
837
|
-
attribute :city, delegate: { to: :address, to_hash_access: :symbol }
|
|
123
|
+
### Batch loading
|
|
838
124
|
|
|
839
|
-
|
|
840
|
-
attribute :city, delegate: { to: :address, to_hash_access: :symbol, hash_access: :symbol }
|
|
841
|
-
|
|
842
|
-
# reads record["address"] and if exists returns record["address"][:city]
|
|
843
|
-
attribute :city,
|
|
844
|
-
delegate: {
|
|
845
|
-
to: :address,
|
|
846
|
-
allow_nil: true,
|
|
847
|
-
to_hash_access: { mode: :string, allow_missing_key: true },
|
|
848
|
-
hash_access: { mode: :symbol, allow_missing_key: false }
|
|
849
|
-
}
|
|
850
|
-
```
|
|
851
|
-
|
|
852
|
-
## Presenter
|
|
853
|
-
|
|
854
|
-
Moves computed attribute logic out of `:value` blocks and into a
|
|
855
|
-
`presenter do ... end` block, keeping serializers readable as schemas rather
|
|
856
|
-
than bags of lambdas.
|
|
857
|
-
|
|
858
|
-
Computed attributes can be written as inline blocks:
|
|
125
|
+
Load data for all serialized objects with one call.
|
|
859
126
|
|
|
860
127
|
```ruby
|
|
861
|
-
class UserSerializer <
|
|
862
|
-
|
|
863
|
-
attribute :role, value: proc { |user, ctx| user.id == ctx[:current_user_id] ? :self : :other }
|
|
864
|
-
end
|
|
865
|
-
```
|
|
866
|
-
|
|
867
|
-
They can also be written as methods in a `presenter do ... end` block:
|
|
868
|
-
|
|
869
|
-
```ruby
|
|
870
|
-
class UserSerializer < Serega
|
|
871
|
-
attribute :name
|
|
872
|
-
attribute :role
|
|
873
|
-
|
|
874
|
-
presenter do
|
|
875
|
-
def name
|
|
876
|
-
[first_name, last_name].compact.join(' ')
|
|
877
|
-
end
|
|
128
|
+
class UserSerializer < AppSerializer
|
|
129
|
+
batch :followers_count, ->(users) { Follow.where(user: users).group(:user_id).count }
|
|
878
130
|
|
|
879
|
-
|
|
880
|
-
id == __ctx__[:current_user_id] ? :self : :other
|
|
881
|
-
end
|
|
882
|
-
end
|
|
131
|
+
attribute :followers_count, batch: true
|
|
883
132
|
end
|
|
884
|
-
|
|
885
|
-
user = OpenStruct.new(id: 1, first_name: 'Bruce', last_name: 'Wayne')
|
|
886
|
-
UserSerializer.to_h(user, context: {current_user_id: 1})
|
|
887
|
-
# => {name: "Bruce Wayne", role: :self}
|
|
888
133
|
```
|
|
889
134
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
own stay out of the parent. The parent's blocks are copied when the child class
|
|
893
|
-
is created, so a `presenter` block added to a parent afterwards does not reach it.
|
|
894
|
-
|
|
895
|
-
Presenter methods run on a `SimpleDelegator` wrapping the serialized object, so
|
|
896
|
-
every method of that object is available directly. Any method not defined in a
|
|
897
|
-
`presenter` block is delegated to the object on the first call, and a real
|
|
898
|
-
delegator method is defined for it — so all subsequent serializations call it
|
|
899
|
-
directly.
|
|
900
|
-
|
|
901
|
-
The wrapped object is accessible via `__getobj__` when you need an unambiguous
|
|
902
|
-
reference to it. The serialization context is accessible via `__ctx__`.
|
|
135
|
+
→ [Batch loading guide](docs/batch-loading.md) ·
|
|
136
|
+
[Preloads guide](docs/preloads.md)
|
|
903
137
|
|
|
904
|
-
|
|
905
|
-
without them costs nothing — its attribute values, batch loaders and value
|
|
906
|
-
callables receive the raw objects.
|
|
138
|
+
### Formatters
|
|
907
139
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
### Plugin :activerecord_preloads
|
|
911
|
-
|
|
912
|
-
Automatically preloads associations to serialized objects.
|
|
913
|
-
|
|
914
|
-
Every association you declare with `:preload` is loaded once during
|
|
915
|
-
serialization using `ActiveRecord::Associations::Preloader`, so there are no
|
|
916
|
-
N+1 queries. The plugin does this by registering a [`preload_with`](#custom-preloading)
|
|
917
|
-
handler; to use a different ORM, register your own instead.
|
|
140
|
+
Format dates, money and other values the same way everywhere.
|
|
918
141
|
|
|
919
142
|
```ruby
|
|
920
143
|
class AppSerializer < Serega
|
|
921
|
-
|
|
922
|
-
plugin :activerecord_preloads
|
|
923
|
-
end
|
|
924
|
-
|
|
925
|
-
class AlbumSerializer < AppSerializer
|
|
926
|
-
# no preloads
|
|
927
|
-
attribute :title
|
|
928
|
-
|
|
929
|
-
# preloads :downloads, as manually specified
|
|
930
|
-
attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
|
|
144
|
+
formatter :iso8601, [:iso8601, 3], allow_nil: true
|
|
931
145
|
end
|
|
932
146
|
|
|
933
147
|
class UserSerializer < AppSerializer
|
|
934
|
-
|
|
935
|
-
attribute :username
|
|
936
|
-
|
|
937
|
-
# preloads :user_stats, as auto_preload is enabled for :delegate attributes
|
|
938
|
-
attribute :comments_count, delegate: { to: :user_stats }
|
|
939
|
-
|
|
940
|
-
# preloads :albums, as auto_preload is enabled for :serializer attributes
|
|
941
|
-
attribute :albums, serializer: AlbumSerializer
|
|
942
|
-
end
|
|
943
|
-
|
|
944
|
-
UserSerializer.to_h(users)
|
|
945
|
-
# 1 query to load :user_stats for all users
|
|
946
|
-
# + 1 query to load :albums for all users
|
|
947
|
-
# + 1 query to load :downloads for all albums
|
|
948
|
-
# = 3 queries total, regardless of how many users/albums are serialized
|
|
949
|
-
```
|
|
950
|
-
|
|
951
|
-
### Plugin :root
|
|
952
|
-
|
|
953
|
-
Adds a root key to serialized data.
|
|
954
|
-
|
|
955
|
-
Accepts options:
|
|
956
|
-
|
|
957
|
-
- :root - specifies root for all responses
|
|
958
|
-
- :root_one - specifies the root key for single object serialization only
|
|
959
|
-
- :root_many - specifies the root key for multiple objects serialization only
|
|
960
|
-
|
|
961
|
-
Adds additional config options:
|
|
962
|
-
|
|
963
|
-
- config.root.one
|
|
964
|
-
- config.root.many
|
|
965
|
-
- config.root.one=
|
|
966
|
-
- config.root.many=
|
|
967
|
-
|
|
968
|
-
The default root is `:data`.
|
|
969
|
-
|
|
970
|
-
```ruby
|
|
971
|
-
class UserSerializer < Serega
|
|
972
|
-
plugin :root # default root is :data
|
|
973
|
-
end
|
|
974
|
-
|
|
975
|
-
class UserSerializer < Serega
|
|
976
|
-
plugin :root, root: :users
|
|
977
|
-
end
|
|
978
|
-
|
|
979
|
-
class UserSerializer < Serega
|
|
980
|
-
plugin :root, root_one: :user, root_many: :people
|
|
981
|
-
end
|
|
982
|
-
|
|
983
|
-
class UserSerializer < Serega
|
|
984
|
-
plugin :root, root: nil # no root key by default
|
|
985
|
-
end
|
|
986
|
-
```
|
|
987
|
-
|
|
988
|
-
The root key can also be changed per serialization, or removed entirely by
|
|
989
|
-
providing `root: nil` (it can still be added back per serialization).
|
|
990
|
-
|
|
991
|
-
```ruby
|
|
992
|
-
class UserSerializer < Serega
|
|
993
|
-
plugin :root
|
|
148
|
+
attribute :created_at, format: :iso8601
|
|
994
149
|
end
|
|
995
|
-
|
|
996
|
-
UserSerializer.to_h(nil) # => {:data=>nil}
|
|
997
|
-
UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
|
|
998
|
-
UserSerializer.to_h(nil, root: nil) # => nil
|
|
999
150
|
```
|
|
1000
151
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
Depends on: [`:root`][root] plugin, that must be loaded first
|
|
1004
|
-
|
|
1005
|
-
Adds metadata to the serialized response via the class-level `meta_attribute`
|
|
1006
|
-
method, which accepts:
|
|
152
|
+
→ [Formatters guide](docs/formatters.md)
|
|
1007
153
|
|
|
1008
|
-
|
|
1009
|
-
- `**options` [Hash]
|
|
154
|
+
### Hashes, Structs and Data
|
|
1010
155
|
|
|
1011
|
-
|
|
1012
|
-
- `:value` - describes metadata value as any `#callable` instance
|
|
1013
|
-
- `:hide_nil` - does not show the metadata key if the value is nil.
|
|
1014
|
-
It is `false` by default
|
|
1015
|
-
- `:hide_empty` - does not show the metadata key if the value is nil or empty.
|
|
1016
|
-
It is `false` by default.
|
|
1017
|
-
|
|
1018
|
-
- `&block` [Proc] - describes value for the current meta attribute
|
|
156
|
+
Serialize Hash records, and get results as `Struct` or `Data` objects.
|
|
1019
157
|
|
|
1020
158
|
```ruby
|
|
1021
|
-
class
|
|
1022
|
-
|
|
1023
|
-
plugin :metadata
|
|
1024
|
-
|
|
1025
|
-
meta_attribute(:version, const: '1.2.3')
|
|
1026
|
-
meta_attribute(:ab_tests, :names, value: ABTests.new.method(:names))
|
|
1027
|
-
meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
|
|
1028
|
-
next unless records.respond_to?(:total_count)
|
|
1029
|
-
|
|
1030
|
-
{
|
|
1031
|
-
page: records.page,
|
|
1032
|
-
per_page: records.per_page,
|
|
1033
|
-
total_count: records.total_count
|
|
1034
|
-
}
|
|
1035
|
-
end
|
|
159
|
+
class EventSerializer < Serega
|
|
160
|
+
attribute :title, hash_access: true # reads event[:title]
|
|
1036
161
|
end
|
|
1037
162
|
|
|
1038
|
-
|
|
1039
|
-
# =>
|
|
163
|
+
UserSerializer.to_h(user) # => {name: "Bruce Wayne"}
|
|
164
|
+
UserSerializer.to_struct(user) # => #<struct name="Bruce Wayne">
|
|
165
|
+
UserSerializer.to_data(user) # => #<data name="Bruce Wayne">
|
|
1040
166
|
```
|
|
1041
167
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
Depends on: [`:root`][root] plugin, that must be loaded first
|
|
168
|
+
→ [Hash records guide](docs/hash-records.md) ·
|
|
169
|
+
[Serializing guide](docs/serializing.md)
|
|
1045
170
|
|
|
1046
|
-
|
|
1047
|
-
which is defined statically on the serializer).
|
|
171
|
+
### Ractors
|
|
1048
172
|
|
|
1049
|
-
|
|
1050
|
-
By default, it has the `:meta` value.
|
|
1051
|
-
|
|
1052
|
-
The key can be changed in children serializers using this method:
|
|
1053
|
-
`config.context_metadata.key=(value)`.
|
|
173
|
+
Serialize in parallel Ractors.
|
|
1054
174
|
|
|
1055
175
|
```ruby
|
|
1056
176
|
class UserSerializer < Serega
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
# Same:
|
|
1061
|
-
# plugin :context_metadata
|
|
1062
|
-
# config.context_metadata.key = :meta
|
|
1063
|
-
end
|
|
1064
|
-
|
|
1065
|
-
UserSerializer.to_h(nil, meta: { version: '1.0.1' })
|
|
1066
|
-
# => {:data=>nil, :version=>"1.0.1"}
|
|
1067
|
-
```
|
|
1068
|
-
|
|
1069
|
-
### Plugin :formatters
|
|
1070
|
-
|
|
1071
|
-
Defines named value formatters once and applies them to any attribute.
|
|
1072
|
-
|
|
1073
|
-
Use `config.formatters.add()` to register formatters. The `:format`
|
|
1074
|
-
attribute option then accepts a formatter name or a callable directly.
|
|
1075
|
-
|
|
1076
|
-
Formatters receive up to 2 parameters: the value and the context.
|
|
1077
|
-
|
|
1078
|
-
```ruby
|
|
1079
|
-
class AppSerializer < Serega
|
|
1080
|
-
plugin :formatters, formatters: {
|
|
1081
|
-
iso8601: ->(value) { value.iso8601 },
|
|
1082
|
-
on_off: ->(value) { value ? 'ON' : 'OFF' },
|
|
1083
|
-
money: ->(value) { value.round(2) },
|
|
1084
|
-
date: DateTypeFormatter # callable
|
|
1085
|
-
}
|
|
1086
|
-
end
|
|
1087
|
-
|
|
1088
|
-
class UserSerializer < Serega
|
|
1089
|
-
# Additionally we can add formatters via config in subclasses
|
|
1090
|
-
config.formatters.add(
|
|
1091
|
-
iso8601: ->(value) { value.iso8601 },
|
|
1092
|
-
on_off: ->(value) { value ? 'ON' : 'OFF' },
|
|
1093
|
-
money: ->(value) { value.round(2) }
|
|
1094
|
-
)
|
|
1095
|
-
|
|
1096
|
-
# Using predefined formatter
|
|
1097
|
-
attribute :commission, format: :money
|
|
1098
|
-
attribute :is_logged_in, format: :on_off
|
|
1099
|
-
attribute :created_at, format: :iso8601
|
|
1100
|
-
attribute :updated_at, format: :iso8601
|
|
1101
|
-
|
|
1102
|
-
# Using `callable` formatter
|
|
1103
|
-
attribute :score_percent, format: PercentFormatter # callable class
|
|
1104
|
-
attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
|
|
1105
|
-
end
|
|
1106
|
-
```
|
|
1107
|
-
|
|
1108
|
-
### Plugin :string_modifiers
|
|
1109
|
-
|
|
1110
|
-
Allows `:only`, `:except` and `:with` to be given as a single comma-separated
|
|
1111
|
-
string, with nested attributes in parentheses. Useful for accepting a field
|
|
1112
|
-
list straight from a query parameter.
|
|
1113
|
-
|
|
1114
|
-
Modifiers can still be provided the old way, as nested hashes or arrays.
|
|
1115
|
-
|
|
1116
|
-
```ruby
|
|
1117
|
-
PostSerializer.plugin :string_modifiers
|
|
1118
|
-
PostSerializer.new(only: "id,user(id,username)").to_h(post)
|
|
1119
|
-
PostSerializer.new(except: "user(username,email)").to_h(post)
|
|
1120
|
-
PostSerializer.new(with: "user(email)").to_h(post)
|
|
1121
|
-
|
|
1122
|
-
PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
|
|
1123
|
-
```
|
|
1124
|
-
|
|
1125
|
-
### Plugin :if
|
|
1126
|
-
|
|
1127
|
-
Adds `:if`, `:unless`, `:if_value`, `:unless_value` attribute options to
|
|
1128
|
-
conditionally remove attributes from the response.
|
|
1129
|
-
|
|
1130
|
-
`:if`/`:unless` receive the serialized object and context, and are checked
|
|
1131
|
-
before the attribute value is found. `:if_value`/`:unless_value` receive the
|
|
1132
|
-
already-found value and context, checked after. The latter two cannot be
|
|
1133
|
-
used with the `:serializer` option, since a relationship has no "serialized
|
|
1134
|
-
value" of its own — use `:if`/`:unless` instead.
|
|
1135
|
-
|
|
1136
|
-
See also a `:hide` option that is available without any plugins to hide
|
|
1137
|
-
attribute without conditions.
|
|
1138
|
-
Look at [select serialized fields](#selecting-fields) for `:hide` usage examples.
|
|
1139
|
-
|
|
1140
|
-
```ruby
|
|
1141
|
-
class UserSerializer < Serega
|
|
1142
|
-
attribute :email, if: :active? # translates to `if user.active?`
|
|
1143
|
-
attribute :email, if: proc {|user| user.active?} # same
|
|
1144
|
-
attribute :email, if: proc {|user, ctx| user == ctx[:current_user]}
|
|
1145
|
-
attribute :email, if: CustomPolicy.method(:view_email?)
|
|
1146
|
-
|
|
1147
|
-
attribute :email, unless: :hidden? # translates to `unless user.hidden?`
|
|
1148
|
-
attribute :email, unless: proc {|user| user.hidden?} # same
|
|
1149
|
-
attribute :email, unless: proc {|user, context| context[:show_emails]}
|
|
1150
|
-
attribute :email, unless: CustomPolicy.method(:hide_email?)
|
|
1151
|
-
|
|
1152
|
-
attribute :email, if_value: :present? # if email.present?
|
|
1153
|
-
attribute :email, if_value: proc {|email| email.present?} # same
|
|
1154
|
-
attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]}
|
|
1155
|
-
attribute :email, if_value: CustomPolicy.method(:view_email?)
|
|
1156
|
-
|
|
1157
|
-
attribute :email, unless_value: :blank? # unless email.blank?
|
|
1158
|
-
attribute :email, unless_value: proc {|email| email.blank?} # same
|
|
1159
|
-
attribute :email, unless_value: proc {|email, context| context[:show_emails]}
|
|
1160
|
-
attribute :email, unless_value: CustomPolicy.method(:hide_email?)
|
|
1161
|
-
end
|
|
1162
|
-
```
|
|
1163
|
-
|
|
1164
|
-
### Plugin :camel_case
|
|
1165
|
-
|
|
1166
|
-
Without this plugin, responding with *camelCased* keys means specifying the
|
|
1167
|
-
attribute name and method directly for each attribute:
|
|
1168
|
-
`attribute :firstName, method: first_name`
|
|
1169
|
-
|
|
1170
|
-
This plugin camelCases every attribute name automatically, replacing `_x`
|
|
1171
|
-
with `X` throughout the string. The transformation runs once, when the
|
|
1172
|
-
attribute is defined, not on every serialization.
|
|
1173
|
-
|
|
1174
|
-
Provide a custom transformation when adding the plugin, for example
|
|
1175
|
-
`plugin :camel_case, transform: ->(name) { name.camelize }`
|
|
1176
|
-
|
|
1177
|
-
Skip camelCase for a single attribute with `camel_case: false`.
|
|
1178
|
-
|
|
1179
|
-
This plugin transforms only attribute keys, without affecting the `root`,
|
|
1180
|
-
`metadata` and `context_metadata` plugins keys.
|
|
1181
|
-
|
|
1182
|
-
If you wish to [select serialized fields](#selecting-fields), you should
|
|
1183
|
-
provide them camelCased.
|
|
1184
|
-
|
|
1185
|
-
```ruby
|
|
1186
|
-
class AppSerializer < Serega
|
|
1187
|
-
plugin :camel_case
|
|
1188
|
-
end
|
|
1189
|
-
|
|
1190
|
-
class UserSerializer < AppSerializer
|
|
1191
|
-
attribute :first_name
|
|
1192
|
-
attribute :last_name
|
|
1193
|
-
attribute :full_name, camel_case: false,
|
|
1194
|
-
value: proc { |user| [user.first_name, user.last_name].compact.join(" ") }
|
|
177
|
+
attribute :name
|
|
178
|
+
make_shareable!
|
|
1195
179
|
end
|
|
1196
180
|
|
|
1197
|
-
|
|
1198
|
-
user = OpenStruct.new(first_name: "Bruce", last_name: "Wayne")
|
|
1199
|
-
UserSerializer.to_h(user)
|
|
1200
|
-
# => {firstName: "Bruce", lastName: "Wayne", full_name: "Bruce Wayne"}
|
|
1201
|
-
|
|
1202
|
-
UserSerializer.new(only: %i[firstName lastName]).to_h(user)
|
|
1203
|
-
# => {firstName: "Bruce", lastName: "Wayne"}
|
|
181
|
+
Ractor.new(user) { |user| UserSerializer.to_h(user) }.value
|
|
1204
182
|
```
|
|
1205
183
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
Helps to secure from malicious queries that serialize too much
|
|
1209
|
-
or from accidental serializing of objects with cyclic relations.
|
|
1210
|
-
|
|
1211
|
-
Depth limit is checked when constructing a serialization plan, that is when
|
|
1212
|
-
`#new` method is called, ex: `SomeSerializer.new(with: params[:with])`.
|
|
1213
|
-
It can be useful to instantiate serializer before any other business logic
|
|
1214
|
-
to get possible errors earlier.
|
|
1215
|
-
|
|
1216
|
-
Any class-level serialization methods also check the depth limit as they also
|
|
1217
|
-
instantiate serializer.
|
|
184
|
+
→ [Ractors guide](docs/ractors.md)
|
|
1218
185
|
|
|
1219
|
-
|
|
1220
|
-
Depth limit error details can be found in the additional
|
|
1221
|
-
`Serega::DepthLimitError#details` method
|
|
186
|
+
### And more
|
|
1222
187
|
|
|
1223
|
-
|
|
188
|
+
- [Attributes](docs/attributes.md) — all attribute options, relations and nested serializers
|
|
189
|
+
- [Conditional attributes](docs/conditional-attributes.md) — `if:`, `unless:`, `if_value:`, `unless_value:`
|
|
190
|
+
- [Context](docs/context.md) — pass `current_user` and other request data
|
|
191
|
+
- [Presenter](docs/presenter.md) — write computed attributes as methods
|
|
192
|
+
- [Prepare initial objects](docs/prepare-initial-objects.md) — serialize records by ids
|
|
193
|
+
- [Configuration](docs/configuration.md) — all config options
|
|
194
|
+
- [Root key and metadata](docs/root-and-metadata.md) — wrap results in `{data: ...}` and add metadata
|
|
1224
195
|
|
|
1225
|
-
|
|
1226
|
-
- `config.depth_limit.limit=`
|
|
196
|
+
## Documentation
|
|
1227
197
|
|
|
1228
|
-
|
|
198
|
+
- 📋 [Cheatsheet](CHEATSHEET.md) — every option on one page
|
|
199
|
+
- 📚 [Guides](docs/README.md) — each feature in depth
|
|
200
|
+
- 🔎 [Documentation site](https://andreyruby.github.io/serega/) — guides and API reference
|
|
201
|
+
- 🔤 [Type signatures](https://github.com/andreyruby/serega/tree/master/sig) — RBS of the public API
|
|
202
|
+
- 📝 [Changelog](https://github.com/andreyruby/serega/blob/master/CHANGELOG.md)
|
|
1229
203
|
|
|
1230
|
-
|
|
1231
|
-
class AppSerializer < Serega
|
|
1232
|
-
plugin :depth_limit, limit: 10 # set limit for all child classes
|
|
1233
|
-
end
|
|
1234
|
-
|
|
1235
|
-
class UserSerializer < AppSerializer
|
|
1236
|
-
config.depth_limit.limit = 5 # overrides limit for UserSerializer
|
|
1237
|
-
end
|
|
1238
|
-
```
|
|
1239
|
-
|
|
1240
|
-
### Plugin :explicit_many_option
|
|
1241
|
-
|
|
1242
|
-
Requires the `:many` option on every relationship attribute (an attribute
|
|
1243
|
-
with the `:serializer` option or a block defining a nested serializer), so
|
|
1244
|
-
it's always explicit whether it returns one object or many.
|
|
1245
|
-
|
|
1246
|
-
```ruby
|
|
1247
|
-
class BaseSerializer < Serega
|
|
1248
|
-
plugin :explicit_many_option
|
|
1249
|
-
config.base_serializer = self
|
|
1250
|
-
end
|
|
1251
|
-
|
|
1252
|
-
class PostSerializer < BaseSerializer
|
|
1253
|
-
attribute :text
|
|
1254
|
-
|
|
1255
|
-
attribute :user, many: false do
|
|
1256
|
-
attribute :name
|
|
1257
|
-
end
|
|
204
|
+
## Development
|
|
1258
205
|
|
|
1259
|
-
|
|
1260
|
-
|
|
206
|
+
```sh
|
|
207
|
+
bundle install
|
|
208
|
+
bundle exec rake # tests and linter
|
|
209
|
+
bin/console # irb with serega loaded
|
|
1261
210
|
```
|
|
1262
211
|
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
- The `Serega::SeregaError` is a base error raised by this gem.
|
|
1266
|
-
- The `Serega::AttributeNotExist` error is raised when validating attributes in
|
|
1267
|
-
`:only, :except, :with` modifiers. This error contains additional methods:
|
|
1268
|
-
|
|
1269
|
-
- `#serializer` - shows current serializer
|
|
1270
|
-
- `#attributes` - lists not existing attributes
|
|
1271
|
-
|
|
1272
|
-
## Release
|
|
1273
|
-
|
|
1274
|
-
To release a new version, read [RELEASE.md](https://github.com/aglushkov/serega/blob/master/RELEASE.md).
|
|
1275
|
-
|
|
1276
|
-
## Development
|
|
1277
|
-
|
|
1278
|
-
- `bundle install` - install dependencies
|
|
1279
|
-
- `bin/console` - open irb console with loaded gems
|
|
1280
|
-
- `bundle exec rspec` - run tests
|
|
1281
|
-
- `bundle exec rubocop` - check code standards
|
|
1282
|
-
- `yard stats --list-undoc --no-cache` - view undocumented code
|
|
1283
|
-
- `yard server --reload` - view code documentation
|
|
212
|
+
Releasing: [RELEASE.md](https://github.com/andreyruby/serega/blob/master/RELEASE.md).
|
|
1284
213
|
|
|
1285
214
|
## Contributing
|
|
1286
215
|
|
|
1287
|
-
Bug reports, pull requests and
|
|
216
|
+
Bug reports, pull requests and ideas are welcome!
|
|
1288
217
|
|
|
1289
218
|
## License
|
|
1290
219
|
|
|
1291
|
-
The gem is available as open source under the terms of the
|
|
220
|
+
The gem is available as open source under the terms of the
|
|
221
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
|
1292
222
|
|
|
1293
|
-
[
|
|
1294
|
-
[
|
|
1295
|
-
[camel_case]: #plugin-camel_case
|
|
1296
|
-
[context_metadata]: #plugin-context_metadata
|
|
1297
|
-
[depth_limit]: #plugin-depth_limit
|
|
1298
|
-
[formatters]: #plugin-formatters
|
|
1299
|
-
[hash_access]: #serializing-hash-records
|
|
1300
|
-
[metadata]: #plugin-metadata
|
|
1301
|
-
[preloads]: #preloads
|
|
1302
|
-
[presenter]: #presenter
|
|
1303
|
-
[root]: #plugin-root
|
|
1304
|
-
[string_modifiers]: #plugin-string_modifiers
|
|
1305
|
-
[if]: #plugin-if
|
|
1306
|
-
[build-badge]: https://github.com/aglushkov/serega/actions/workflows/main.yml/badge.svg?event=push
|
|
1307
|
-
[build]: https://github.com/aglushkov/serega/actions/workflows/main.yml
|
|
223
|
+
[build-badge]: https://github.com/andreyruby/serega/actions/workflows/main.yml/badge.svg?event=push
|
|
224
|
+
[build]: https://github.com/andreyruby/serega/actions/workflows/main.yml
|