serega 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +319 -141
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +37 -105
  5. data/lib/serega/attribute_normalizer.rb +176 -0
  6. data/lib/serega/config.rb +26 -9
  7. data/lib/serega/object_serializer.rb +11 -10
  8. data/lib/serega/plan.rb +128 -0
  9. data/lib/serega/plan_point.rb +94 -0
  10. data/lib/serega/plugins/batch/batch.rb +187 -41
  11. data/lib/serega/plugins/batch/lib/loader.rb +4 -4
  12. data/lib/serega/plugins/batch/lib/loaders.rb +3 -3
  13. data/lib/serega/plugins/batch/lib/plugins_extensions/activerecord_preloads.rb +2 -2
  14. data/lib/serega/plugins/batch/lib/plugins_extensions/formatters.rb +19 -1
  15. data/lib/serega/plugins/batch/lib/plugins_extensions/preloads.rb +6 -4
  16. data/lib/serega/plugins/batch/lib/validations/check_opt_batch.rb +9 -5
  17. data/lib/serega/plugins/formatters/formatters.rb +28 -31
  18. data/lib/serega/plugins/if/if.rb +24 -6
  19. data/lib/serega/plugins/preloads/lib/format_user_preloads.rb +11 -13
  20. data/lib/serega/plugins/preloads/lib/main_preload_path.rb +2 -2
  21. data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +21 -15
  22. data/lib/serega/plugins/preloads/preloads.rb +72 -40
  23. data/lib/serega/plugins/presenter/presenter.rb +0 -9
  24. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +11 -1
  25. data/lib/serega/plugins.rb +3 -3
  26. data/lib/serega/utils/enum_deep_dup.rb +18 -18
  27. data/lib/serega/utils/enum_deep_freeze.rb +35 -0
  28. data/lib/serega/utils/to_hash.rb +17 -9
  29. data/lib/serega.rb +22 -15
  30. metadata +6 -6
  31. data/lib/serega/map.rb +0 -91
  32. data/lib/serega/map_point.rb +0 -66
  33. data/lib/serega/plugins/batch/lib/batch_option_model.rb +0 -73
  34. 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 objects and to serialize them to Hash or JSON.
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 [activerecord_preloads][activerecord_preloads] plugins)
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 [context_metadata][context_metadata] plugins)
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 not repeat them in each serializer.
33
- Children serializers will inherit everything (plugins, config, attributes) from parent.
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. Sub-option :allow_nil by default is false
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 to change method called on delegated object
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 :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
102
+ attribute(:email, preload: :emails) { |user| user.emails.find(&:verified?) }
97
103
 
