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.
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