serega 0.6.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
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