serega 0.6.1 → 0.8.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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +651 -0
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +20 -9
  5. data/lib/serega/config.rb +46 -1
  6. data/lib/serega/errors.rb +8 -5
  7. data/lib/serega/helpers/serializer_class_helper.rb +6 -0
  8. data/lib/serega/json/adapter.rb +6 -0
  9. data/lib/serega/json/json.rb +20 -0
  10. data/lib/serega/json/oj.rb +20 -0
  11. data/lib/serega/map.rb +17 -0
  12. data/lib/serega/map_point.rb +38 -1
  13. data/lib/serega/object_serializer.rb +17 -4
  14. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +66 -18
  15. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +102 -40
  16. data/lib/serega/plugins/batch/batch.rb +144 -10
  17. data/lib/serega/plugins/batch/lib/loader.rb +33 -1
  18. data/lib/serega/plugins/batch/lib/loaders.rb +15 -2
  19. data/lib/serega/plugins/batch/lib/plugins_extensions.rb +23 -1
  20. data/lib/serega/plugins/batch/lib/validations/check_batch_opt_key.rb +12 -0
  21. data/lib/serega/plugins/batch/lib/validations/check_batch_opt_loader.rb +12 -0
  22. data/lib/serega/plugins/batch/lib/validations/check_opt_batch.rb +12 -0
  23. data/lib/serega/plugins/context_metadata/context_metadata.rb +95 -13
  24. data/lib/serega/plugins/formatters/formatters.rb +94 -8
  25. data/lib/serega/plugins/hide_nil/hide_nil.rb +33 -7
  26. data/lib/serega/plugins/metadata/metadata.rb +108 -35
  27. data/lib/serega/plugins/metadata/validations/check_block.rb +3 -0
  28. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +4 -1
  29. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +4 -1
  30. data/lib/serega/plugins/metadata/validations/check_opts.rb +3 -0
  31. data/lib/serega/plugins/metadata/validations/check_path.rb +3 -0
  32. data/lib/serega/plugins/preloads/lib/enum_deep_freeze.rb +10 -1
  33. data/lib/serega/plugins/preloads/lib/format_user_preloads.rb +13 -4
  34. data/lib/serega/plugins/preloads/lib/main_preload_path.rb +16 -3
  35. data/lib/serega/plugins/preloads/lib/preloads_constructor.rb +4 -6
  36. data/lib/serega/plugins/preloads/preloads.rb +145 -12
  37. data/lib/serega/plugins/preloads/validations/check_opt_preload.rb +11 -0
  38. data/lib/serega/plugins/preloads/validations/check_opt_preload_path.rb +12 -0
  39. data/lib/serega/plugins/presenter/presenter.rb +20 -11
  40. data/lib/serega/plugins/root/root.rb +131 -19
  41. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +42 -12
  42. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +14 -0
  43. data/lib/serega/plugins.rb +7 -1
  44. data/lib/serega/utils/enum_deep_dup.rb +5 -0
  45. data/lib/serega/utils/to_hash.rb +21 -3
  46. data/lib/serega/validations/attribute/check_block.rb +7 -2
  47. data/lib/serega/validations/attribute/check_name.rb +6 -0
  48. data/lib/serega/validations/attribute/check_opt_const.rb +12 -9
  49. data/lib/serega/validations/attribute/check_opt_delegate.rb +19 -12
  50. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -0
  51. data/lib/serega/validations/attribute/check_opt_key.rb +12 -9
  52. data/lib/serega/validations/attribute/check_opt_many.rb +3 -0
  53. data/lib/serega/validations/attribute/check_opt_serializer.rb +3 -0
  54. data/lib/serega/validations/attribute/check_opt_value.rb +12 -9
  55. data/lib/serega/validations/check_attribute_params.rb +29 -1
  56. data/lib/serega/validations/check_initiate_params.rb +17 -0
  57. data/lib/serega/validations/check_serialize_params.rb +16 -0
  58. data/lib/serega/validations/initiate/check_modifiers.rb +15 -0
  59. data/lib/serega/validations/utils/check_allowed_keys.rb +14 -0
  60. data/lib/serega/validations/utils/check_opt_is_bool.rb +11 -0
  61. data/lib/serega/validations/utils/check_opt_is_hash.rb +11 -0
  62. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +11 -0
  63. data/lib/serega/version.rb +4 -0
  64. data/lib/serega.rb +86 -24
  65. metadata +3 -3
  66. data/lib/serega/serializer.rb +0 -32
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 23e6466f457dd12a35967fc62aa58e6322dcca3c4a6731f7b3cd7d750f1fec87
4
- data.tar.gz: 1a669d52eafa9703f5a787d5af8dfa439101f4f6024fbf70095b67c8edd06dcc
3
+ metadata.gz: b2ebcd147c1714ebb2b4e923ee179c788158b225ac76791817229a82975926ec
4
+ data.tar.gz: eef2621b3d6b9ac6ee1da3de29d3006ff99a0c2c91082cdc7795e82665ee92e5
5
5
  SHA512:
