serega 0.7.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '001918eece6824f604d5bdd2a43321346d474355fd629759eaa894dabf94d7f4'
4
- data.tar.gz: e68ee37be935e8c4a9bc2eca849a6d56b68feb56155519a612976cddd2013871
3
+ metadata.gz: b2ebcd147c1714ebb2b4e923ee179c788158b225ac76791817229a82975926ec
4
+ data.tar.gz: eef2621b3d6b9ac6ee1da3de29d3006ff99a0c2c91082cdc7795e82665ee92e5
5
5
  SHA512:
6
- metadata.gz: 4683bc9c753fbbcfc40c31e7a2833bddfe3a01711067a718ad9a01f263d8e7e576b768432e33dcaad688ddd2d3017e60ebfc3111d22a661d6f670ae465f9210c
7
- data.tar.gz: ad5fd5c647484d9b68053e28a0e8a728e285727b7419e797cf89f73ea00abaf226c6c59d137afb7ba41b47fc1490b4948e11128d059257ed431da0574657998e
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.7.0
1
+ 0.8.0
@@ -9,12 +9,15 @@ class Serega
9
9
  # Attribute instance methods
10
10
  #
11
11
  module AttributeInstanceMethods
12
+ # Returns attribute name
12
13
  # @return [Symbol] Attribute name
13
14
  attr_reader :name
14
15
 
16
+ # Returns attribute options
15
17
  # @return [Hash] Attribute options
16
18
  attr_reader :opts
17
19
 
20
+ # Returns attribute block
18
21
  # @return [Proc] Attribute originally added block
19
22
  attr_reader :block
20
23
 
@@ -40,26 +43,32 @@ class Serega
40
43
  @block = block
41
44
  end
42
45
 
43
- # @return [Symbol] Object method name to will be used to get attribute value unless block provided
46
+ # Method name that will be used to get attribute value
47
+ #
48
+ # @return [Symbol] key
44
49
  def key
45
50
  @key ||= opts.key?(:key) ? opts[:key].to_sym : name
46
51
  end
47
52
 
48
- # @return [Boolean, nil] Attribute initial :hide option value
53
+ # Shows current opts[:hide] option
54
+ # @return [Boolean, nil] Attribute :hide option value
49
55
  def hide
50
56
  opts[:hide]
51
57
  end
52
58
 
53
- # @return [Boolean, nil] Attribute initialization :many option
59
+ # Shows current opts[:many] option
60
+ # @return [Boolean, nil] Attribute :many option value
54
61
  def many
55
62
  opts[:many]
56
63
  end
57
64
 
65
+ # Shows whether attribute has specified serializer
58
66
  # @return [Boolean] Checks if attribute is relationship (if :serializer option exists)
59
67
  def relation?
60
68
  !opts[:serializer].nil?
61
69
  end
62
70
 
71
+ # Shows specified serializer class
63
72
  # @return [Serega, nil] Attribute serializer if exists
64
73
  def serializer
65
74
  return @serializer if instance_variable_defined?(:@serializer)
@@ -73,6 +82,7 @@ class Serega
73
82
  end
74
83
  end
75
84
 
85
+ # Returns final block that will be used to find attribute value
76
86
  # @return [Proc] Proc to find attribute value
77
87
  def value_block
78
88
  return @value_block if instance_variable_defined?(:@value_block)
@@ -130,12 +140,13 @@ class Serega
130
140
  end
131
141
 
132
142
  def delegate_block
133
- return unless opts.key?(:delegate)
143
+ delegate = opts[:delegate]
144
+ return unless delegate
134
145
 
135
- key_method_name = key
136
- delegate_to = opts[:delegate][:to]
146
+ key_method_name = delegate[:key] || key
147
+ delegate_to = delegate[:to]
137
148
 
138
- if opts[:delegate][:allow_nil]
149
+ if delegate[:allow_nil]
139
150
  proc { |object| object.public_send(delegate_to)&.public_send(key_method_name) }
140
151
  else
141
152
  proc { |object| object.public_send(delegate_to).public_send(key_method_name) }
data/lib/serega/config.rb CHANGED
@@ -8,6 +8,10 @@ class Serega
8
8
  #
9
9
  class SeregaConfig
10
10
  # :nocov: We can't use both :oj and :json adapters together
