serega 0.10.0 → 0.11.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 +319 -141
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +37 -105
- data/lib/serega/attribute_normalizer.rb +176 -0
- data/lib/serega/config.rb +26 -9
- data/lib/serega/object_serializer.rb +11 -10
- data/lib/serega/plan.rb +128 -0
- data/lib/serega/plan_point.rb +94 -0
- data/lib/serega/plugins/batch/batch.rb +187 -41
- data/lib/serega/plugins/batch/lib/loader.rb +4 -4
- data/lib/serega/plugins/batch/lib/loaders.rb +3 -3
- data/lib/serega/plugins/batch/lib/plugins_extensions/activerecord_preloads.rb +2 -2
- data/lib/serega/plugins/batch/lib/plugins_extensions/formatters.rb +19 -1
- data/lib/serega/plugins/batch/lib/plugins_extensions/preloads.rb +6 -4
- data/lib/serega/plugins/batch/lib/validations/check_opt_batch.rb +9 -5
- data/lib/serega/plugins/formatters/formatters.rb +28 -31
- data/lib/serega/plugins/if/if.rb +24 -6
- data/lib/serega/plugins/preloads/lib/format_user_preloads.rb +11 -13
- data/lib/serega/plugins/preloads/lib/main_preload_path.rb +2 -2
- data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +21 -15
- data/lib/serega/plugins/preloads/preloads.rb +72 -40
- data/lib/serega/plugins/presenter/presenter.rb +0 -9
- data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +11 -1
- data/lib/serega/plugins.rb +3 -3
- data/lib/serega/utils/enum_deep_dup.rb +18 -18
- data/lib/serega/utils/enum_deep_freeze.rb +35 -0
- data/lib/serega/utils/to_hash.rb +17 -9
- data/lib/serega.rb +22 -15
- metadata +6 -6
- data/lib/serega/map.rb +0 -91
- data/lib/serega/map_point.rb +0 -66
- data/lib/serega/plugins/batch/lib/batch_option_model.rb +0 -73
- data/lib/serega/plugins/preloads/lib/enum_deep_freeze.rb +0 -26
data/README.md
CHANGED
@@ -5,7 +5,8 @@
|
|
5
5
|
|
6
6
|
# Serega Ruby Serializer
|
7
7
|
|
8
|
-
The Serega Ruby Serializer provides easy and powerful DSL to describe your
|
8
|
+
The Serega Ruby Serializer provides easy and powerful DSL to describe your
|
9
|
+
objects and to serialize them to Hash or JSON.
|
9
10
|
|
10
11
|
---
|
11
12
|
|
@@ -16,9 +17,11 @@ The Serega Ruby Serializer provides easy and powerful DSL to describe your objec
|
|
16
17
|
It has some great features:
|
17
18
|
|
18
19
|
- Manually [select serialized fields](#selecting-fields)
|
19
|
-
- Solutions for N+1 problem (via [batch][batch], [preloads][preloads] or
|
20
|
+
- Solutions for N+1 problem (via [batch][batch], [preloads][preloads] or
|
21
|
+
[activerecord_preloads][activerecord_preloads] plugins)
|
20
22
|
- Built-in object presenter ([presenter][presenter] plugin)
|
21
|
-
- Adding custom metadata (via [metadata][metadata] or
|
23
|
+
- Adding custom metadata (via [metadata][metadata] or
|
24
|
+
[context_metadata][context_metadata] plugins)
|
22
25
|
- Attributes formatters ([formatters][formatters] plugin)
|
23
26
|
- Conditional attributes ([if][if] plugin)
|
24
27
|
|
@@ -26,11 +29,13 @@ It has some great features:
|
|
26
29
|
|
27
30
|
`bundle add serega`
|
28
31
|
|
29
|
-
|
30
32
|
### Define serializers
|
31
33
|
|
32
|
-
Most apps should define **base serializer** with common plugins and settings to
|
33
|
-
|
34
|
+
Most apps should define **base serializer** with common plugins and settings to
|
35
|
+
not repeat them in each serializer.
|
36
|
+
|
37
|
+
Serializers will inherit everything (plugins, config, attributes) from their
|
38
|
+
superclasses.
|
34
39
|
|
35
40
|
```ruby
|
36
41
|
class AppSerializer < Serega
|
@@ -68,10 +73,11 @@ class UserSerializer < Serega
|
|
68
73
|
# Option :value can be used with callable object to define attribute value
|
69
74
|
attribute :first_name, value: proc { |user| user.profile&.first_name }
|
70
75
|
|
71
|
-
# Option :delegate can be used to define attribute value.
|
76
|
+
# Option :delegate can be used to define attribute value.
|
77
|
+
# Sub-option :allow_nil by default is false
|
72
78
|
attribute :first_name, delegate: { to: :profile, allow_nil: true }
|
73
79
|
|
74
|
-
# Option :delegate can be used with :key sub-option
|
80
|
+
# Option :delegate can be used with :key sub-option
|
75
81
|
attribute :first_name, delegate: { to: :profile, key: :fname }
|
76
82
|
|
77
83
|
# Option :const specifies attribute with specific constant value
|
@@ -93,10 +99,10 @@ class UserSerializer < Serega
|
|
93
99
|
|
94
100
|
# Option `:preload` can be specified when enabled `:preloads` plugin
|
95
101
|
# It allows to specify associations to preload to attribute value
|
96
|
-
attribute
|
102
|
+
attribute(:email, preload: :emails) { |user| user.emails.find(&:verified?) }
|
97
103
|
|
98
|
-
# Options `:if`, `:unless`, `:if_value`, `:unless_value` can be specified
|
99
|
-
# They hide attribute key and value from response
|
104
|
+
# Options `:if`, `:unless`, `:if_value`, `:unless_value` can be specified
|
105
|
+
# when enabled `:if` plugin. They hide attribute key and value from response.
|
100
106
|
# See more usage examples in :if plugin section.
|
101
107
|
attribute :email, if: proc { |user, ctx| user == ctx[:current_user] }
|
102
108
|
attribute :email, if_value: :present?
|
@@ -113,16 +119,19 @@ end
|
|
113
119
|
|
114
120
|
---
|
115
121
|
|
116
|
-
⚠️ Attribute names are checked to include only "a-z", "A-Z", "0-9", "_", "-",
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
122
|
+
⚠️ Attribute names are checked to include only "a-z", "A-Z", "0-9", "\_", "-",
|
123
|
+
"~" characters.
|
124
|
+
|
125
|
+
We allow ONLY this characters as we want to be able to use attributes names in
|
126
|
+
URLs without escaping.
|
127
|
+
|
128
|
+
This check can be disabled this way:
|
121
129
|
|
122
|
-
This names check can be disabled globally or per-serializer via:
|
123
130
|
```ruby
|
131
|
+
# Disable globally
|
124
132
|
Serega.config.check_attribute_name = false
|
125
133
|
|
134
|
+
# Disable for specific serializer
|
126
135
|
class SomeSerializer < Serega
|
127
136
|
config.check_attribute_name = false
|
128
137
|
end
|
@@ -130,10 +139,10 @@ end
|
|
130
139
|
|
131
140
|
### Serializing
|
132
141
|
|
133
|
-
We can serialize objects using class methods `.to_h`, `.to_json`, `.as_json` and
|
142
|
+
We can serialize objects using class methods `.to_h`, `.to_json`, `.as_json` and
|
143
|
+
same instance methods `#to_h`, `#to_json`, `#as_json`.
|
134
144
|
`to_h` method is also aliased as `call`.
|
135
145
|
|
136
|
-
|
137
146
|
```ruby
|
138
147
|
user = OpenStruct.new(username: 'serega')
|
139
148
|
|
@@ -151,28 +160,53 @@ UserSerializer.as_json(user) # => {"username":"serega"}
|
|
151
160
|
UserSerializer.as_json([user]) # => [{"username":"serega"}]
|
152
161
|
```
|
153
162
|
|
163
|
+
If you always serialize same attributes it will make sense to save instance
|
164
|
+
of serializer and reuse this instance, it will be a bit faster (fields will be
|
165
|
+
prepared only once).
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
# Example with all fields
|
169
|
+
serializer = UserSerializer.new
|
170
|
+
serializer.to_h(user1)
|
171
|
+
serializer.to_h(user2)
|
172
|
+
|
173
|
+
# Example with custom fields
|
174
|
+
serializer = UserSerializer.new(only: [:username, :avatar])
|
175
|
+
serializer.to_h(user1)
|
176
|
+
serializer.to_h(user2)
|
177
|
+
```
|
178
|
+
|
154
179
|
---
|
155
|
-
⚠️ When you serialize `Struct` object, specify manually `many: false`. As Struct
|
180
|
+
⚠️ When you serialize `Struct` object, specify manually `many: false`. As Struct
|
181
|
+
is Enumerable and we check `object.is_a?(Enumerable)` to detect if we should
|
182
|
+
return array.
|
156
183
|
|
157
184
|
```ruby
|
158
|
-
|
185
|
+
UserSerializer.to_h(user_struct, many: false)
|
159
186
|
```
|
160
187
|
|
161
188
|
### Selecting Fields
|
162
189
|
|
163
|
-
|
190
|
+
By default all attributes are serialized (except marked as `hide: true`).
|
164
191
|
|
165
|
-
|
192
|
+
We can provide **modifiers** to select only needed attributes:
|
166
193
|
|
167
|
-
|
168
|
-
|
169
|
-
|
194
|
+
- *only* - lists attributes to serialize;
|
195
|
+
- *except* - lists attributes to not serialize;
|
196
|
+
- *with* - lists attributes to serialize additionally (By default all attributes
|
197
|
+
are exposed and will be serialized, but some attributes can be hidden when
|
198
|
+
they are defined with `hide: true` option, more on this below. `with` modifier
|
199
|
+
can be used to expose such attributes).
|
170
200
|
|
171
|
-
|
201
|
+
Modifiers can be provided as Hash, Array, String, Symbol or their combinations.
|
172
202
|
|
173
|
-
|
203
|
+
With plugin [string_modifiers][string_modifiers] we can provide modifiers as
|
204
|
+
single `String` with attributes split by comma `,` and nested values inside
|
205
|
+
brackets `()`, like: `username,enemies(username,email)`. This can be very useful
|
206
|
+
to accept list of fields in **GET** requests.
|
174
207
|
|
175
|
-
|
208
|
+
When provided non-existing attribute, `Serega::AttributeNotExist` error will be
|
209
|
+
raised. This error can be muted with `check_initiate_params: false` parameter.
|
176
210
|
|
177
211
|
```ruby
|
178
212
|
class UserSerializer < Serega
|
@@ -185,53 +219,88 @@ class UserSerializer < Serega
|
|
185
219
|
attribute :enemies, serializer: UserSerializer, hide: true
|
186
220
|
end
|
187
221
|
|
188
|
-
joker = OpenStruct.new(
|
189
|
-
|
222
|
+
joker = OpenStruct.new(
|
223
|
+
username: 'The Joker',
|
224
|
+
first_name: 'jack',
|
225
|
+
last_name: 'Oswald White',
|
226
|
+
email: 'joker@mail.com',
|
227
|
+
enemies: []
|
228
|
+
)
|
229
|
+
|
230
|
+
bruce = OpenStruct.new(
|
231
|
+
username: 'Batman',
|
232
|
+
first_name: 'Bruce',
|
233
|
+
last_name: 'Wayne',
|
234
|
+
email: 'bruce@wayneenterprises.com',
|
235
|
+
enemies: []
|
236
|
+
)
|
237
|
+
|
190
238
|
joker.enemies << bruce
|
191
239
|
bruce.enemies << joker
|
192
240
|
|
193
241
|
# Default
|
194
|
-
UserSerializer.to_h(bruce)
|
242
|
+
UserSerializer.to_h(bruce)
|
243
|
+
# => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
|
195
244
|
|
196
245
|
# With `:only` modifier
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
UserSerializer.
|
201
|
-
|
246
|
+
fields = [:username, { enemies: [:username, :email] }]
|
247
|
+
fields_as_string = 'username,enemies(username,email)'
|
248
|
+
|
249
|
+
UserSerializer.to_h(bruce, only: fields)
|
250
|
+
UserSerializer.new(only: fields).to_h(bruce)
|
251
|
+
UserSerializer.new(only: fields_as_string).to_h(bruce)
|
252
|
+
# =>
|
253
|
+
# {
|
254
|
+
# :username=>"Batman",
|
255
|
+
# :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]
|
256
|
+
# }
|
202
257
|
|
203
258
|
# With `:except` modifier
|
204
|
-
|
205
|
-
|
206
|
-
UserSerializer.
|
207
|
-
UserSerializer.to_h(bruce, except:
|
259
|
+
fields = %i[first_name last_name]
|
260
|
+
fields_as_string = 'first_name,last_name'
|
261
|
+
UserSerializer.new(except: fields).to_h(bruce)
|
262
|
+
UserSerializer.to_h(bruce, except: fields)
|
263
|
+
UserSerializer.to_h(bruce, except: fields_as_string)
|
208
264
|
# => {:username=>"Batman"}
|
209
265
|
|
210
266
|
# With `:with` modifier
|
211
|
-
|
212
|
-
|
213
|
-
UserSerializer.
|
214
|
-
UserSerializer.to_h(bruce, with:
|
215
|
-
|
267
|
+
fields = %i[email enemies]
|
268
|
+
fields_as_string = 'email,enemies'
|
269
|
+
UserSerializer.new(with: fields).to_h(bruce)
|
270
|
+
UserSerializer.to_h(bruce, with: fields)
|
271
|
+
UserSerializer.to_h(bruce, with: fields_as_string)
|
272
|
+
# =>
|
273
|
+
# {
|
274
|
+
# :username=>"Batman",
|
275
|
+
# :first_name=>"Bruce",
|
276
|
+
# :last_name=>"Wayne",
|
277
|
+
# :email=>"bruce@wayneenterprises.com",
|
278
|
+
# :enemies=>[
|
279
|
+
# {:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}
|
280
|
+
# ]
|
281
|
+
# }
|
216
282
|
|
217
283
|
# With not existing attribute
|
218
|
-
|
219
|
-
|
220
|
-
UserSerializer.
|
221
|
-
UserSerializer.to_h(bruce, only:
|
284
|
+
fields = %i[first_name enemy]
|
285
|
+
fields_as_string = 'first_name,enemy')
|
286
|
+
UserSerializer.new(only: fields).to_h(bruce)
|
287
|
+
UserSerializer.to_h(bruce, only: fields)
|
288
|
+
UserSerializer.to_h(bruce, only: fields_as_string)
|
222
289
|
# => raises Serega::AttributeNotExist, "Attribute 'enemy' not exists"
|
223
290
|
|
224
291
|
# With not existing attribute and disabled validation
|
225
|
-
|
226
|
-
|
227
|
-
UserSerializer.
|
228
|
-
UserSerializer.to_h(bruce, only:
|
292
|
+
fields = %i[first_name enemy]
|
293
|
+
fields_as_string = 'first_name,enemy'
|
294
|
+
UserSerializer.new(only: fields, check_initiate_params: false).to_h(bruce)
|
295
|
+
UserSerializer.to_h(bruce, only: fields, check_initiate_params: false)
|
296
|
+
UserSerializer.to_h(bruce, only: fields_as_string, check_initiate_params: false)
|
229
297
|
# => {:first_name=>"Bruce"}
|
230
298
|
```
|
231
299
|
|
232
300
|
### Using Context
|
233
301
|
|
234
|
-
Sometimes you can decide to use some context during serialization, like
|
302
|
+
Sometimes you can decide to use some context during serialization, like
|
303
|
+
current_user or any.
|
235
304
|
|
236
305
|
```ruby
|
237
306
|
class UserSerializer < Serega
|
@@ -241,8 +310,11 @@ class UserSerializer < Serega
|
|
241
310
|
end
|
242
311
|
|
243
312
|
user = OpenStruct.new(email: 'email@example.com')
|
244
|
-
UserSerializer.(user, context: {current_user: user})
|
313
|
+
UserSerializer.(user, context: {current_user: user})
|
314
|
+
# => {:email=>"email@example.com"}
|
315
|
+
|
245
316
|
UserSerializer.new.to_h(user, context: {current_user: user}) # same
|
317
|
+
# => {:email=>"email@example.com"}
|
246
318
|
```
|
247
319
|
|
248
320
|
## Configuration
|
@@ -252,21 +324,26 @@ This is initial config options, other config options can be added by plugins
|
|
252
324
|
```ruby
|
253
325
|
class AppSerializer < Serega
|
254
326
|
# Configure adapter to serialize to JSON.
|
255
|
-
# It is `JSON.dump` by default. When Oj gem is loaded then default is
|
327
|
+
# It is `JSON.dump` by default. When Oj gem is loaded then default is
|
328
|
+
# `Oj.dump(data, mode: :compat)`
|
256
329
|
config.to_json = ->(data) { Oj.dump(data, mode: :compat) }
|
257
330
|
|
258
|
-
# Configure adapter to de-serialize JSON.
|
259
|
-
#
|
331
|
+
# Configure adapter to de-serialize JSON.
|
332
|
+
# De-serialization is used only for `#as_json` method.
|
333
|
+
# It is `JSON.parse` by default.
|
334
|
+
# When Oj gem is loaded then default is `Oj.load(data)`
|
260
335
|
config.from_json = ->(data) { Oj.load(data) }
|
261
336
|
|
262
337
|
# Disable/enable validation of modifiers params `:with`, `:except`, `:only`
|
263
|
-
# By default it is enabled. After disabling,
|
338
|
+
# By default it is enabled. After disabling,
|
339
|
+
# when provided not existed attribute it will be just skipped.
|
264
340
|
config.check_initiate_params = false # default is true, enabled
|
265
341
|
|
266
|
-
# Stores in memory prepared `
|
267
|
-
# Next time serialization happens with same modifiers (
|
268
|
-
#
|
269
|
-
|
342
|
+
# Stores in memory prepared `plans` - list of serialized attributes.
|
343
|
+
# Next time serialization happens with same modifiers (`only, except, with`),
|
344
|
+
# we will reuse already prepared `plans`.
|
345
|
+
# This defines storage size (count of stored `plans` with different modifiers).
|
346
|
+
config.max_cached_plans_per_serializer_count = 50 # default is 0, disabled
|
270
347
|
end
|
271
348
|
```
|
272
349
|
|
@@ -278,6 +355,7 @@ Allows to define `:preloads` to attributes and then allows to merge preloads
|
|
278
355
|
from serialized attributes and return single associations hash.
|
279
356
|
|
280
357
|
Plugin accepts options:
|
358
|
+
|
281
359
|
- `auto_preload_attributes_with_delegate` - default `false`
|
282
360
|
- `auto_preload_attributes_with_serializer` - default `false`
|
283
361
|
- `auto_hide_attributes_with_preload` - default `false`
|
@@ -285,7 +363,8 @@ Plugin accepts options:
|
|
285
363
|
This options are very handy if you want to forget about finding preloads manually.
|
286
364
|
|
287
365
|
Preloads can be disabled with `preload: false` attribute option option.
|
288
|
-
Also automatically added preloads can be overwritten with manually specified
|
366
|
+
Also automatically added preloads can be overwritten with manually specified
|
367
|
+
`preload: :another_value`.
|
289
368
|
|
290
369
|
Some examples, **please read comments in the code below**
|
291
370
|
|
@@ -301,13 +380,16 @@ class UserSerializer < AppSerializer
|
|
301
380
|
# No preloads
|
302
381
|
attribute :username
|
303
382
|
|
304
|
-
#
|
305
|
-
attribute :followers_count, preload: :user_stats,
|
383
|
+
# `preload: :user_stats` added manually
|
384
|
+
attribute :followers_count, preload: :user_stats,
|
385
|
+
value: proc { |user| user.user_stats.followers_count }
|
306
386
|
|
307
|
-
#
|
387
|
+
# `preload: :user_stats` added automatically, as
|
388
|
+
# `auto_preload_attributes_with_delegate` option is true
|
308
389
|
attribute :comments_count, delegate: { to: :user_stats }
|
309
390
|
|
310
|
-
#
|
391
|
+
# `preload: :albums` added automatically as
|
392
|
+
# `auto_preload_attributes_with_serializer` option is true
|
311
393
|
attribute :albums, serializer: 'AlbumSerializer'
|
312
394
|
end
|
313
395
|
|
@@ -315,14 +397,21 @@ class AlbumSerializer < AppSerializer
|
|
315
397
|
attribute :images_count, delegate: { to: :album_stats }
|
316
398
|
end
|
317
399
|
|
318
|
-
# By default preloads are empty, as we specify `auto_hide_attributes_with_preload
|
319
|
-
#
|
320
|
-
UserSerializer.new.preloads
|
321
|
-
|
400
|
+
# By default preloads are empty, as we specify `auto_hide_attributes_with_preload`
|
401
|
+
# so attributes with preloads will be skipped so nothing should be preloaded
|
402
|
+
UserSerializer.new.preloads
|
403
|
+
# => {}
|
404
|
+
|
405
|
+
UserSerializer.new(with: :followers_count).preloads
|
406
|
+
# => {:user_stats=>{}}
|
407
|
+
|
408
|
+
UserSerializer.new(with: %i[followers_count comments_count]).preloads
|
409
|
+
# => {:user_stats=>{}}
|
322
410
|
|
323
|
-
UserSerializer.new(
|
324
|
-
|
325
|
-
|
411
|
+
UserSerializer.new(
|
412
|
+
with: [:followers_count, :comments_count, { albums: :images_count }]
|
413
|
+
).preloads
|
414
|
+
# => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
|
326
415
|
```
|
327
416
|
|
328
417
|
---
|
@@ -331,7 +420,10 @@ One tricky case, that you will probably never see in real life:
|
|
331
420
|
Manually you can preload multiple associations, like this:
|
332
421
|
|
333
422
|
```ruby
|
334
|
-
attribute :image,
|
423
|
+
attribute :image,
|
424
|
+
serializer: ImageSerializer,
|
425
|
+
preload: { attachment: :blob },
|
426
|
+
value: proc { |record| record.attachment }
|
335
427
|
```
|
336
428
|
|
337
429
|
In this case we mark last element (in this case it will be `blob`) as main,
|
@@ -340,12 +432,20 @@ If you need to preload them to `attachment`,
|
|
340
432
|
please specify additionally `:preload_path` option like this:
|
341
433
|
|
342
434
|
```ruby
|
343
|
-
attribute :image,
|
435
|
+
attribute :image,
|
436
|
+
serializer: ImageSerializer,
|
437
|
+
preload: { attachment: :blob },
|
438
|
+
preload_path: %i[attachment],
|
439
|
+
value: proc { |record| record.attachment }
|
344
440
|
```
|
441
|
+
|
345
442
|
---
|
346
443
|
|
347
|
-
📌 Plugin `:preloads` only allows to group preloads together in single Hash, but
|
444
|
+
📌 Plugin `:preloads` only allows to group preloads together in single Hash, but
|
445
|
+
they should be preloaded manually.
|
348
446
|
|
447
|
+
There are only [activerecord_preloads][activerecord_preloads] plugin that can
|
448
|
+
be used to preload this associations automatically.
|
349
449
|
|
350
450
|
### Plugin :activerecord_preloads
|
351
451
|
|
@@ -353,10 +453,9 @@ attribute :image, serializer: ImageSerializer, preload: { attachment: :blob }, p
|
|
353
453
|
|
354
454
|
Automatically preloads associations to serialized objects.
|
355
455
|
|
356
|
-
It takes all defined preloads from serialized attributes (including attributes
|
357
|
-
merges them into single associations hash and then
|
358
|
-
to preload associations to
|
359
|
-
|
456
|
+
It takes all defined preloads from serialized attributes (including attributes
|
457
|
+
from serialized relations), merges them into single associations hash and then
|
458
|
+
uses ActiveRecord::Associations::Preloader to preload associations to objects.
|
360
459
|
|
361
460
|
```ruby
|
362
461
|
class AppSerializer < Serega
|
@@ -376,59 +475,121 @@ end
|
|
376
475
|
|
377
476
|
class AlbumSerializer < AppSerializer
|
378
477
|
attribute :title
|
379
|
-
attribute :downloads_count, preload: :downloads,
|
478
|
+
attribute :downloads_count, preload: :downloads,
|
479
|
+
value: proc { |album| album.downloads.count }
|
380
480
|
end
|
381
481
|
|
382
|
-
UserSerializer.to_h(user)
|
482
|
+
UserSerializer.to_h(user)
|
483
|
+
# => preloads {users_stats: {}, albums: { downloads: {} }}
|
383
484
|
```
|
384
485
|
|
385
486
|
### Plugin :batch
|
386
487
|
|
387
|
-
Adds ability to load attributes values in batches.
|
488
|
+
Adds ability to load nested attributes values in batches.
|
388
489
|
|
389
|
-
It can be used to
|
490
|
+
It can be used to find value for attributes in optimal way:
|
390
491
|
|
391
|
-
|
392
|
-
|
393
|
-
|
492
|
+
- load associations for multiple objects
|
493
|
+
- load counters for multiple objects
|
494
|
+
- make any heavy calculations for multiple objects only once
|
495
|
+
|
496
|
+
After including plugin, attributes gain new `:batch` option:
|
497
|
+
|
498
|
+
```ruby
|
499
|
+
attribute :name, batch: { key: :id, loader: :name_loader, default: nil }
|
394
500
|
```
|
395
501
|
|
396
|
-
|
397
|
-
|
398
|
-
- `
|
399
|
-
|
502
|
+
`:batch` option must be a hash with this keys:
|
503
|
+
|
504
|
+
- `key` (required) [Symbol, Proc, callable] - Defines current object identifier.
|
505
|
+
Later `loader` will accept array of `keys` to detect this keys values.
|
506
|
+
- `loader` (required) [Symbol, Proc, callable] - Defines how to fetch values for
|
507
|
+
batch of keys. Receives 3 parameters: keys, context, plan_point.
|
508
|
+
- `default` (optional) - Default value for attribute.
|
509
|
+
By default it is `nil` or `[]` when attribute has option `many: true`
|
510
|
+
(ex: `attribute :tags, many: true, batch: { ... }`).
|
400
511
|
|
401
|
-
If `:loader` was defined using name (as Symbol) then batch loader must be
|
512
|
+
If `:loader` was defined using name (as Symbol) then batch loader must be
|
513
|
+
defined using `config.batch.define(:loader_name) { ... }` method.
|
402
514
|
|
403
|
-
|
515
|
+
Result of this `:loader` callable must be a **Hash** where:
|
516
|
+
|
517
|
+
- keys - provided keys
|
518
|
+
- values - values for according keys
|
519
|
+
|
520
|
+
`Batch` plugin can be defined with two specific attributes:
|
521
|
+
|
522
|
+
- `auto_hide: true` - Marks attributes with defined :batch as hidden, so it
|
523
|
+
will not be serialized by default
|
524
|
+
- `default_key: :id` - Set default object key (in this case :id) that will be
|
525
|
+
used for all attributes with :batch option specified.
|
404
526
|
|
405
527
|
```ruby
|
406
|
-
|
407
|
-
|
528
|
+
plugin :batch, auto_hide: true, default_key: :id
|
529
|
+
```
|
408
530
|
|
409
|
-
|
410
|
-
attribute :comments_count, batch: { key: :id, loader: PostCommentsCountBatchLoader, default: 0}
|
531
|
+
Options `auto_hide` and `default_key` can be overwritten in nested serializers.
|
411
532
|
|
412
|
-
|
413
|
-
|
533
|
+
```ruby
|
534
|
+
class AppSerializer
|
535
|
+
plugin :batch, auto_hide: true, default_key: :id
|
536
|
+
end
|
537
|
+
|
538
|
+
class UserSerializer < AppSerializer
|
539
|
+
config.batch.auto_hide = false
|
540
|
+
config.batch.default_key = :user_id
|
541
|
+
end
|
542
|
+
```
|
543
|
+
|
544
|
+
---
|
545
|
+
⚠️ ATTENTION: `Batch` plugin must be added to serializers which have no
|
546
|
+
`:batch` attributes, but have nested serializers, that have some. For example
|
547
|
+
when you serialize `User -> Album -> Song` and Song has `:batch` attribute, then
|
548
|
+
`:batch` plugin must be added to the User serializer also. \
|
549
|
+
Best way would be to create one parent `AppSerializer < Serega` for all your
|
550
|
+
serializers and add `:batch` plugin only to this parent `AppSerializer`
|
551
|
+
|
552
|
+
```ruby
|
553
|
+
class AppSerializer < Serega
|
554
|
+
plugin :batch, auto_hide: true, default_key: :id
|
555
|
+
end
|
556
|
+
|
557
|
+
class PostSerializer < AppSerializer
|
558
|
+
attribute :comments_count,
|
559
|
+
batch: {
|
560
|
+
loader: CommentsCountBatchLoader, # callable(keys, context, plan_point)
|
561
|
+
key: :id, # can be skipped (as :id value is same as configured :default_key)
|
562
|
+
default: 0
|
563
|
+
}
|
564
|
+
|
565
|
+
# Define batch loader via Symbol, later we should define this loader via
|
566
|
+
# `config.batch.define(:posts_comments_counter) { ... }`
|
567
|
+
#
|
568
|
+
# Loader will receive array of ids, as `default_key: :id` plugin option was specified.
|
569
|
+
# Default value for not found counters is nil, as `:default` option not defined
|
570
|
+
attribute :comments_count,
|
571
|
+
batch: { loader: :posts_comments_counter }
|
414
572
|
|
415
573
|
# Define batch loader with serializer
|
416
|
-
attribute :comments,
|
574
|
+
attribute :comments,
|
575
|
+
serializer: CommentSerializer,
|
576
|
+
batch: {loader: :posts_comments, default: []}
|
417
577
|
|
418
578
|
# Resulted block must return hash like { key => value(s) }
|
419
|
-
config.
|
579
|
+
config.batch.define(:posts_comments_counter) do |keys|
|
420
580
|
Comment.group(:post_id).where(post_id: keys).count
|
421
581
|
end
|
422
582
|
|
423
|
-
# We can return objects that will be automatically serialized if attribute
|
583
|
+
# We can return objects that will be automatically serialized if attribute
|
584
|
+
# defined with :serializer
|
424
585
|
# Parameter `context` can be used when loading batch
|
425
|
-
# Parameter `point` can be used to find nested attributes
|
426
|
-
config.
|
427
|
-
# point.
|
428
|
-
# point.preloads -
|
586
|
+
# Parameter `point` can be used to find nested attributes to serialize
|
587
|
+
config.batch.define(:posts_comments) do |keys, context, point|
|
588
|
+
# point.child_plan - if you need to manually check all nested attributes
|
589
|
+
# point.preloads - nested preloads (works with :preloads plugin only)
|
429
590
|
|
430
591
|
Comment
|
431
|
-
.preload(point.preloads) #
|
592
|
+
.preload(point.preloads) # Skip if :activerecord_preloads plugin used
|
432
593
|
.where(post_id: keys)
|
433
594
|
.where(is_spam: false)
|
434
595
|
.group_by(&:post_id)
|
@@ -441,11 +602,13 @@ end
|
|
441
602
|
Allows to add root key to your serialized data
|
442
603
|
|
443
604
|
Accepts options:
|
605
|
+
|
444
606
|
- :root - specifies root for all responses
|
445
607
|
- :root_one - specifies root for single object serialization only
|
446
608
|
- :root_many - specifies root for multiple objects serialization only
|
447
609
|
|
448
610
|
Adds additional config options:
|
611
|
+
|
449
612
|
- config.root.one
|
450
613
|
- config.root.many
|
451
614
|
- config.root.one=
|
@@ -455,11 +618,12 @@ Default root is `:data`.
|
|
455
618
|
|
456
619
|
Root also can be changed per serialization.
|
457
620
|
|
458
|
-
Also root can be removed for all responses by providing `root: nil`.
|
459
|
-
you still can to add it per
|
621
|
+
Also root can be removed for all responses by providing `root: nil`.
|
622
|
+
In this case no root will be added to response, but you still can to add it per
|
623
|
+
serialization
|
460
624
|
|
461
625
|
```ruby
|
462
|
-
#@example Define plugin
|
626
|
+
#@example Define :root plugin with different options
|
463
627
|
|
464
628
|
class UserSerializer < Serega
|
465
629
|
plugin :root # default root is :data
|
@@ -497,7 +661,8 @@ Depends on: [`:root`][root] plugin, that must be loaded first
|
|
497
661
|
Adds ability to describe metadata and adds it to serialized response
|
498
662
|
|
499
663
|
Added class-level method `:meta_attribute`, to define metadata, it accepts:
|
500
|
-
|
664
|
+
|
665
|
+
- *path [Array of Symbols] - nested hash keys.
|
501
666
|
- **options [Hash] - defaults are `hide_nil: false, hide_empty: false`
|
502
667
|
- &block [Proc] - describes value for current meta attribute
|
503
668
|
|
@@ -511,11 +676,16 @@ class AppSerializer < Serega
|
|
511
676
|
meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
|
512
677
|
next unless records.respond_to?(:total_count)
|
513
678
|
|
514
|
-
{
|
679
|
+
{
|
680
|
+
page: records.page,
|
681
|
+
per_page: records.per_page,
|
682
|
+
total_count: records.total_count
|
683
|
+
}
|
515
684
|
end
|
516
685
|
end
|
517
686
|
|
518
|
-
AppSerializer.to_h(nil)
|
687
|
+
AppSerializer.to_h(nil)
|
688
|
+
# => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=>[:foo, :bar]}}
|
519
689
|
```
|
520
690
|
|
521
691
|
### Plugin :context_metadata
|
@@ -524,9 +694,11 @@ Depends on: [`:root`][root] plugin, that must be loaded first
|
|
524
694
|
|
525
695
|
Allows to provide metadata and attach it to serialized response.
|
526
696
|
|
527
|
-
Accepts option `:context_metadata_key` with name of keyword that must be used to
|
697
|
+
Accepts option `:context_metadata_key` with name of keyword that must be used to
|
698
|
+
provide metadata. By default it is `:meta`
|
528
699
|
|
529
|
-
Key can be changed in children serializers using config
|
700
|
+
Key can be changed in children serializers using config
|
701
|
+
`config.context_metadata.key=(value)`
|
530
702
|
|
531
703
|
```ruby
|
532
704
|
class UserSerializer < Serega
|
@@ -548,7 +720,8 @@ Allows to define `formatters` and apply them on attributes.
|
|
548
720
|
|
549
721
|
Config option `config.formatters.add` can be used to add formatters.
|
550
722
|
|
551
|
-
Attribute option `:format` now can be used with name of formatter or with
|
723
|
+
Attribute option `:format` now can be used with name of formatter or with
|
724
|
+
callable instance.
|
552
725
|
|
553
726
|
```ruby
|
554
727
|
class AppSerializer < Serega
|
@@ -606,9 +779,10 @@ end
|
|
606
779
|
|
607
780
|
Allows to specify modifiers as strings.
|
608
781
|
|
609
|
-
Serialized attributes must be split with `,` and nested attributes must be
|
782
|
+
Serialized attributes must be split with `,` and nested attributes must be
|
783
|
+
defined inside brackets `(`, `)`.
|
610
784
|
|
611
|
-
Modifiers can still be provided old way
|
785
|
+
Modifiers can still be provided old way using nested hashes or arrays.
|
612
786
|
|
613
787
|
```ruby
|
614
788
|
PostSerializer.plugin :string_modifiers
|
@@ -616,7 +790,7 @@ PostSerializer.new(only: "id,user(id,username)").to_h(post)
|
|
616
790
|
PostSerializer.new(except: "user(username,email)").to_h(post)
|
617
791
|
PostSerializer.new(with: "user(email)").to_h(post)
|
618
792
|
|
619
|
-
# Modifiers can still be provided old way
|
793
|
+
# Modifiers can still be provided old way using nested hashes or arrays.
|
620
794
|
PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
|
621
795
|
```
|
622
796
|
|
@@ -625,46 +799,51 @@ PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
|
|
625
799
|
Plugin adds `:if`, `:unless`, `:if_value`, `:unless_value` options to
|
626
800
|
attributes so we can remove attributes from response in various ways.
|
627
801
|
|
628
|
-
Use `:if` and `:unless` when you want to hide attributes before finding
|
629
|
-
and use `:if_value` and `:unless_value` to hide attributes
|
802
|
+
Use `:if` and `:unless` when you want to hide attributes before finding
|
803
|
+
attribute value, and use `:if_value` and `:unless_value` to hide attributes
|
804
|
+
after finding final value.
|
630
805
|
|
631
|
-
Options `:if` and `:unless` accept currently serialized object and context as
|
632
|
-
Options `:if_value` and `:unless_value` accept already found
|
806
|
+
Options `:if` and `:unless` accept currently serialized object and context as
|
807
|
+
parameters. Options `:if_value` and `:unless_value` accept already found
|
808
|
+
serialized value and context as parameters.
|
633
809
|
|
634
|
-
Options `:if_value` and `:unless_value` cannot be used with :serializer option,
|
635
|
-
serialized objects have no "serialized value".
|
810
|
+
Options `:if_value` and `:unless_value` cannot be used with :serializer option,
|
811
|
+
as serialized objects have no "serialized value".
|
812
|
+
Use `:if` and `:unless` in this case.
|
636
813
|
|
637
814
|
See also a `:hide` option that is available without any plugins to hide
|
638
|
-
attribute without conditions.
|
815
|
+
attribute without conditions.
|
816
|
+
Look at [select serialized fields](#selecting-fields) for `:hide` usage examples.
|
639
817
|
|
640
818
|
```ruby
|
641
819
|
class UserSerializer < Serega
|
642
|
-
attribute :email, if: :active? # if user.active
|
820
|
+
attribute :email, if: :active? # translates to `if user.active?`
|
643
821
|
attribute :email, if: proc {|user| user.active?} # same
|
644
|
-
attribute :email, if: proc {|user, ctx| user == ctx[:current_user]}
|
645
|
-
attribute :email, if: CustomPolicy.method(:view_email?)
|
822
|
+
attribute :email, if: proc {|user, ctx| user == ctx[:current_user]}
|
823
|
+
attribute :email, if: CustomPolicy.method(:view_email?)
|
646
824
|
|
647
|
-
attribute :email, unless: :hidden? # unless user.hidden
|
825
|
+
attribute :email, unless: :hidden? # translates to `unless user.hidden?`
|
648
826
|
attribute :email, unless: proc {|user| user.hidden?} # same
|
649
|
-
attribute :email, unless: proc {|user, context| context[:show_emails]}
|
650
|
-
attribute :email, unless: CustomPolicy.method(:hide_email?)
|
827
|
+
attribute :email, unless: proc {|user, context| context[:show_emails]}
|
828
|
+
attribute :email, unless: CustomPolicy.method(:hide_email?)
|
651
829
|
|
652
830
|
attribute :email, if_value: :present? # if email.present?
|
653
831
|
attribute :email, if_value: proc {|email| email.present?} # same
|
654
|
-
attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]}
|
655
|
-
attribute :email, if_value: CustomPolicy.method(:view_email?)
|
832
|
+
attribute :email, if_value: proc {|email, ctx| ctx[:show_emails]}
|
833
|
+
attribute :email, if_value: CustomPolicy.method(:view_email?)
|
656
834
|
|
657
835
|
attribute :email, unless_value: :blank? # unless email.blank?
|
658
836
|
attribute :email, unless_value: proc {|email| email.blank?} # same
|
659
|
-
attribute :email, unless_value: proc {|email, context| context[:show_emails]}
|
660
|
-
attribute :email, unless_value: CustomPolicy.method(:hide_email?)
|
837
|
+
attribute :email, unless_value: proc {|email, context| context[:show_emails]}
|
838
|
+
attribute :email, unless_value: CustomPolicy.method(:hide_email?)
|
661
839
|
end
|
662
840
|
```
|
663
841
|
|
664
842
|
## Errors
|
665
843
|
|
666
844
|
- `Serega::SeregaError` is a base error raised by this gem.
|
667
|
-
- `Serega::AttributeNotExist` error is raised when validating attributes in
|
845
|
+
- `Serega::AttributeNotExist` error is raised when validating attributes in
|
846
|
+
`:only, :except, :with` modifiers
|
668
847
|
|
669
848
|
## Release
|
670
849
|
|
@@ -687,7 +866,6 @@ Bug reports, pull requests and improvements ideas are very welcome!
|
|
687
866
|
|
688
867
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
689
868
|
|
690
|
-
|
691
869
|
[activerecord_preloads]: #plugin-activerecord_preloads
|
692
870
|
[batch]: #plugin-batch
|
693
871
|
[context_metadata]: #plugin-context_metadata
|