98
- # Options `:if`, `:unless`, `:if_value`, `:unless_value` can be specified when enabled `:if` plugin
99
- # They hide attribute key and value from response when conditions satisfied
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", "_", "-", "~" characters.
117
- We have this check as:
118
- - Attributes names can be used in URL without encoding.
119
- - Plugin [string_modifiers][string_modifiers] already uses "," and "()" as attribute names delimeters.
120
- - We are protected from errors when added some non-english character looking as english.
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 same instance methods `#to_h`, `#to_json`, `#as_json`.
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 is Enumerable and we check `object.is_a?(Enumerable)` to detect if we should return array.
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
- UserSerializer.to_h(user_struct, many: false)
185
+ UserSerializer.to_h(user_struct, many: false)
159
186
  ```
160
187
 
161
188
  ### Selecting Fields
162
189
 
163
- By default all attributes are serialized (except marked as `hide: true`).
190
+ By default all attributes are serialized (except marked as `hide: true`).
164
191
 
165
- We can provide **modifiers** to select only needed attributes:
192
+ We can provide **modifiers** to select only needed attributes:
166
193
 
167
- - *only* - lists attributes to serialize;
168
- - *except* - lists attributes to not serialize;
169
- - *with* - lists attributes to serialize additionally (By default all attributes are exposed and will be serialized, but some attributes can be hidden when they are defined with `hide: true` option, more on this below. `with` modifier can be used to expose such attributes).
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
- Modifiers can be provided as Hash, Array, String, Symbol or their combinations.
201
+ Modifiers can be provided as Hash, Array, String, Symbol or their combinations.
172
202
 
173
- With plugin [string_modifiers][string_modifiers] we can provide modifiers as single `String` with attributes split by comma `,` and nested values inside brackets `()`, like: `username,enemies(username,email)`. This can be very useful to accept list of fields in **GET** requests.
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
- When provided non-existing attribute, `Serega::AttributeNotExist` error will be raised. This error can be muted with `check_initiate_params: false` parameter.
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(username: 'The Joker', first_name: 'jack', last_name: 'Oswald White', email: 'joker@mail.com', enemies: [])
189
- bruce = OpenStruct.new(username: 'Batman', first_name: 'Bruce', last_name: 'Wayne', email: 'bruce@wayneenterprises.com', enemies: [])
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) # => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
242
+ UserSerializer.to_h(bruce)
243
+ # => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
195
244
 
196
245
  # With `:only` modifier
197
- # Next 3 lines are identical:
198
- UserSerializer.to_h(bruce, only: [:username, { enemies: [:username, :email] }])
199
- UserSerializer.new(only: [:username, { enemies: [:username, :email] }]).to_h(bruce)
200
- UserSerializer.new(only: 'username,enemies(username,email)').to_h(bruce)
201
- # => {:username=>"Batman", :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]}
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
- # Next 3 lines are identical:
205
- UserSerializer.new(except: %i[first_name last_name]).to_h(bruce)
206
- UserSerializer.to_h(bruce, except: %i[first_name last_name])
207
- UserSerializer.to_h(bruce, except: 'first_name,last_name')
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
- # Next 3 lines are identical:
212
- UserSerializer.new(with: %i[email enemies]).to_h(bruce)
213
- UserSerializer.to_h(bruce, with: %i[email enemies])
214
- UserSerializer.to_h(bruce, with: 'email,enemies')
215
- # => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne", :email=>"bruce@wayneenterprises.com", :enemies=>[{:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}]}
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
- # Next 3 lines are identical:
219
- UserSerializer.new(only: %i[first_name enemy]).to_h(bruce)
220
- UserSerializer.to_h(bruce, only: %i[first_name enemy])
221
- UserSerializer.to_h(bruce, only: 'first_name,enemy')
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
- # Next 3 lines are identical:
226
- UserSerializer.new(only: %i[first_name enemy], check_initiate_params: false).to_h(bruce)
227
- UserSerializer.to_h(bruce, only: %i[first_name enemy], check_initiate_params: false)
228
- UserSerializer.to_h(bruce, only: 'first_name,enemy', check_initiate_params: false)
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 current_user or any.
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}) # => {:email=>"email@example.com"}
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 `Oj.dump(data, mode: :compat)`
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. De-serialization is used only for `#as_json` method.
259
- # It is `JSON.parse` by default. When Oj gem is loaded then default is `Oj.load(data)`
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, when provided not existed attribute it will be just skipped.
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 `maps` of serialized attributes during serialization.
267
- # Next time serialization happens with same modifiers (`:only, :except, :with`), we will use already prepared `map`.
268
- # Setting defines storage size (count of stored `maps` with different modifiers).
269
- config.max_cached_map_per_serializer_count = 50 # default is 0, disabled
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 `preload: :another_value`.
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
- # Specify `preload: :user_stats` manually
305
- attribute :followers_count, preload: :user_stats, value: proc { |user| user.user_stats.followers_count }
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
- # Automatically `preloads: :user_stats` as `auto_preload_attributes_with_delegate` option is true
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
- # Automatically `preloads: :albums` as `auto_preload_attributes_with_serializer` option is true
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 = true`,
319
- # and attributes with preloads will be not serialized
320
- UserSerializer.new.preloads # => {}
321
- UserSerializer.new.to_h(OpenStruct.new(username: 'foo')) # => {:username=>"foo"}
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(with: :followers_count).preloads # => {:user_stats=>{}}
324
- UserSerializer.new(with: %i[followers_count comments_count]).preloads # => {:user_stats=>{}}
325
- UserSerializer.new(with: [:followers_count, :comments_count, { albums: :images_count }]).preloads # => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
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, serializer: ImageSerializer, preload: { attachment: :blob }, value: proc { |record| record.attachment }
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, serializer: ImageSerializer, preload: { attachment: :blob }, preload_path: %i[attachment], value: proc { |record| record.attachment }
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 they should be preloaded manually. For now there are only [activerecord_preloads][activerecord_preloads] plugin that can automatically preload associations.
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 from serialized relations),
357
- merges them into single associations hash and then uses ActiveRecord::Associations::Preloader
358
- to preload associations to serialized objects.
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, value: proc { |album| album.downloads.count }
478
+ attribute :downloads_count, preload: :downloads,
479
+ value: proc { |album| album.downloads.count }
380
480
  end
381
481
 
382
- UserSerializer.to_h(user) # => preloads {users_stats: {}, albums: { downloads: {} }}
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 omit N+1, to calculate counters for different objects in single query, to request any data from external storage.
490
+ It can be used to find value for attributes in optimal way:
390
491
 
391
- Added new `:batch` attribute option, example:
392
- ```
393
- attribute :name, batch: { key: :id, loader: :name_loader, default: '' }
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
- Attribute `:batch` option must be a hash with this keys:
397
- - `key` (required) [Symbol, Proc, callable] - Defines identifier of current object
398
- - `loader` (required) [Symbol, Proc, callable] - Defines how to fetch values for batch of keys. Accepts 3 parameters: keys, context, point.
399
- - `default` (optional) - Default value used when loader does not return value for current key. By default it is `nil` or `[]` when attribute has additional option `many: true` (ex: `attribute :name, many: true, batch: { ... }`).
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 defined using `config.batch_loaders.define(:loader_name) { ... }` method. Result of this block must be a Hash where keys are - provided keys, and values are - batch loaded values for according keys.
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
- Batch loader works well with [`activerecord_preloads`][activerecord_preloads] plugin.
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
- class PostSerializer < Serega
407
- plugin :batch
528
+ plugin :batch, auto_hide: true, default_key: :id
529
+ ```
408
530
 
409
- # Define batch loader via callable class, it must accept three args (keys, context, nested_attributes)
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
- # Define batch loader via Symbol, later we should define this loader via config.batch_loaders.define(:posts_comments_counter) { ... }
413
- attribute :comments_count, batch: { key: :id, loader: :posts_comments_counter, default: 0}
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, serializer: CommentSerializer, batch: { key: :id, loader: :posts_comments, default: []}
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.batch_loaders.define(:posts_comments_counter) do |keys|
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 defined with :serializer
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 that will be serialized
426
- config.batch_loaders.define(:posts_comments) do |keys, context, point|
427
- # point.nested_points - if you need to manually check all nested attributes that will be serialized
428
- # point.preloads - if you need to find nested preloads (works with :preloads plugin only)
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) # Can be skipped when used :activerecord_preloads plugin
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`. In this case no root will be added to response, but
459
- you still can to add it per serialization
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
- - *path [Array<Symbol>] - nested hash keys beginning from the root object.
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
- { page: records.page, per_page: records.per_page, total_count: records.total_count }
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) # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=>[:foo, :bar]}}
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 provide metadata. By default it is `:meta`
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 `config.context_metadata.key=(value)`
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 callable instance.
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 defined inside brackets `(`, `)`.
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 with nested hashes or arrays.
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 with nested hashes or arrays.
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 attribute value,
629
- and use `:if_value` and `:unless_value` to hide attributes after we find final value.
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 parameters.
632
- Options `:if_value` and `:unless_value` accept already found serialized value and context as parameters.
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, as
635
- serialized objects have no "serialized value". Use `:if` and `:unless` in this case.
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. Look at [select serialized fields](#selecting-fields) for `:hide` usage examples.
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]} # using context
645
- attribute :email, if: CustomPolicy.method(:view_email?) # You can provide own callable object
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]} # using context
650
- attribute :email, unless: CustomPolicy.method(:hide_email?) # You can provide own callable object
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]} # using context
655
- attribute :email, if_value: CustomPolicy.method(:view_email?) # You can provide own callable object
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]} # using context
660
- attribute :email, unless_value: CustomPolicy.method(:hide_email?) # You can provide own callable object
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 `:only, :except, :with` modifiers
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