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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edecae0c35540be687e394092e3b0a158fa1c2aeff4710ff5ce8e17effba1eb2
4
- data.tar.gz: 7e63d238aad501f31be388824bb6b594e9f7988e17c49e4fc478d9353993c63b
3
+ metadata.gz: 4d54f033e70155c517c02d15cd9a4eb5399e385b0ffdd68d8c6c2b390201a007
4
+ data.tar.gz: 23257bef39a343bea74c063f452fa1feed58738c454e467838cceb2fecdbe862
5
5
  SHA512:
6
- metadata.gz: 506607362308145ffcc0a34474d76d8b0a0edb7fbb570deb1fb973c71c396aab65a1b7b408069d31c8a5e15f8c8146316f835194c53e4a68315fe62eb4992421
7
- data.tar.gz: 6d5b5ebe921a36b197b4829721af89eb92f5e47522101d126cffc8c72e30ad2679a6f9084df2685cf2250d772c5b0974b76861260b2eb19c6f53660fba16f5d4
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
- ### SPECIFIC CASE: Serializing the same object in association
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.40.1
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 prepare_initial_serialization_opts(object, opts)
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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serega
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.40.1
4
+ version: 0.41.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey