serega 0.40.1 → 0.41.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 +4 -4
- data/README.md +114 -1
- data/VERSION +1 -1
- data/lib/serega/plugins/root/root.rb +2 -0
- data/lib/serega.rb +89 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4d54f033e70155c517c02d15cd9a4eb5399e385b0ffdd68d8c6c2b390201a007
|
|
4
|
+
data.tar.gz: 23257bef39a343bea74c063f452fa1feed58738c454e467838cceb2fecdbe862
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2a0df718af2b8f019af2fe9dc8bb82dc21a03631a211ecc00a3fd4e83d6ed3324505727c9124975181869fd11fb452e7670a060ae7e04eddd161186e3353366f
|
|
7
|
+
data.tar.gz: e5bc125a847e929bdaeb7ce044adbb4e866324d702032ee19abd76208949f406b166f4ecf111453d0f7130aa3dfb7d36e3269010df8adc7e05c50eba3c3a52cd
|
data/README.md
CHANGED
|
@@ -19,6 +19,7 @@ It has some great features:
|
|
|
19
19
|
- Secure from malicious queries with [depth_limit][depth_limit] plugin
|
|
20
20
|
- Solutions for N+1 problem (via built-in [batch loading](#batch-loading), [preloads][preloads] or
|
|
21
21
|
[activerecord_preloads][activerecord_preloads] plugin)
|
|
22
|
+
- Load serialized objects by ids ([prepare_initial_objects](#prepare-initial-objects))
|
|
22
23
|
- Built-in object presenter ([presenter][presenter] plugin)
|
|
23
24
|
- Adding custom metadata (via [metadata][metadata] or
|
|
24
25
|
[context_metadata][context_metadata] plugins)
|
|
@@ -29,10 +30,51 @@ It has some great features:
|
|
|
29
30
|
- Auto camelCase keys - [camel_case][camel_case] plugin
|
|
30
31
|
- Serializing Hash records - [hash_access][hash_access] attribute option
|
|
31
32
|
|
|
33
|
+
## Table of Contents
|
|
34
|
+
|
|
35
|
+
<!-- toc -->
|
|
36
|
+
|
|
37
|
+
- [Installation](#installation)
|
|
38
|
+
- [Usage](#usage)
|
|
39
|
+
- [Define serializers](#define-serializers)
|
|
40
|
+
- [Adding attributes](#adding-attributes)
|
|
41
|
+
- [Defining a nested serializer with a block](#defining-a-nested-serializer-with-a-block)
|
|
42
|
+
- [Serializing](#serializing)
|
|
43
|
+
- [Selecting Fields](#selecting-fields)
|
|
44
|
+
- [Using Context](#using-context)
|
|
45
|
+
- [Batch Loading](#batch-loading)
|
|
46
|
+
- [Prepare Initial Objects](#prepare-initial-objects)
|
|
47
|
+
- [Configuration](#configuration)
|
|
48
|
+
- [Preloads](#preloads)
|
|
49
|
+
- [Serializing the same object in association](#serializing-the-same-object-in-association)
|
|
50
|
+
- [Custom preloading](#custom-preloading)
|
|
51
|
+
- [Serializing Hash records](#serializing-hash-records)
|
|
52
|
+
- [Plugins](#plugins)
|
|
53
|
+
- [Plugin :activerecord_preloads](#plugin-activerecord_preloads)
|
|
54
|
+
- [Plugin :root](#plugin-root)
|
|
55
|
+
- [Plugin :metadata](#plugin-metadata)
|
|
56
|
+
- [Plugin :context_metadata](#plugin-context_metadata)
|
|
57
|
+
- [Plugin :formatters](#plugin-formatters)
|
|
58
|
+
- [Plugin :presenter](#plugin-presenter)
|
|
59
|
+
- [Plugin :string_modifiers](#plugin-string_modifiers)
|
|
60
|
+
- [Plugin :if](#plugin-if)
|
|
61
|
+
- [Plugin :camel_case](#plugin-camel_case)
|
|
62
|
+
- [Plugin :depth_limit](#plugin-depth_limit)
|
|
63
|
+
- [Plugin :explicit_many_option](#plugin-explicit_many_option)
|
|
64
|
+
- [Errors](#errors)
|
|
65
|
+
- [Release](#release)
|
|
66
|
+
- [Development](#development)
|
|
67
|
+
- [Contributing](#contributing)
|
|
68
|
+
- [License](#license)
|
|
69
|
+
|
|
70
|
+
<!-- tocstop -->
|
|
71
|
+
|
|
32
72
|
## Installation
|
|
33
73
|
|
|
34
74
|
`bundle add serega`
|
|
35
75
|
|
|
76
|
+
## Usage
|
|
77
|
+
|
|
36
78
|
### Define serializers
|
|
37
79
|
|
|
38
80
|
Most apps should define **base serializer** with common plugins and settings to
|
|
@@ -463,6 +505,77 @@ class UserSerializer < Serega
|
|
|
463
505
|
end
|
|
464
506
|
```
|
|
465
507
|
|
|
508
|
+
### Prepare Initial Objects
|
|
509
|
+
|
|
510
|
+
`prepare_initial_objects` replaces the serialized objects before serialization
|
|
511
|
+
starts, so a serializer can accept ids or other references and load the records
|
|
512
|
+
itself.
|
|
513
|
+
|
|
514
|
+
```ruby
|
|
515
|
+
class UserSerializer < Serega
|
|
516
|
+
# OccamsRecord returns read-only records — faster and with a much lower
|
|
517
|
+
# memory footprint than ActiveRecord models
|
|
518
|
+
prepare_initial_objects { |user_ids| OccamsRecord.query(User.where(id: user_ids)).run }
|
|
519
|
+
|
|
520
|
+
attribute :first_name
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
UserSerializer.to_h(["17", "42"]) # => [{first_name: "Ann"}, {first_name: "Bob"}]
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
The handler accepts the serialization context as a second positional or a
|
|
527
|
+
`:ctx` keyword argument, and can be provided as a callable value:
|
|
528
|
+
|
|
529
|
+
```ruby
|
|
530
|
+
class UserSerializer < Serega
|
|
531
|
+
prepare_initial_objects { |ids, ctx| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }
|
|
532
|
+
# or
|
|
533
|
+
prepare_initial_objects { |ids, ctx:| OccamsRecord.query(User.where(id: ids, account: ctx[:account])).run }
|
|
534
|
+
# or
|
|
535
|
+
prepare_initial_objects UsersLoader
|
|
536
|
+
end
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
The handler runs once per serialization, before the `:many` option is detected,
|
|
540
|
+
so it can turn a single object into a collection and back:
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
class UserSerializer < Serega
|
|
544
|
+
prepare_initial_objects { |user_id| OccamsRecord.query(User.where(id: user_id)).run }
|
|
545
|
+
|
|
546
|
+
attribute :first_name
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
UserSerializer.to_h("17") # => [{first_name: "Ann"}] - an array, as the handler returned a collection
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
A provided `:many` option is used as is. A handler returning `nil` serializes
|
|
553
|
+
`nil`, and errors raised inside the handler are not wrapped.
|
|
554
|
+
|
|
555
|
+
Declared preloads are applied to the prepared objects, so the handler pairs
|
|
556
|
+
with [preloads][preloads] and the
|
|
557
|
+
[activerecord_preloads][activerecord_preloads] plugin:
|
|
558
|
+
|
|
559
|
+
```ruby
|
|
560
|
+
class UserSerializer < Serega
|
|
561
|
+
plugin :activerecord_preloads
|
|
562
|
+
|
|
563
|
+
prepare_initial_objects { |user_ids| User.where(id: user_ids) }
|
|
564
|
+
|
|
565
|
+
attribute :albums_count, preload: :albums, value: proc { |user| user.albums.size }
|
|
566
|
+
end
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
⚠️ The `:preload` option needs ActiveRecord objects. When the handler returns
|
|
570
|
+
anything else — OccamsRecord read-only records, Structs, Hashes, plain objects
|
|
571
|
+
— preloading raises `Serega::SeregaError` (`Can't preload ... to ...`) during
|
|
572
|
+
serialization. Load that data with [batch loading](#batch-loading) instead,
|
|
573
|
+
which works with any objects.
|
|
574
|
+
|
|
575
|
+
The handler runs for the serialized objects only, and not for objects of nested
|
|
576
|
+
serializers. It is inherited by subclasses, so declare it on concrete
|
|
577
|
+
serializers rather than on a base serializer shared by all of them.
|
|
578
|
+
|
|
466
579
|
## Configuration
|
|
467
580
|
|
|
468
581
|
Here are the default options. Other options can be added with plugins.
|
|
@@ -592,7 +705,7 @@ end
|
|
|
592
705
|
|
|
593
706
|
---
|
|
594
707
|
|
|
595
|
-
###
|
|
708
|
+
### Serializing the same object in association
|
|
596
709
|
|
|
597
710
|
For example, you show your current user as "user" and use the same user object
|
|
598
711
|
to serialize "user_stats". `UserStatSerializer` relies on user fields and any
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.41.0
|
|
@@ -229,6 +229,8 @@ class Serega
|
|
|
229
229
|
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
230
230
|
#
|
|
231
231
|
def to_data(object, opts = nil)
|
|
232
|
+
opts = normalize_serialization_opts(opts)
|
|
233
|
+
object = prepare_objects(object, opts[:context])
|
|
232
234
|
opts = prepare_initial_serialization_opts(object, opts)
|
|
233
235
|
serialized_data = serialize(object, opts)
|
|
234
236
|
self.class::SeregaDataBuilder.call(self, serialized_data, opts)
|
data/lib/serega.rb
CHANGED
|
@@ -265,6 +265,63 @@ class Serega
|
|
|
265
265
|
@preload_with = handler
|
|
266
266
|
end
|
|
267
267
|
|
|
268
|
+
#
|
|
269
|
+
# Registers (or returns) the handler that replaces the serialized objects
|
|
270
|
+
# before serialization starts.
|
|
271
|
+
#
|
|
272
|
+
# The handler is called once per serialization with the objects provided to
|
|
273
|
+
# `.call`/`.to_h`/`.to_data` and the serialization context. Its result is
|
|
274
|
+
# serialized instead of the provided objects, which allows to accept ids or
|
|
275
|
+
# other references and load the records in one place.
|
|
276
|
+
#
|
|
277
|
+
# The handler runs before the `:many` option is detected, so it may turn a
|
|
278
|
+
# single object into a collection and back. A provided `:many` serialization
|
|
279
|
+
# option is still used as is.
|
|
280
|
+
#
|
|
281
|
+
# The handler runs only for the serialized objects, and not for objects of
|
|
282
|
+
# nested serializers.
|
|
283
|
+
#
|
|
284
|
+
# @example with a block
|
|
285
|
+
# prepare_initial_objects { |user_ids| User.where(id: user_ids) }
|
|
286
|
+
#
|
|
287
|
+
# @example with a context
|
|
288
|
+
# prepare_initial_objects { |user_ids, ctx| User.where(id: user_ids, account: ctx[:account]) }
|
|
289
|
+
#
|
|
290
|
+
# @example with a keyword context
|
|
291
|
+
# prepare_initial_objects { |user_ids, ctx:| User.where(id: user_ids, account: ctx[:account]) }
|
|
292
|
+
#
|
|
293
|
+
# @example with a callable value
|
|
294
|
+
# prepare_initial_objects UsersLoader
|
|
295
|
+
#
|
|
296
|
+
# @param value [#call, nil] Handler accepting (objects), (objects, context) or (objects, ctx:)
|
|
297
|
+
# @param block [Proc] Handler accepting (objects), (objects, context) or (objects, ctx:)
|
|
298
|
+
#
|
|
299
|
+
# @return [#call, nil] The registered handler
|
|
300
|
+
#
|
|
301
|
+
def prepare_initial_objects(value = nil, &block)
|
|
302
|
+
return @prepare_initial_objects if value.nil? && block.nil?
|
|
303
|
+
raise SeregaError, "prepare_initial_objects accepts a single callable or a block, not both" if value && block
|
|
304
|
+
|
|
305
|
+
handler = value || block
|
|
306
|
+
raise SeregaError, "prepare_initial_objects value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
|
|
307
|
+
|
|
308
|
+
signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2, keyword_args: [:ctx])
|
|
309
|
+
raise SeregaError, prepare_initial_objects_signature_error unless %w[1 2 1_ctx].include?(signature)
|
|
310
|
+
|
|
311
|
+
@prepare_initial_objects_signature = signature
|
|
312
|
+
@prepare_initial_objects = handler
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
#
|
|
316
|
+
# Signature of the registered prepare_initial_objects handler
|
|
317
|
+
#
|
|
318
|
+
# @return [String, nil] Handler signature
|
|
319
|
+
#
|
|
320
|
+
# @private
|
|
321
|
+
def prepare_initial_objects_signature
|
|
322
|
+
@prepare_initial_objects_signature
|
|
323
|
+
end
|
|
324
|
+
|
|
268
325
|
#
|
|
269
326
|
# Serializes provided object to Hash
|
|
270
327
|
#
|
|
@@ -386,8 +443,20 @@ class Serega
|
|
|
386
443
|
# Assign same preload handler
|
|
387
444
|
subclass.preload_with(preload_with) if preload_with
|
|
388
445
|
|
|
446
|
+
# Assign same initial objects handler
|
|
447
|
+
subclass.prepare_initial_objects(prepare_initial_objects) if prepare_initial_objects
|
|
448
|
+
|
|
389
449
|
super
|
|
390
450
|
end
|
|
451
|
+
|
|
452
|
+
def prepare_initial_objects_signature_error
|
|
453
|
+
<<~ERR.strip
|
|
454
|
+
prepare_initial_objects handler arguments should have one of this signatures:
|
|
455
|
+
- (objects) # one argument
|
|
456
|
+
- (objects, ctx) # two arguments
|
|
457
|
+
- (objects, ctx:) # one argument and one :ctx keyword argument
|
|
458
|
+
ERR
|
|
459
|
+
end
|
|
391
460
|
end
|
|
392
461
|
|
|
393
462
|
#
|
|
@@ -436,6 +505,8 @@ class Serega
|
|
|
436
505
|
# @return [Hash] Serialization result
|
|
437
506
|
#
|
|
438
507
|
def call(object, opts = nil)
|
|
508
|
+
opts = normalize_serialization_opts(opts)
|
|
509
|
+
object = prepare_objects(object, opts[:context])
|
|
439
510
|
opts = prepare_initial_serialization_opts(object, opts)
|
|
440
511
|
serialize(object, opts)
|
|
441
512
|
end
|
|
@@ -458,6 +529,8 @@ class Serega
|
|
|
458
529
|
# @return [Data] Serialization result
|
|
459
530
|
#
|
|
460
531
|
def to_data(object, opts = nil)
|
|
532
|
+
opts = normalize_serialization_opts(opts)
|
|
533
|
+
object = prepare_objects(object, opts[:context])
|
|
461
534
|
opts = prepare_initial_serialization_opts(object, opts)
|
|
462
535
|
serialized_data = serialize(object, opts)
|
|
463
536
|
self.class::SeregaDataBuilder.call(self, serialized_data)
|
|
@@ -488,11 +561,26 @@ class Serega
|
|
|
488
561
|
SeregaUtils::ToHash.call(value)
|
|
489
562
|
end
|
|
490
563
|
|
|
491
|
-
def
|
|
564
|
+
def normalize_serialization_opts(opts)
|
|
492
565
|
opts = opts ? opts.transform_keys(&:to_sym) : {}
|
|
493
566
|
self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
|
|
494
567
|
|
|
495
568
|
opts[:context] ||= {}
|
|
569
|
+
opts
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def prepare_objects(objects, context)
|
|
573
|
+
handler = self.class.prepare_initial_objects
|
|
574
|
+
return objects unless handler
|
|
575
|
+
|
|
576
|
+
case self.class.prepare_initial_objects_signature
|
|
577
|
+
when "1" then handler.call(objects)
|
|
578
|
+
when "2" then handler.call(objects, context)
|
|
579
|
+
else handler.call(objects, ctx: context) # "1_ctx"
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def prepare_initial_serialization_opts(object, opts)
|
|
496
584
|
opts[:level_queue] = SeregaEngine::LevelQueue.new
|
|
497
585
|
opts[:many] = SeregaUtils::CollectionDetector.call(object) unless opts.key?(:many)
|
|
498
586
|
opts[:plan] = plan
|