11
+
12
+ #
13
+ # Default config options
14
+ #
11
15
  DEFAULTS = {
12
16
  plugins: [],
13
17
  initiate_keys: %i[only with except check_initiate_params].freeze,
@@ -23,7 +27,9 @@ class Serega
23
27
  # SeregaConfig Instance methods
24
28
  module SeregaConfigInstanceMethods
25
29
  #
26
- # @return [Hash] All config options
30
+ # Shows current config as Hash
31
+ #
32
+ # @return [Hash] config options
27
33
  #
28
34
  attr_reader :opts
29
35
 
@@ -37,31 +43,41 @@ class Serega
37
43
  @opts = SeregaUtils::EnumDeepDup.call(opts)
38
44
  end
39
45
 
46
+ #
47
+ # Shows used plugins
48
+ #
40
49
  # @return [Array] Used plugins
50
+ #
41
51
  def plugins
42
52
  opts.fetch(:plugins)
43
53
  end
44
54
 
45
- # @return [Array<Symbol>] Allowed options keys for serializer initialization
55
+ # Returns options names allowed in `Serega#new` method
56
+ # @return [Array<Symbol>] allowed options keys
46
57
  def initiate_keys
47
58
  opts.fetch(:initiate_keys)
48
59
  end
49
60
 
61
+ # Returns options names allowed in `Serega.attribute` method
50
62
  # @return [Array<Symbol>] Allowed options keys for attribute initialization
51
63
  def attribute_keys
52
64
  opts.fetch(:attribute_keys)
53
65
  end
54
66
 
67
+ # Returns options names allowed in `to_h, to_json, as_json` methods
55
68
  # @return [Array<Symbol>] Allowed options keys for serialization
56
69
  def serialize_keys
57
70
  opts.fetch(:serialize_keys)
58
71
  end
59
72
 
73
+ # Returns :check_initiate_params config option
60
74
  # @return [Boolean] Current :check_initiate_params config option
61
75
  def check_initiate_params
62
76
  opts.fetch(:check_initiate_params)
63
77
  end
64
78
 
79
+ # Sets :check_initiate_params config option
80
+ #
65
81
  # @param value [Boolean] Set :check_initiate_params config option
66
82
  #
67
83
  # @return [Boolean] :check_initiate_params config option
@@ -70,11 +86,14 @@ class Serega
70
86
  opts[:check_initiate_params] = value
71
87
  end
72
88
 
89
+ # Returns :max_cached_map_per_serializer_count config option
73
90
  # @return [Boolean] Current :max_cached_map_per_serializer_count config option
74
91
  def max_cached_map_per_serializer_count
75
92
  opts.fetch(:max_cached_map_per_serializer_count)
76
93
  end
77
94
 
95
+ # Sets :max_cached_map_per_serializer_count config option
96
+ #
78
97
  # @param value [Boolean] Set :check_initiate_params config option
79
98
  #
80
99
  # @return [Boolean] New :max_cached_map_per_serializer_count config option
@@ -83,22 +102,26 @@ class Serega
83
102
  opts[:max_cached_map_per_serializer_count] = value
84
103
  end
85
104
 
105
+ # Returns current `to_json` adapter
86
106
  # @return [#call] Callable that used to construct JSON
87
107
  def to_json
88
108
  opts.fetch(:to_json)
89
109
  end
90
110
 
111
+ # Sets current `to_json` adapter
91
112
  # @param value [#call] Callable that used to construct JSON
92
113
  # @return [#call] Provided callable object
93
114
  def to_json=(value)
94
115
  opts[:to_json] = value
95
116
  end
96
117
 
118
+ # Returns current `from_json` adapter
97
119
  # @return [#call] Callable that used to parse JSON
98
120
  def from_json
99
121
  opts.fetch(:from_json)
100
122
  end
101
123
 
124
+ # Sets current `from_json` adapter
102
125
  # @param value [#call] Callable that used to parse JSON
103
126
  # @return [#call] Provided callable object
104
127
  def from_json=(value)
@@ -9,6 +9,7 @@ class Serega
9
9
  # Stores link to current serializer class
10
10
  #
11
11
  module SerializerClassHelper
12
+ # Shows serializer class current class is namespaced under
12
13
  # @return [Class<Serega>] Serializer class that current class is namespaced under.
13
14
  attr_accessor :serializer_class
14
15
  end
@@ -11,9 +11,11 @@ class Serega
11
11
  module InstanceMethods
12
12
  extend Forwardable
13
13
 
14
+ # Shows current attribute
14
15
  # @return [Serega::SeregaAttribute] Current attribute
15
16
  attr_reader :attribute
16
17
 
18
+ # Shows nested points
17
19
  # @return [NilClass, Array<Serega::SeregaMapPoint>] Nested points or nil
18
20
  attr_reader :nested_points
19
21
 
@@ -22,6 +22,8 @@ class Serega
22
22
  preload_handler.preload(object, preloads)
23
23
  end
24
24
 
25
+ # Returns handlers which will try to check if serialized object fits for preloading using this handler
26
+ #
25
27
  # @return [Array] Registered preload adapters for different types of initial records
26
28
  def handlers
27
29
  @handlers ||= [
@@ -35,6 +35,7 @@ class Serega
35
35
  # end
36
36
  #
37
37
  module Batch
38
+ # Returns plugin name
38
39
  # @return [Symbol] Plugin name
39
40
  def self.plugin_name
40
41
  :batch
@@ -159,6 +160,7 @@ class Serega
159
160
  @many = many
160
161
  end
161
162
 
163
+ # Returns proc that will be used to batch load registered keys values
162
164
  # @return [#call] batch loader
163
165
  def loader
164
166
  @batch_loader ||= begin
@@ -168,6 +170,7 @@ class Serega
168
170
  end
169
171
  end
170
172
 
173
+ # Returns proc that will be used to find batch_key for current attribute.
171
174
  # @return [Object] key (uid) of batch loaded object
172
175
  def key
173
176
  @batch_key ||= begin
@@ -176,6 +179,7 @@ class Serega
176
179
  end
177
180
  end
178
181
 
182
+ # Returns default value to use if batch loader does not return value for some key
179
183
  # @return [Object] default value for missing key
180
184
  def default_value
181
185
  if opts.key?(:default)
@@ -192,6 +196,8 @@ class Serega
192
196
  # @see Serega::SeregaConfig
193
197
  #
194
198
  module ConfigInstanceMethods
199
+ #
200
+ # Returns all batch loaders registered for current serializer
195
201
  #
196
202
  # @return [Serega::SeregaPlugins::Batch::BatchLoadersConfig] configuration for batch loaders
197
203
  #
@@ -253,11 +259,13 @@ class Serega
253
259
  #
254
260
  # Serega::SeregaMapPoint additional/patched class methods
255
261
  #
256
- # @see Serega::SeregaAttribute
262
+ # @see SeregaAttribute
257
263
  #
258
264
  module MapPointInstanceMethods
259
265
  #
260
- # @return [Serega::Batch::BatchModel] batch model that encapsulates everything needed to load current batch
266
+ # Returns BatchModel, an object that encapsulates all batch_loader methods for current point
267
+ #
268
+ # @return [BatchModel] batch model that encapsulates everything needed to load current batch
261
269
  #
262
270
  def batch
263
271
  return @batch if instance_variable_defined?(:@batch)
@@ -272,7 +280,7 @@ class Serega
272
280
  #
273
281
  # Serega additional/patched instance methods
274
282
  #
275
- # @see Serega
283
+ # @see Serega::InstanceMethods
276
284
  #
277
285
  module InstanceMethods
278
286
  private
@@ -32,6 +32,7 @@ class Serega
32
32
 
33
33
  delegate_opts = opts[:delegate]
34
34
  check_opt_delegate_to(delegate_opts)
35
+ check_opt_delegate_key(delegate_opts)
35
36
  check_opt_delegate_allow_nil(delegate_opts)
36
37
  end
37
38
 
@@ -42,13 +43,16 @@ class Serega
42
43
  Utils::CheckOptIsStringOrSymbol.call(delegate_opts, :to)
43
44
  end
44
45
 
45
- def check_opt_delegate_allow_nil(delegate_opts)
46
- return unless delegate_opts.key?(:allow_nil)
46
+ def check_opt_delegate_key(delegate_opts)
47
+ Utils::CheckOptIsStringOrSymbol.call(delegate_opts, :key)
48
+ end
47
49
 
50
+ def check_opt_delegate_allow_nil(delegate_opts)
48
51
  Utils::CheckOptIsBool.call(delegate_opts, :allow_nil)
49
52
  end
50
53
 
51
54
  def check_usage_with_other_params(opts, block)
55
+ raise SeregaError, "Option :delegate can not be used together with option :key" if opts.key?(:key)
52
56
  raise SeregaError, "Option :delegate can not be used together with option :const" if opts.key?(:const)
53
57
  raise SeregaError, "Option :delegate can not be used together with option :value" if opts.key?(:value)
54
58
  raise SeregaError, "Option :delegate can not be used together with block" if block
data/lib/serega.rb CHANGED
@@ -4,9 +4,11 @@ require_relative "serega/version"
4
4
 
5
5
  # Parent class for your serializers
6
6
  class Serega
7
+ # Frozen hash
7
8
  # @return [Hash] frozen hash
8
9
  FROZEN_EMPTY_HASH = {}.freeze
9
10
 
11
+ # Frozen array
10
12
  # @return [Array] frozen array
11
13
  FROZEN_EMPTY_ARRAY = [].freeze
12
14
  end
@@ -64,6 +66,7 @@ class Serega
64
66
  # Serializers class methods
65
67
  #
66
68
  module ClassMethods
69
+ # Returns current config
67
70
  # @return [SeregaConfig] current serializer config
68
71
  attr_reader :config
69
72
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serega
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Glushkov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-07 00:00:00.000000000 Z
11
+ date: 2022-12-08 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  JSON Serializer
@@ -25,6 +25,7 @@ executables: []
25
25
  extensions: []
26
26
  extra_rdoc_files: []
27
27
  files:
28
+ - README.md
28
29
  - VERSION
29
30
  - lib/serega.rb
30
31
  - lib/serega/attribute.rb