6
- metadata.gz: 68caa2feaa385cf5fb031867bbaed551c770a5883cea1c3494714425244726fc51cfa49e02bd573437fab1d9e3812527e98fec4df04bc5a92ddfdc819209ca5f
7
- data.tar.gz: 91854118f3058bdf899f2d6a8472b67bf0aefd60767c77f5954892c885d031bc9265423a240b9f19744679a74368bd0586cc30a4e83defa5a32a6e28fb2d9039
6
+ metadata.gz: 7891e792121105b7ee566b926347a63d82fb9fae186b7cf97766d4450141538d81dbb3bf7ac571b2ff7ed12bf8567f35d3e769a68e8979e47aad5c1c99be0300
7
+ data.tar.gz: 3dd972104c307eff809f5c41af005fb75fdd7caa6d6a494b90a8c63e89a0e25dc89de1547b762c6ad1f7ab3aa5e5f5811b470a6f20adaed55c7155c17fc00207
data/README.md ADDED
@@ -0,0 +1,651 @@
1
+ [![Gem Version](https://badge.fury.io/rb/serega.svg)](https://badge.fury.io/rb/serega)
2
+ [![GitHub Actions](https://github.com/aglushkov/serega/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/aglushkov/serega/actions/workflows/main.yml)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/f10c0659e16e25e49faa/test_coverage)](https://codeclimate.com/github/aglushkov/serega/test_coverage)
4
+ [![Maintainability](https://api.codeclimate.com/v1/badges/f10c0659e16e25e49faa/maintainability)](https://codeclimate.com/github/aglushkov/serega/maintainability)
5
+
6
+ # Serega Ruby Serializer
7
+
8
+ The Serega Ruby Serializer provides easy and powerful DSL to describe your objects and to serialize them to Hash or JSON.
9
+
10
+ ---
11
+
12
+ 📌 Serega does not depend on any gem and works with any framework
13
+
14
+ ---
15
+
16
+ It has some great features:
17
+
18
+ - Manually [select serialized fields](#selecting-fields)
19
+ - Solutions for N+1 problem (via [batch][batch], [preloads][preloads] or [activerecord_preloads][activerecord_preloads] plugins)
20
+ - Built-in object presenter ([presenter][presenter] plugin)
21
+ - Adding custom metadata (via [metadata][metadata] or [context_metadata][context_metadata] plugins)
22
+ - Attributes formatters ([formatters][formatters] plugin)
23
+
24
+ ## Installation
25
+
26
+ `bundle add serega`
27
+
28
+
29
+ ### Define serializers
30
+
31
+ Most apps should define **base serializer** with common plugins and settings to not repeat them in each serializer.
32
+ Children serializers will inherit everything (plugins, config, attributes) from parent.
33
+
34
+ ```ruby
35
+ class AppSerializer < Serega
36
+ # plugin :one
37
+ # plugin :two
38
+
39
+ # config.one = :one
40
+ # config.two = :two
41
+ end
42
+
43
+ class UserSerializer < AppSerializer
44
+ # attribute :one
45
+ # attribute :two
46
+ end
47
+
48
+ class CommentSerializer < AppSerializer
49
+ # attribute :one
50
+ # attribute :two
51
+ end
52
+ ```
53
+
54
+ ### Adding attributes
55
+
56
+ ```ruby
57
+ class UserSerializer < Serega
58
+ # Regular attribute
59
+ attribute :first_name
60
+
61
+ # Option :key specifies method in object
62
+ attribute :first_name, key: :old_first_name
63
+
64
+ # Block is used to define attribute value
65
+ attribute(:first_name) { |user| user.profile&.first_name }
66
+
67
+ # Option :value can be used with callable object to define attribute value
68
+ attribute :first_name, value: proc { |user| user.profile&.first_name }
69
+
70
+ # Option :delegate can be used to define attribute value. Sub-option :allow_nil by default is false
71
+ attribute :first_name, delegate: { to: :profile, allow_nil: true }
72
+
73
+ # Option :delegate can be used with :key sub-option to change method called on delegated object
74
+ attribute :first_name, delegate: { to: :profile, key: :fname }
75
+
76
+ # Option :const specifies attribute with specific constant value
77
+ attribute(:type, const: 'user')
78
+
79
+ # Option :hide specifies attributes that should not be serialized by default
80
+ attribute :tags, hide: true
81
+
82
+ # Option :serializer specifies nested serializer for attribute
83
+ # We can specify serializer as Class, String or Proc.
84
+ # Use String or Proc if you have cross references in serializers.
85
+ attribute :posts, serializer: PostSerializer
86
+ attribute :posts, serializer: "PostSerializer"
87
+ attribute :posts, serializer: -> { PostSerializer }
88
+
89
+ # Option `:many` specifies a has_many relationship
90
+ # Usually it is defined automatically by checking `is_a?(Enumerable)`
91
+ attribute :posts, serializer: PostSerializer, many: true
92
+
93
+ # Option `:preload` can be specified when enabled `:preloads` plugin
94
+ # It allows to specify associations to preload to attribute value
95
+ attribute :email, preload: :emails, value: proc { |user| user.emails.find(&:verified?) }
96
+
97
+ # Option `:hide_nil` can be specified when enabled `:hide_nil` plugin
98
+ # It is literally hides attribute if its value is nil
99
+ attribute :email, hide_nil: true
100
+
101
+ # Option `:format` can be specified when enabled `:formatters` plugin
102
+ # It changes attribute value
103
+ attribute :created_at, format: :iso_time
104
+ attribute :updated_at, format: :iso_time
105
+
106
+ # Option `:format` also can be used as Proc
107
+ attribute :created_at, format: proc { |time| time.strftime("%Y-%m-%d")}
108
+ end
109
+ ```
110
+
111
+ ### Serializing
112
+
113
+ We can serialize objects using class methods `.to_h`, `.to_json`, `.as_json` and same instance methods `#to_h`, `#to_json`, `#as_json`.
114
+ `to_h` method is also aliased as `call`.
115
+
116
+
117
+ ```ruby
118
+ user = OpenStruct.new(username: 'serega')
119
+
120
+ class UserSerializer < Serega
121
+ attribute :username
122
+ end
123
+
124
+ UserSerializer.to_h(user) # => {username: "serega"}
125
+ UserSerializer.to_h([user]) # => [{username: "serega"}]
126
+
127
+ UserSerializer.to_json(user) # => '{"username":"serega"}'
128
+ UserSerializer.to_json([user]) # => '[{"username":"serega"}]'
129
+
130
+ UserSerializer.as_json(user) # => {"username":"serega"}
131
+ UserSerializer.as_json([user]) # => [{"username":"serega"}]
132
+ ```
133
+
134
+ ---
135
+ ⚠️ 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.
136
+
137
+ ```ruby
138
+ UserSerializer.to_h(user_struct, many: false)
139
+ ```
140
+
141
+ ### Selecting Fields
142
+
143
+ By default all attributes are serialized (except marked as `hide: true`).
144
+
145
+ We can provide **modifiers** to select only needed attributes:
146
+
147
+ - *only* - lists attributes to serialize;
148
+ - *except* - lists attributes to not serialize;
149
+ - *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).
150
+
151
+ Modifiers can be provided as Hash, Array, String, Symbol or their combinations.
152
+
153
+ 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 field in **GET** requests.
154
+
155
+ When provided non-existing attribute, `Serega::AttributeNotExist` error will be raised. This error can be muted with `check_initiate_params: false` parameter.
156
+
157
+ ```ruby
158
+ class UserSerializer < Serega
159
+ plugin :string_modifiers # to send all modifiers in one string
160
+
161
+ attribute :username
162
+ attribute :first_name
163
+ attribute :last_name
164
+ attribute :email, hide: true
165
+ attribute :enemies, serializer: UserSerializer, hide: true
166
+ end
167
+
168
+ joker = OpenStruct.new(username: 'The Joker', first_name: 'jack', last_name: 'Oswald White', email: 'joker@mail.com', enemies: [])
169
+ bruce = OpenStruct.new(username: 'Batman', first_name: 'Bruce', last_name: 'Wayne', email: 'bruce@wayneenterprises.com', enemies: [])
170
+ joker.enemies << bruce
171
+ bruce.enemies << joker
172
+
173
+ # Default
174
+ UserSerializer.to_h(bruce) # => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne"}
175
+
176
+ # With `:only` modifier
177
+ # Next 3 lines are identical:
178
+ UserSerializer.to_h(bruce, only: [:username, { enemies: [:username, :email] }])
179
+ UserSerializer.new(only: [:username, { enemies: [:username, :email] }]).to_h(bruce)
180
+ UserSerializer.new(only: 'username,enemies(username,email)').to_h(bruce)
181
+ # => {:username=>"Batman", :enemies=>[{:username=>"The Joker", :email=>"joker@mail.com"}]}
182
+
183
+ # With `:except` modifier
184
+ # Next 3 lines are identical:
185
+ UserSerializer.new(except: %i[first_name last_name]).to_h(bruce)
186
+ UserSerializer.to_h(bruce, except: %i[first_name last_name])
187
+ UserSerializer.to_h(bruce, except: 'first_name,last_name')
188
+ # => {:username=>"Batman"}
189
+
190
+ # With `:with` modifier
191
+ # Next 3 lines are identical:
192
+ UserSerializer.new(with: %i[email enemies]).to_h(bruce)
193
+ UserSerializer.to_h(bruce, with: %i[email enemies])
194
+ UserSerializer.to_h(bruce, with: 'email,enemies')
195
+ # => {:username=>"Batman", :first_name=>"Bruce", :last_name=>"Wayne", :email=>"bruce@wayneenterprises.com", :enemies=>[{:username=>"The Joker", :first_name=>"jack", :last_name=>"Oswald White"}]}
196
+
197
+ # With not existing attribute
198
+ # Next 3 lines are identical:
199
+ UserSerializer.new(only: %i[first_name enemy]).to_h(bruce)
200
+ UserSerializer.to_h(bruce, only: %i[first_name enemy])
201
+ UserSerializer.to_h(bruce, only: 'first_name,enemy')
202
+ # => raises Serega::AttributeNotExist, "Attribute 'enemy' not exists"
203
+
204
+ # With not existing attribute and disabled validation
205
+ # Next 3 lines are identical:
206
+ UserSerializer.new(only: %i[first_name enemy], check_initiate_params: false).to_h(bruce)
207
+ UserSerializer.to_h(bruce, only: %i[first_name enemy], check_initiate_params: false)
208
+ UserSerializer.to_h(bruce, only: 'first_name,enemy', check_initiate_params: false)
209
+ # => {:first_name=>"Bruce"}
210
+ ```
211
+
212
+ ### Using Context
213
+
214
+ Sometimes you can decide to use some context during serialization, like current_user or any.
215
+
216
+ ```ruby
217
+ class UserSerializer < Serega
218
+ attribute(:email) do |user, ctx|
219
+ user.email if ctx[:current_user] == user
220
+ end
221
+ end
222
+
223
+ user = OpenStruct.new(email: 'email@example.com')
224
+ UserSerializer.(user, context: {current_user: user}) # => {:email=>"email@example.com"}
225
+ UserSerializer.new.to_h(user, context: {current_user: user}) # same
226
+ ```
227
+
228
+ ## Configuration
229
+
230
+ This is initial config options, other config options can be added by plugins
231
+
232
+ ```ruby
233
+ class AppSerializer < Serega
234
+ # Configure adapter to serialize to JSON.
235
+ # It is `JSON.dump` by default. When Oj gem is loaded then default is `Oj.dump(data, mode: :compat)`
236
+ config.to_json = ->(data) { Oj.dump(data, mode: :compat) }
237
+
238
+ # Configure adapter to de-serialize JSON. De-serialization is used only for `#as_json` method.
239
+ # It is `JSON.parse` by default. When Oj gem is loaded then default is `Oj.load(data)`
240
+ config.from_json = ->(data) { Oj.load(data) }
241
+
242
+ # Disable/enable validation of modifiers params `:with`, `:except`, `:only`
243
+ # By default it is enabled. After disabling, when provided not existed attribute it will be just skipped.
244
+ config.check_initiate_params = false # default is true, enabled
245
+
246
+ # Stores in memory prepared `maps` of serialized attributes during serialization.
247
+ # Next time serialization happens with same modifiers (`:only, :except, :with`), we will use already prepared `map`.
248
+ # Setting defines storage size (count of stored `maps` with different modifiers).
249
+ config.max_cached_map_per_serializer_count = 50 # default is 0, disabled
250
+ end
251
+ ```
252
+
253
+ ## Plugins
254
+
255
+ ### Plugin :preloads
256
+
257
+ Allows to define `:preloads` to attributes and then allows to merge preloads
258
+ from serialized attributes and return single associations hash.
259
+
260
+ Plugin accepts options:
261
+ - `auto_preload_attributes_with_delegate` - default false
262
+ - `auto_preload_attributes_with_serializer` - default false
263
+ - `auto_hide_attributes_with_preload` - default false
264
+
265
+ This options are very handy if you want to forget about finding preloads manually.
266
+
267
+ Preloads can be disabled with `preload: false` attribute option option.
268
+ Also automatically added preloads can be overwritten with manually specified `preload: :another_value`.
269
+
270
+ Some examples, **please read comments in the code below**
271
+
272
+ ```ruby
273
+ class AppSerializer < Serega
274
+ plugin :preloads,
275
+ auto_preload_attributes_with_delegate: true,
276
+ auto_preload_attributes_with_serializer: true,
277
+ auto_hide_attributes_with_preload: true
278
+ end
279
+
280
+ class UserSerializer < AppSerializer
281
+ # No preloads
282
+ attribute :username
283
+
284
+ # Specify `preload: :user_stats` manually
285
+ attribute :followers_count, preload: :user_stats, value: proc { |user| user.user_stats.followers_count }
286
+
287
+ # Automatically `preloads: :user_stats` as `auto_preload_attributes_with_delegate` option is true
288
+ attribute :comments_count, delegate: { to: :user_stats }
289
+
290
+ # Automatically `preloads: :albums` as `auto_preload_attributes_with_serializer` option is true
291
+ attribute :albums, serializer: 'AlbumSerializer'
292
+ end
293
+
294
+ class AlbumSerializer < AppSerializer
295
+ attribute :images_count, delegate: { to: :album_stats }
296
+ end
297
+
298
+ # By default preloads are empty, as we specify `auto_hide_attributes_with_preload = true`,
299
+ # and attributes with preloads will be not serialized
300
+ UserSerializer.new.preloads # => {}
301
+ UserSerializer.new.to_h(OpenStruct.new(username: 'foo')) # => {:username=>"foo"}
302
+
303
+ UserSerializer.new(with: :followers_count).preloads # => {:user_stats=>{}}
304
+ UserSerializer.new(with: %i[followers_count comments_count]).preloads # => {:user_stats=>{}}
305
+ UserSerializer.new(with: [:followers_count, :comments_count, { albums: :images_count }]).preloads # => {:user_stats=>{}, :albums=>{:album_stats=>{}}}
306
+ ```
307
+
308
+ ---
309
+ One tricky case, that you will probably never see in real life:
310
+
311
+ Manually you can preload multiple associations, like this:
312
+
313
+ ```ruby
314
+ attribute :image, serializer: ImageSerializer, preload: { attachment: :blob }, value: proc { |record| record.attachment }
315
+ ```
316
+
317
+ In this case we mark last element (in this case it will be `blob`) as main,
318
+ so nested associations, if any, will be preloaded to this `blob`.
319
+ If you need to preload them to `attachment`,
320
+ please specify additionally `:preload_path` option like this:
321
+
322
+ ```ruby
323
+ attribute :image, serializer: ImageSerializer, preload: { attachment: :blob }, preload_path: %i[attachment], value: proc { |record| record.attachment }
324
+ ```
325
+ ---
326
+
327
+ 📌 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.
328
+
329
+
330
+ ### Plugin :activerecord_preloads
331
+
332
+ (depends on [preloads][preloads] plugin, that must be loaded first)
333
+
334
+ Automatically preloads associations to serialized objects.
335
+
336
+ It takes all defined preloads from serialized attributes (including attributes from serialized relations),
337
+ merges them into single associations hash and then uses ActiveRecord::Associations::Preloader
338
+ to preload associations to serialized objects.
339
+
340
+
341
+ ```ruby
342
+ class AppSerializer < Serega
343
+ plugin :preloads,
344
+ auto_preload_attributes_with_delegate: true,
345
+ auto_preload_attributes_with_serializer: true,
346
+ auto_hide_attributes_with_preload: false
347
+
348
+ plugin :activerecord_preloads
349
+ end
350
+
351
+ class UserSerializer < AppSerializer
352
+ attribute :username
353
+ attribute :comments_count, delegate: { to: :user_stats }
354
+ attribute :albums, serializer: AlbumSerializer
355
+ end
356
+
357
+ class AlbumSerializer < AppSerializer
358
+ attribute :title
359
+ attribute :downloads_count, preload: :downloads, value: proc { |album| album.downloads.count }
360
+ end
361
+
362
+ UserSerializer.to_h(user) # => preloads {users_stats: {}, albums: { downloads: {} }}
363
+ ```
364
+
365
+ ### Plugin :batch
366
+
367
+ Adds ability to load attributes values in batches.
368
+
369
+ It can be used to omit N+1, to calculate counters for different objects in single query, to request any data from external storage.
370
+
371
+ Added new `:batch` attribute option, example:
372
+ ```
373
+ attribute :name, batch: { key: :id, loader: :name_loader, default: '' }
374
+ ```
375
+
376
+ `:batch` option must be a hash with this keys:
377
+ - :key (required) [Symbol, Proc, callable] - Defines identifier of current object
378
+ - :loader (required) [Symbol, Proc, callable] - Defines how to fetch values for batch of keys. Accepts 3 parameters: keys, context, point.
379
+ - :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` (`attribute :name, many: true, batch: { ... }`).
380
+
381
+ If `:loader` was defined via Symbol then batch loader must be defined using `config.batch_loaders.define(:loader_name) { ... }` method.
382
+ Result of this block must be a Hash where keys are - provided keys, and values are - batch loaded values for according keys.
383
+
384
+ Batch loader works well with [`activerecord_preloads`][activerecord_preloads] plugin.
385
+
386
+ ```ruby
387
+ class PostSerializer < Serega
388
+ plugin :batch
389
+
390
+ # Define batch loader via callable class, it must accept three args (keys, context, nested_attributes)
391
+ attribute :comments_count, batch: { key: :id, loader: PostCommentsCountBatchLoader, default: 0}
392
+
393
+ # Define batch loader via Symbol, later we should define this loader via config.batch_loaders.define(:posts_comments_counter) { ... }
394
+ attribute :comments_count, batch: { key: :id, loader: :posts_comments_counter, default: 0}
395
+
396
+ # Define batch loader with serializer
397
+ attribute :comments, serializer: CommentSerializer, batch: { key: :id, loader: :posts_comments, default: []}
398
+
399
+ # Resulted block must return hash like { key => value(s) }
400
+ config.batch_loaders.define(:posts_comments_counter) do |keys|
401
+ Comment.group(:post_id).where(post_id: keys).count
402
+ end
403
+
404
+ # We can return objects that will be automatically serialized if attribute defined with :serializer
405
+ # Parameter `context` can be used when loading batch
406
+ # Parameter `point` can be used to find nested attributes that will be serialized
407
+ config.batch_loaders.define(:posts_comments) do |keys, context, point|
408
+ # point.nested_points - if you need to manually check all nested attributes that will be serialized
409
+ # point.preloads - if you need to find nested preloads (works with :preloads plugin only)
410
+
411
+ Comment
412
+ .preload(point.preloads) # Can be skipped when used :activerecord_preloads plugin
413
+ .where(post_id: keys)
414
+ .where(is_spam: false)
415
+ .group_by(&:post_id)
416
+ end
417
+ end
418
+ ```
419
+
420
+ ### Plugin :root
421
+
422
+ Allows to add root key to your serialized data
423
+
424
+ Accepts options:
425
+ - :root - specifies root for all responses
426
+ - :root_one - specifies root for single object serialization only
427
+ - :root_many - specifies root for multiple objects serialization only
428
+
429
+ Adds additional config options:
430
+ - config.root.one
431
+ - config.root.many
432
+ - config.root.one=
433
+ - config.root_many=
434
+
435
+ Default root is `:data`.
436
+
437
+ Root also can be changed per serialization.
438
+
439
+ Also root can be removed for all responses by providing `root: nil`. In this case no root will be added to response, but
440
+ you still can to add it per serialization
441
+
442
+ ```ruby
443
+ #@example Define plugin
444
+
445
+ class UserSerializer < Serega
446
+ plugin :root # default root is :data
447
+ end
448
+
449
+ class UserSerializer < Serega
450
+ plugin :root, root: :users
451
+ end
452
+
453
+ class UserSerializer < Serega
454
+ plugin :root, root_one: :user, root_many: :people
455
+ end
456
+
457
+ class UserSerializer < Serega
458
+ plugin :root, root: nil # no root by default
459
+ end
460
+ ```
461
+
462
+ ```ruby
463
+ # @example Change root per serialization:
464
+
465
+ class UserSerializer < Serega
466
+ plugin :root
467
+ end
468
+
469
+ UserSerializer.to_h(nil) # => {:data=>nil}
470
+ UserSerializer.to_h(nil, root: :user) # => {:user=>nil}
471
+ UserSerializer.to_h(nil, root: nil) # => nil
472
+ ```
473
+
474
+ ### Plugin :metadata
475
+
476
+ Depends on: [`:root`][root] plugin, that must be loaded first
477
+
478
+ Adds ability to describe metadata and adds it to serialized response
479
+
480
+ Added class-level method `:meta_attribute`, to define metadata, it accepts:
481
+ - *path [Array<Symbol>] - nested hash keys beginning from the root object.
482
+ - **options [Hash] - defaults are `hide_nil: false, hide_empty: false`
483
+ - &block [Proc] - describes value for current meta attribute
484
+
485
+ ```ruby
486
+ class AppSerializer < Serega
487
+ plugin :root
488
+ plugin :metadata
489
+
490
+ meta_attribute(:version) { '1.2.3' }
491
+ meta_attribute(:ab_tests, :names) { %i[foo bar] }
492
+ meta_attribute(:meta, :paging, hide_nil: true) do |records, ctx|
493
+ next unless records.respond_to?(:total_count)
494
+
495
+ { page: records.page, per_page: records.per_page, total_count: records.total_count }
496
+ end
497
+ end
498
+
499
+ AppSerializer.to_h(nil) # => {:data=>nil, :version=>"1.2.3", :ab_tests=>{:names=>[:foo, :bar]}}
500
+ ```
501
+
502
+ ### Plugin :context_metadata
503
+
504
+ Depends on: [`:root`][root] plugin, that must be loaded first
505
+
506
+ Allows to provide metadata and attach it to serialized response.
507
+
508
+ Accepts option `:context_metadata_key` with name of keyword that must be used to provide metadata. By default it is `:meta`
509
+
510
+ Key can be changed in children serializers using config `config.context_metadata.key=(value)`
511
+
512
+ ```ruby
513
+ class UserSerializer < Serega
514
+ plugin :root, root: :data
515
+ plugin :context_metadata, context_metadata_key: :meta
516
+
517
+ # Same:
518
+ # plugin :context_metadata
519
+ # config.context_metadata.key = :meta
520
+ end
521
+
522
+ UserSerializer.to_h(nil, meta: { version: '1.0.1' })
523
+ # => {:data=>nil, :version=>"1.0.1"}
524
+ ```
525
+
526
+ ### Plugin :formatters
527
+
528
+ Allows to define `formatters` and apply them on attributes.
529
+
530
+ Config option `config.formatters.add` can be used to add formatters.
531
+
532
+ Attribute option `:format` now can be used with name of formatter or with callable instance.
533
+
534
+ ```ruby
535
+ class AppSerializer < Serega
536
+ plugin :formatters, formatters: {
537
+ iso8601: ->(value) { time.iso8601.round(6) },
538
+ on_off: ->(value) { value ? 'ON' : 'OFF' },
539
+ money: ->(value) { value.round(2) }
540
+ }
541
+ end
542
+
543
+ class UserSerializer < Serega
544
+ # Additionally we can add formatters via config in subclasses
545
+ config.formatters.add(
546
+ iso8601: ->(value) { time.iso8601.round(6) },
547
+ on_off: ->(value) { value ? 'ON' : 'OFF' },
548
+ money: ->(value) { value.round(2) }
549
+ )
550
+
551
+ # Using predefined formatter
552
+ attribute :commission, format: :money
553
+ attribute :is_logined, format: :on_off
554
+ attribute :created_at, format: :iso8601
555
+ attribute :updated_at, format: :iso8601
556
+
557
+ # Using `callable` formatter
558
+ attribute :score_percent, format: PercentFormmatter # callable class
559
+ attribute :score_percent, format: proc { |percent| "#{percent.round(2)}%" }
560
+ end
561
+ ```
562
+
563
+ ### Plugin :presenter
564
+
565
+ Helps to write clear code by adding attribute names as methods to Presenter
566
+
567
+ ```ruby
568
+ class UserSerializer < Serega
569
+ plugin :presenter
570
+
571
+ attribute :name
572
+ attribute :address
573
+
574
+ class Presenter
575
+ def name
576
+ [first_name, last_name].compact_blank.join(' ')
577
+ end
578
+
579
+ def address
580
+ [country, city, address].join("\n")
581
+ end
582
+ end
583
+ end
584
+ ```
585
+
586
+ ### Plugin :string_modifiers
587
+
588
+ Allows to specify modifiers as strings.
589
+
590
+ Serialized attributes must be split with `,` and nested attributes must be defined inside brackets `(`, `)`.
591
+
592
+ Modifiers can still be provided old way with nested hashes or arrays.
593
+
594
+ ```ruby
595
+ PostSerializer.plugin :string_modifiers
596
+ PostSerializer.new(only: "id,user(id,username)").to_h(post)
597
+ PostSerializer.new(except: "user(username,email)").to_h(post)
598
+ PostSerializer.new(with: "user(email)").to_h(post)
599
+
600
+ # Modifiers can still be provided old way with nested hashes or arrays.
601
+ PostSerializer.new(with: {user: %i[email, username]}).to_h(post)
602
+ ```
603
+
604
+ ### Plugin :hide_nil
605
+
606
+ Allows to hide attributes with `nil` values
607
+
608
+ ```ruby
609
+ class UserSerializer < Serega
610
+ plugin :hide_nil
611
+
612
+ attribute :email, hide_nil: true
613
+ end
614
+ ```
615
+
616
+ ## Errors
617
+
618
+ - `Serega::SeregaError` is a base error raised by this gem.
619
+ - `Serega::AttributeNotExist` error is raised when validating attributes in `:only, :except, :with` modifiers
620
+
621
+ ## Release
622
+
623
+ To release a new version, read [RELEASE.md](https://github.com/aglushkov/serega/blob/master/RELEASE.md).
624
+
625
+ ## Development
626
+
627
+ - `bundle install` - install dependencies
628
+ - `bin/console` - open irb console with loaded gems
629
+ - `bundle exec rspec` - run tests
630
+ - `bundle exec rubocop` - check code standards
631
+ - `yard stats --list-undoc --no-cache` - view undocumented code
632
+ - `yard server --reload` - view code documentation
633
+
634
+ ## Contributing
635
+
636
+ Bug reports, pull requests and improvements ideas are very welcome!
637
+
638
+ ## License
639
+
640
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
641
+
642
+
643
+ [activerecord_preloads]: #plugin-activerecord_preloads
644
+ [batch]: #plugin-batch
645
+ [context_metadata]: #plugin-context_metadata
646
+ [formatters]: #plugin-formatters
647
+ [metadata]: #plugin-metadata
648
+ [preloads]: #plugin-preloads
649
+ [presenter]: #plugin-presenter
650
+ [root]: #plugin-root
651
+ [string_modifiers]: #plugin-string_modifiers
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.6.1
1
+ 0.8.0