serega 0.40.1 → 0.42.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 +174 -60
- data/VERSION +1 -1
- data/lib/serega/config.rb +1 -1
- data/lib/serega/object_serializer.rb +10 -3
- data/lib/serega/plan_point.rb +7 -1
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +1 -20
- data/lib/serega/plugins/root/root.rb +2 -0
- data/lib/serega/plugins.rb +2 -2
- data/lib/serega/presenter.rb +66 -0
- data/lib/serega.rb +130 -5
- metadata +2 -2
- data/lib/serega/plugins/presenter/presenter.rb +0 -242
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4ec97ed9e6fc97612c5094991de19b1c5d43a44168e5a29593bdf75f77e5f200
|
|
4
|
+
data.tar.gz: 54d42defad67c2ea46bac516870caacb36ce38643826137900b46a8661001cc3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5c9f5c91fc97ae0b08578b6fb7c98c3718194094af81fb6175cd3854f602b8daa450204cd75b054cd9ee62586aed08f120b32d209a4817fb5bfdb74c6b83bd55
|
|
7
|
+
data.tar.gz: bd05febbd13a095a4e00f0336514c540c0781eac2707e29e8d9c64e6b4bc0152a7b717e05b93bfdca969e01b1fed4eeeeb52a330b3e9c7a820986b0b30e5b9e0
|
data/README.md
CHANGED
|
@@ -19,7 +19,8 @@ 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
|
-
-
|
|
22
|
+
- Load serialized objects by ids ([prepare_initial_objects](#prepare-initial-objects))
|
|
23
|
+
- Built-in object presenter ([presenter][presenter])
|
|
23
24
|
- Adding custom metadata (via [metadata][metadata] or
|
|
24
25
|
[context_metadata][context_metadata] plugins)
|
|
25
26
|
- Value formatters ([formatters][formatters] plugin) helps to transform
|
|
@@ -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
|
+
- [Presenter](#presenter)
|
|
53
|
+
- [Plugins](#plugins)
|
|
54
|
+
- [Plugin :activerecord_preloads](#plugin-activerecord_preloads)
|
|
55
|
+
- [Plugin :root](#plugin-root)
|
|
56
|
+
- [Plugin :metadata](#plugin-metadata)
|
|
57
|
+
- [Plugin :context_metadata](#plugin-context_metadata)
|
|
58
|
+
- [Plugin :formatters](#plugin-formatters)
|
|
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
|
|
@@ -86,8 +128,8 @@ class UserSerializer < Serega
|
|
|
86
128
|
config.base_serializer = Serega
|
|
87
129
|
|
|
88
130
|
# Method :itself is handy to serialize the same object with a nested set of
|
|
89
|
-
# attributes. Note: with
|
|
90
|
-
# the
|
|
131
|
+
# attributes. Note: with a custom presenter the serialized value will be
|
|
132
|
+
# the presenter instance itself; use `method: :__getobj__` to serialize the
|
|
91
133
|
# original unwrapped object instead.
|
|
92
134
|
attribute :statistics, method: :itself do
|
|
93
135
|
attribute :likes_count
|
|
@@ -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
|
|
@@ -736,6 +849,62 @@ attribute :city,
|
|
|
736
849
|
}
|
|
737
850
|
```
|
|
738
851
|
|
|
852
|
+
## Presenter
|
|
853
|
+
|
|
854
|
+
Moves computed attribute logic out of `:value` blocks and into a
|
|
855
|
+
`presenter do ... end` block, keeping serializers readable as schemas rather
|
|
856
|
+
than bags of lambdas.
|
|
857
|
+
|
|
858
|
+
Computed attributes can be written as inline blocks:
|
|
859
|
+
|
|
860
|
+
```ruby
|
|
861
|
+
class UserSerializer < Serega
|
|
862
|
+
attribute :name, value: proc { |user| [user.first_name, user.last_name].compact.join(' ') }
|
|
863
|
+
attribute :role, value: proc { |user, ctx| user.id == ctx[:current_user_id] ? :self : :other }
|
|
864
|
+
end
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
They can also be written as methods in a `presenter do ... end` block:
|
|
868
|
+
|
|
869
|
+
```ruby
|
|
870
|
+
class UserSerializer < Serega
|
|
871
|
+
attribute :name
|
|
872
|
+
attribute :role
|
|
873
|
+
|
|
874
|
+
presenter do
|
|
875
|
+
def name
|
|
876
|
+
[first_name, last_name].compact.join(' ')
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def role
|
|
880
|
+
id == __ctx__[:current_user_id] ? :self : :other
|
|
881
|
+
end
|
|
882
|
+
end
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
user = OpenStruct.new(id: 1, first_name: 'Bruce', last_name: 'Wayne')
|
|
886
|
+
UserSerializer.to_h(user, context: {current_user_id: 1})
|
|
887
|
+
# => {name: "Bruce Wayne", role: :self}
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Multiple `presenter` blocks accumulate. A child serializer runs its parent's
|
|
891
|
+
blocks followed by its own, so it gets the parent's presenter methods while its
|
|
892
|
+
own stay out of the parent. The parent's blocks are copied when the child class
|
|
893
|
+
is created, so a `presenter` block added to a parent afterwards does not reach it.
|
|
894
|
+
|
|
895
|
+
Presenter methods run on a `SimpleDelegator` wrapping the serialized object, so
|
|
896
|
+
every method of that object is available directly. Any method not defined in a
|
|
897
|
+
`presenter` block is delegated to the object on the first call, and a real
|
|
898
|
+
delegator method is defined for it — so all subsequent serializations call it
|
|
899
|
+
directly.
|
|
900
|
+
|
|
901
|
+
The wrapped object is accessible via `__getobj__` when you need an unambiguous
|
|
902
|
+
reference to it. The serialization context is accessible via `__ctx__`.
|
|
903
|
+
|
|
904
|
+
Objects are wrapped only when the serializer has presenter methods. A serializer
|
|
905
|
+
without them costs nothing — its attribute values, batch loaders and value
|
|
906
|
+
callables receive the raw objects.
|
|
907
|
+
|
|
739
908
|
## Plugins
|
|
740
909
|
|
|
741
910
|
### Plugin :activerecord_preloads
|
|
@@ -936,61 +1105,6 @@ class UserSerializer < Serega
|
|
|
936
1105
|
end
|
|
937
1106
|
```
|
|
938
1107
|
|
|
939
|
-
### Plugin :presenter
|
|
940
|
-
|
|
941
|
-
Moves computed attribute logic out of blocks and into a dedicated `Presenter` class,
|
|
942
|
-
keeping serializers readable as schemas rather than bags of lambdas.
|
|
943
|
-
|
|
944
|
-
Without the plugin, computed attributes live as inline blocks:
|
|
945
|
-
|
|
946
|
-
```ruby
|
|
947
|
-
class UserSerializer < Serega
|
|
948
|
-
attribute :name, value: proc { |u| [u.first_name, u.last_name].compact.join(' ') }
|
|
949
|
-
attribute :role, value: proc { |u, ctx| u == ctx[:current_user] ? :self : :other }
|
|
950
|
-
end
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
With the plugin, they move into a `presenter do ... end` block:
|
|
954
|
-
|
|
955
|
-
```ruby
|
|
956
|
-
class UserSerializer < Serega
|
|
957
|
-
plugin :presenter
|
|
958
|
-
|
|
959
|
-
attribute :name
|
|
960
|
-
attribute :role
|
|
961
|
-
|
|
962
|
-
presenter do
|
|
963
|
-
def name
|
|
964
|
-
[first_name, last_name].compact.join(' ')
|
|
965
|
-
end
|
|
966
|
-
|
|
967
|
-
def role
|
|
968
|
-
id == __ctx__[:current_user_id] ? :self : :other
|
|
969
|
-
end
|
|
970
|
-
end
|
|
971
|
-
end
|
|
972
|
-
```
|
|
973
|
-
|
|
974
|
-
The block is evaluated inside the serializer's own `Presenter` class, so
|
|
975
|
-
multiple `presenter` blocks accumulate.
|
|
976
|
-
|
|
977
|
-
`Presenter` inherits from `SimpleDelegator`, so every method of the serialized
|
|
978
|
-
object is available directly inside presenter methods. Any method not explicitly
|
|
979
|
-
defined on `Presenter` is resolved via `method_missing` on the first call, which
|
|
980
|
-
also defines a real delegator method — so all subsequent serializations call it
|
|
981
|
-
directly, without going through `method_missing` again.
|
|
982
|
-
|
|
983
|
-
The original wrapped object is accessible via `__getobj__` (standard
|
|
984
|
-
`SimpleDelegator` API) when you need an unambiguous reference to it.
|
|
985
|
-
|
|
986
|
-
The serialization context is accessible via the private method `__ctx__`.
|
|
987
|
-
|
|
988
|
-
Objects are wrapped in the `Presenter` only when the serializer's `Presenter`
|
|
989
|
-
class (or an inherited one) actually defines custom methods. Loading the
|
|
990
|
-
plugin in a base serializer adds no overhead to serializers that don't
|
|
991
|
-
customize their presenters — their attribute values, batch loaders and value
|
|
992
|
-
callables keep receiving the raw objects.
|
|
993
|
-
|
|
994
1108
|
### Plugin :string_modifiers
|
|
995
1109
|
|
|
996
1110
|
Allows `:only`, `:except` and `:with` to be given as a single comma-separated
|
|
@@ -1185,7 +1299,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
1185
1299
|
[hash_access]: #serializing-hash-records
|
|
1186
1300
|
[metadata]: #plugin-metadata
|
|
1187
1301
|
[preloads]: #preloads
|
|
1188
|
-
[presenter]: #
|
|
1302
|
+
[presenter]: #presenter
|
|
1189
1303
|
[root]: #plugin-root
|
|
1190
1304
|
[string_modifiers]: #plugin-string_modifiers
|
|
1191
1305
|
[if]: #plugin-if
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.42.0
|
data/lib/serega/config.rb
CHANGED
|
@@ -33,7 +33,7 @@ class Serega
|
|
|
33
33
|
delegate_default_allow_nil: false,
|
|
34
34
|
max_cached_plans_per_serializer_count: 0,
|
|
35
35
|
auto_preload: {has_delegate_option: false, has_serializer_option: false},
|
|
36
|
-
auto_preload_excluded_methods: %i[itself].freeze,
|
|
36
|
+
auto_preload_excluded_methods: %i[itself __getobj__].freeze,
|
|
37
37
|
hide_by_default: false,
|
|
38
38
|
batch_id_option: :id,
|
|
39
39
|
base_serializer: nil,
|
|
@@ -16,7 +16,7 @@ class Serega
|
|
|
16
16
|
#
|
|
17
17
|
# @private
|
|
18
18
|
module InstanceMethods
|
|
19
|
-
attr_reader :context, :plan, :many, :opts, :level_queue
|
|
19
|
+
attr_reader :context, :plan, :many, :opts, :level_queue, :presenter
|
|
20
20
|
|
|
21
21
|
# @param plan [SeregaPlan] Serialization plan
|
|
22
22
|
# @param context [Hash] Serialization context
|
|
@@ -30,6 +30,8 @@ class Serega
|
|
|
30
30
|
@many = many
|
|
31
31
|
@opts = opts
|
|
32
32
|
@level_queue = opts[:level_queue]
|
|
33
|
+
# Looked up once here and reused for every enqueued chunk of the level.
|
|
34
|
+
@presenter = self.class.serializer_class.presenter
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
# Enqueues this level and returns its result container(s). The containers are
|
|
@@ -78,9 +80,14 @@ class Serega
|
|
|
78
80
|
# containers. This is where objects enter their level, so every object a
|
|
79
81
|
# point resolves against and a batch loader receives has the same shape.
|
|
80
82
|
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
+
# Each object is wrapped in the serializer's presenter before it is enqueued,
|
|
84
|
+
# so the whole level — value resolution and batch loaders alike — sees
|
|
85
|
+
# presenters. Serializers without a presenter enqueue the objects as they
|
|
86
|
+
# are — wrapping would only add overhead and break class checks
|
|
87
|
+
# (object.is_a?, Hash === object) without changing anything.
|
|
83
88
|
def enqueue(objects)
|
|
89
|
+
objects = objects.map { |object| presenter.new(object, context) } if presenter
|
|
90
|
+
|
|
84
91
|
level_queue.enqueue(self, objects)
|
|
85
92
|
end
|
|
86
93
|
|
data/lib/serega/plan_point.rb
CHANGED
|
@@ -85,12 +85,18 @@ class Serega
|
|
|
85
85
|
# Runs this point's declared preloads over the given objects using the
|
|
86
86
|
# serializer's registered preload handler.
|
|
87
87
|
#
|
|
88
|
+
# Presenters are unwrapped first, as preload handlers work with the
|
|
89
|
+
# serialized objects themselves.
|
|
90
|
+
#
|
|
88
91
|
# @param objects [Array] objects serialized at this point's level
|
|
89
92
|
# @return [void]
|
|
90
93
|
def run_preloads(objects)
|
|
91
94
|
return unless preloads
|
|
92
95
|
|
|
93
|
-
|
|
96
|
+
serializer_class = self.class.serializer_class
|
|
97
|
+
objects = objects.map(&:__getobj__) if serializer_class.presenter
|
|
98
|
+
|
|
99
|
+
handler = serializer_class.preload_with
|
|
94
100
|
unless handler
|
|
95
101
|
raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
|
|
96
102
|
end
|
|
@@ -88,28 +88,9 @@ class Serega
|
|
|
88
88
|
# @private
|
|
89
89
|
def self.after_load_plugin(serializer_class, **_opts)
|
|
90
90
|
serializer_class.preload_with do |objects, preloads|
|
|
91
|
-
Preloader.preload(
|
|
91
|
+
Preloader.preload(objects, preloads)
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
|
-
|
|
95
|
-
#
|
|
96
|
-
# The underlying records to preload onto. The :presenter plugin wraps every
|
|
97
|
-
# serialized object in a SimpleDelegator, but ActiveRecord's Preloader needs
|
|
98
|
-
# the real records, so unwrap them via #__getobj__ when presenter is used.
|
|
99
|
-
# Objects are wrapped only when the Presenter class has custom methods.
|
|
100
|
-
#
|
|
101
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
102
|
-
# @param objects [Array] objects serialized at the current level
|
|
103
|
-
#
|
|
104
|
-
# @return [Array] the underlying records
|
|
105
|
-
#
|
|
106
|
-
# @private
|
|
107
|
-
def self.records(serializer_class, objects)
|
|
108
|
-
return objects unless serializer_class.plugin_used?(:presenter)
|
|
109
|
-
return objects unless serializer_class.custom_presenter?
|
|
110
|
-
|
|
111
|
-
objects.map(&:__getobj__)
|
|
112
|
-
end
|
|
113
94
|
end
|
|
114
95
|
|
|
115
96
|
register_plugin(ActiverecordPreloads.plugin_name, ActiverecordPreloads)
|
|
@@ -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/plugins.rb
CHANGED
|
@@ -30,10 +30,10 @@ class Serega
|
|
|
30
30
|
# @raise [SeregaError] Raises SeregaError when plugin was not found
|
|
31
31
|
#
|
|
32
32
|
# @example Find plugin when providing name
|
|
33
|
-
# SeregaPlugins.find_plugin(:
|
|
33
|
+
# SeregaPlugins.find_plugin(:root) # => SeregaPlugins::Root
|
|
34
34
|
#
|
|
35
35
|
# @example Find plugin when providing plugin itself
|
|
36
|
-
# SeregaPlugins.find_plugin(
|
|
36
|
+
# SeregaPlugins.find_plugin(Root) # => Root
|
|
37
37
|
#
|
|
38
38
|
# @return [Class<Module>] Plugin core module
|
|
39
39
|
#
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "delegate"
|
|
4
|
+
require "forwardable"
|
|
5
|
+
|
|
6
|
+
class Serega
|
|
7
|
+
#
|
|
8
|
+
# Wraps the serialized object, so computed attribute values can be defined as
|
|
9
|
+
# methods instead of `:value` callables.
|
|
10
|
+
#
|
|
11
|
+
# SeregaPresenter inherits from SimpleDelegator:
|
|
12
|
+
# - All methods of the serialized object are available directly inside presenter methods.
|
|
13
|
+
# - Methods not defined on SeregaPresenter are resolved via method_missing on the first call
|
|
14
|
+
# and then defined as real delegators, so subsequent calls skip method_missing entirely.
|
|
15
|
+
# - The original object is accessible via __getobj__ (standard SimpleDelegator API).
|
|
16
|
+
# - The serialization context is accessible via the private method __ctx__.
|
|
17
|
+
#
|
|
18
|
+
# The `presenter do ... end` block is evaluated inside the serializer's own
|
|
19
|
+
# SeregaPresenter class, so multiple blocks accumulate.
|
|
20
|
+
#
|
|
21
|
+
# @example
|
|
22
|
+
# class UserSerializer < Serega
|
|
23
|
+
# attribute :name
|
|
24
|
+
# attribute :role
|
|
25
|
+
#
|
|
26
|
+
# presenter do
|
|
27
|
+
# def name
|
|
28
|
+
# [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# def role
|
|
32
|
+
# id == __ctx__[:current_user_id] ? :self : :other
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# @private
|
|
38
|
+
class SeregaPresenter < SimpleDelegator
|
|
39
|
+
#
|
|
40
|
+
# @param object [Object] Serialized object to wrap
|
|
41
|
+
# @param ctx [Hash, nil] Serialization context
|
|
42
|
+
#
|
|
43
|
+
def initialize(object, ctx = nil)
|
|
44
|
+
super(object)
|
|
45
|
+
@__ctx__ = ctx
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
attr_reader :__ctx__
|
|
51
|
+
|
|
52
|
+
#
|
|
53
|
+
# Delegates all missing methods to serialized object.
|
|
54
|
+
#
|
|
55
|
+
# Creates delegator method after first #method_missing hit to improve
|
|
56
|
+
# performance of following serializations.
|
|
57
|
+
#
|
|
58
|
+
def method_missing(name, *_args, &_block) # rubocop:disable Style/MissingRespondToMissing -- base SimpleDelegator class has this method
|
|
59
|
+
super.tap do
|
|
60
|
+
self.class.def_delegator :__getobj__, name
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
extend Forwardable
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/serega.rb
CHANGED
|
@@ -62,6 +62,7 @@ require_relative "serega/validations/check_batch_loader_params"
|
|
|
62
62
|
require_relative "serega/validations/check_serialize_params"
|
|
63
63
|
|
|
64
64
|
require_relative "serega/config"
|
|
65
|
+
require_relative "serega/presenter"
|
|
65
66
|
require_relative "serega/object_serializer"
|
|
66
67
|
require_relative "serega/plan_point"
|
|
67
68
|
require_relative "serega/plan"
|
|
@@ -70,6 +71,7 @@ require_relative "serega/plugins"
|
|
|
70
71
|
|
|
71
72
|
class Serega
|
|
72
73
|
@config = SeregaConfig.new
|
|
74
|
+
@presenter_class = nil
|
|
73
75
|
|
|
74
76
|
# Validates `Serializer.attribute` params
|
|
75
77
|
check_attribute_params_class = Class.new(SeregaValidations::CheckAttributeParams)
|
|
@@ -170,10 +172,17 @@ class Serega
|
|
|
170
172
|
end
|
|
171
173
|
|
|
172
174
|
#
|
|
173
|
-
#
|
|
175
|
+
# Lists blocks given to `presenter`, in definition order
|
|
174
176
|
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
+
# @return [Array<Proc>] presenter blocks list
|
|
178
|
+
#
|
|
179
|
+
# @private
|
|
180
|
+
def presenter_blocks
|
|
181
|
+
@presenter_blocks ||= []
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
#
|
|
185
|
+
# Adds attribute
|
|
177
186
|
#
|
|
178
187
|
# @param name [Symbol] Attribute name. Attribute value will be found by executing `object.<name>`
|
|
179
188
|
# @param opts [Hash] Options to serialize attribute
|
|
@@ -233,6 +242,30 @@ class Serega
|
|
|
233
242
|
batch_loaders[batch_loader.name] = batch_loader
|
|
234
243
|
end
|
|
235
244
|
|
|
245
|
+
#
|
|
246
|
+
# Defines presenter methods — evaluates the block inside the serializer's own
|
|
247
|
+
# presenter class. Multiple blocks accumulate.
|
|
248
|
+
#
|
|
249
|
+
# presenter do
|
|
250
|
+
# def name
|
|
251
|
+
# [first_name, last_name].compact.join(" ")
|
|
252
|
+
# end
|
|
253
|
+
# end
|
|
254
|
+
#
|
|
255
|
+
# @param block [Proc] Presenter methods
|
|
256
|
+
#
|
|
257
|
+
# @return [Class<SeregaPresenter>, nil] the serializer presenter class,
|
|
258
|
+
# nil when the serializer has none
|
|
259
|
+
#
|
|
260
|
+
def presenter(&block)
|
|
261
|
+
return @presenter_class unless block
|
|
262
|
+
|
|
263
|
+
@presenter_class ||= Class.new(SeregaPresenter)
|
|
264
|
+
@presenter_class.class_exec(&block)
|
|
265
|
+
presenter_blocks << block
|
|
266
|
+
@presenter_class
|
|
267
|
+
end
|
|
268
|
+
|
|
236
269
|
#
|
|
237
270
|
# Registers (or returns) the handler used to preload an attribute's
|
|
238
271
|
# associations onto the records gathered during serialization.
|
|
@@ -265,6 +298,63 @@ class Serega
|
|
|
265
298
|
@preload_with = handler
|
|
266
299
|
end
|
|
267
300
|
|
|
301
|
+
#
|
|
302
|
+
# Registers (or returns) the handler that replaces the serialized objects
|
|
303
|
+
# before serialization starts.
|
|
304
|
+
#
|
|
305
|
+
# The handler is called once per serialization with the objects provided to
|
|
306
|
+
# `.call`/`.to_h`/`.to_data` and the serialization context. Its result is
|
|
307
|
+
# serialized instead of the provided objects, which allows to accept ids or
|
|
308
|
+
# other references and load the records in one place.
|
|
309
|
+
#
|
|
310
|
+
# The handler runs before the `:many` option is detected, so it may turn a
|
|
311
|
+
# single object into a collection and back. A provided `:many` serialization
|
|
312
|
+
# option is still used as is.
|
|
313
|
+
#
|
|
314
|
+
# The handler runs only for the serialized objects, and not for objects of
|
|
315
|
+
# nested serializers.
|
|
316
|
+
#
|
|
317
|
+
# @example with a block
|
|
318
|
+
# prepare_initial_objects { |user_ids| User.where(id: user_ids) }
|
|
319
|
+
#
|
|
320
|
+
# @example with a context
|
|
321
|
+
# prepare_initial_objects { |user_ids, ctx| User.where(id: user_ids, account: ctx[:account]) }
|
|
322
|
+
#
|
|
323
|
+
# @example with a keyword context
|
|
324
|
+
# prepare_initial_objects { |user_ids, ctx:| User.where(id: user_ids, account: ctx[:account]) }
|
|
325
|
+
#
|
|
326
|
+
# @example with a callable value
|
|
327
|
+
# prepare_initial_objects UsersLoader
|
|
328
|
+
#
|
|
329
|
+
# @param value [#call, nil] Handler accepting (objects), (objects, context) or (objects, ctx:)
|
|
330
|
+
# @param block [Proc] Handler accepting (objects), (objects, context) or (objects, ctx:)
|
|
331
|
+
#
|
|
332
|
+
# @return [#call, nil] The registered handler
|
|
333
|
+
#
|
|
334
|
+
def prepare_initial_objects(value = nil, &block)
|
|
335
|
+
return @prepare_initial_objects if value.nil? && block.nil?
|
|
336
|
+
raise SeregaError, "prepare_initial_objects accepts a single callable or a block, not both" if value && block
|
|
337
|
+
|
|
338
|
+
handler = value || block
|
|
339
|
+
raise SeregaError, "prepare_initial_objects value must be a Proc or respond to #call" if !handler.is_a?(Proc) && !handler.respond_to?(:call)
|
|
340
|
+
|
|
341
|
+
signature = SeregaUtils::MethodSignature.call(handler, pos_limit: 2, keyword_args: [:ctx])
|
|
342
|
+
raise SeregaError, prepare_initial_objects_signature_error unless %w[1 2 1_ctx].include?(signature)
|
|
343
|
+
|
|
344
|
+
@prepare_initial_objects_signature = signature
|
|
345
|
+
@prepare_initial_objects = handler
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
#
|
|
349
|
+
# Signature of the registered prepare_initial_objects handler
|
|
350
|
+
#
|
|
351
|
+
# @return [String, nil] Handler signature
|
|
352
|
+
#
|
|
353
|
+
# @private
|
|
354
|
+
def prepare_initial_objects_signature
|
|
355
|
+
@prepare_initial_objects_signature
|
|
356
|
+
end
|
|
357
|
+
|
|
268
358
|
#
|
|
269
359
|
# Serializes provided object to Hash
|
|
270
360
|
#
|
|
@@ -321,7 +411,6 @@ class Serega
|
|
|
321
411
|
|
|
322
412
|
# Patched in:
|
|
323
413
|
# - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
|
|
324
|
-
# - plugin :presenter (defines Presenter)
|
|
325
414
|
def inherited(subclass)
|
|
326
415
|
config_class = Class.new(self::SeregaConfig)
|
|
327
416
|
config_class.serializer_class = subclass
|
|
@@ -386,8 +475,25 @@ class Serega
|
|
|
386
475
|
# Assign same preload handler
|
|
387
476
|
subclass.preload_with(preload_with) if preload_with
|
|
388
477
|
|
|
478
|
+
# Assign same initial objects handler
|
|
479
|
+
subclass.prepare_initial_objects(prepare_initial_objects) if prepare_initial_objects
|
|
480
|
+
|
|
481
|
+
# Assign same presenter blocks
|
|
482
|
+
presenter_blocks.each do |presenter_block|
|
|
483
|
+
subclass.presenter(&presenter_block)
|
|
484
|
+
end
|
|
485
|
+
|
|
389
486
|
super
|
|
390
487
|
end
|
|
488
|
+
|
|
489
|
+
def prepare_initial_objects_signature_error
|
|
490
|
+
<<~ERR.strip
|
|
491
|
+
prepare_initial_objects handler arguments should have one of this signatures:
|
|
492
|
+
- (objects) # one argument
|
|
493
|
+
- (objects, ctx) # two arguments
|
|
494
|
+
- (objects, ctx:) # one argument and one :ctx keyword argument
|
|
495
|
+
ERR
|
|
496
|
+
end
|
|
391
497
|
end
|
|
392
498
|
|
|
393
499
|
#
|
|
@@ -436,6 +542,8 @@ class Serega
|
|
|
436
542
|
# @return [Hash] Serialization result
|
|
437
543
|
#
|
|
438
544
|
def call(object, opts = nil)
|
|
545
|
+
opts = normalize_serialization_opts(opts)
|
|
546
|
+
object = prepare_objects(object, opts[:context])
|
|
439
547
|
opts = prepare_initial_serialization_opts(object, opts)
|
|
440
548
|
serialize(object, opts)
|
|
441
549
|
end
|
|
@@ -458,6 +566,8 @@ class Serega
|
|
|
458
566
|
# @return [Data] Serialization result
|
|
459
567
|
#
|
|
460
568
|
def to_data(object, opts = nil)
|
|
569
|
+
opts = normalize_serialization_opts(opts)
|
|
570
|
+
object = prepare_objects(object, opts[:context])
|
|
461
571
|
opts = prepare_initial_serialization_opts(object, opts)
|
|
462
572
|
serialized_data = serialize(object, opts)
|
|
463
573
|
self.class::SeregaDataBuilder.call(self, serialized_data)
|
|
@@ -488,11 +598,26 @@ class Serega
|
|
|
488
598
|
SeregaUtils::ToHash.call(value)
|
|
489
599
|
end
|
|
490
600
|
|
|
491
|
-
def
|
|
601
|
+
def normalize_serialization_opts(opts)
|
|
492
602
|
opts = opts ? opts.transform_keys(&:to_sym) : {}
|
|
493
603
|
self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
|
|
494
604
|
|
|
495
605
|
opts[:context] ||= {}
|
|
606
|
+
opts
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def prepare_objects(objects, context)
|
|
610
|
+
handler = self.class.prepare_initial_objects
|
|
611
|
+
return objects unless handler
|
|
612
|
+
|
|
613
|
+
case self.class.prepare_initial_objects_signature
|
|
614
|
+
when "1" then handler.call(objects)
|
|
615
|
+
when "2" then handler.call(objects, context)
|
|
616
|
+
else handler.call(objects, ctx: context) # "1_ctx"
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def prepare_initial_serialization_opts(object, opts)
|
|
496
621
|
opts[:level_queue] = SeregaEngine::LevelQueue.new
|
|
497
622
|
opts[:many] = SeregaUtils::CollectionDetector.call(object) unless opts.key?(:many)
|
|
498
623
|
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.
|
|
4
|
+
version: 0.42.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrey
|
|
@@ -64,10 +64,10 @@ files:
|
|
|
64
64
|
- lib/serega/plugins/metadata/validations/check_opt_value.rb
|
|
65
65
|
- lib/serega/plugins/metadata/validations/check_opts.rb
|
|
66
66
|
- lib/serega/plugins/metadata/validations/check_path.rb
|
|
67
|
-
- lib/serega/plugins/presenter/presenter.rb
|
|
68
67
|
- lib/serega/plugins/root/root.rb
|
|
69
68
|
- lib/serega/plugins/string_modifiers/parse_string_modifiers.rb
|
|
70
69
|
- lib/serega/plugins/string_modifiers/string_modifiers.rb
|
|
70
|
+
- lib/serega/presenter.rb
|
|
71
71
|
- lib/serega/utils/collection_detector.rb
|
|
72
72
|
- lib/serega/utils/enum_deep_dup.rb
|
|
73
73
|
- lib/serega/utils/enum_deep_freeze.rb
|
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "delegate"
|
|
4
|
-
require "forwardable"
|
|
5
|
-
|
|
6
|
-
class Serega
|
|
7
|
-
module SeregaPlugins
|
|
8
|
-
#
|
|
9
|
-
# Plugin :presenter — moves computed attribute logic into a dedicated Presenter class.
|
|
10
|
-
#
|
|
11
|
-
# Presenter inherits from SimpleDelegator:
|
|
12
|
-
# - All methods of the serialized object are available directly inside presenter methods.
|
|
13
|
-
# - Methods not defined on Presenter are resolved via method_missing on the first call
|
|
14
|
-
# and then defined as real delegators, so subsequent calls skip method_missing entirely.
|
|
15
|
-
# - The original object is accessible via __getobj__ (standard SimpleDelegator API).
|
|
16
|
-
# - The serialization context is accessible via the private method __ctx__.
|
|
17
|
-
#
|
|
18
|
-
# The `presenter do ... end` block is evaluated inside the serializer's own
|
|
19
|
-
# Presenter class, so multiple blocks accumulate.
|
|
20
|
-
#
|
|
21
|
-
# @example
|
|
22
|
-
# class UserSerializer < Serega
|
|
23
|
-
# plugin :presenter
|
|
24
|
-
#
|
|
25
|
-
# attribute :name
|
|
26
|
-
# attribute :role
|
|
27
|
-
#
|
|
28
|
-
# presenter do
|
|
29
|
-
# def name
|
|
30
|
-
# [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
|
|
31
|
-
# end
|
|
32
|
-
#
|
|
33
|
-
# def role
|
|
34
|
-
# id == __ctx__[:current_user_id] ? :self : :other
|
|
35
|
-
# end
|
|
36
|
-
# end
|
|
37
|
-
# end
|
|
38
|
-
#
|
|
39
|
-
module Presenter
|
|
40
|
-
# @return [Symbol] Plugin name
|
|
41
|
-
# @private
|
|
42
|
-
def self.plugin_name
|
|
43
|
-
:presenter
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
#
|
|
47
|
-
# Applies plugin code to specific serializer
|
|
48
|
-
#
|
|
49
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
50
|
-
# @param _opts [Hash] Plugin options
|
|
51
|
-
#
|
|
52
|
-
# @return [void]
|
|
53
|
-
#
|
|
54
|
-
# @private
|
|
55
|
-
def self.load_plugin(serializer_class, **_opts)
|
|
56
|
-
serializer_class.extend(ClassMethods)
|
|
57
|
-
serializer_class::SeregaObjectSerializer.include(SeregaObjectSerializerInstanceMethods)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
#
|
|
61
|
-
# Runs callbacks after plugin was attached
|
|
62
|
-
#
|
|
63
|
-
# @param serializer_class [Class<Serega>] Current serializer class
|
|
64
|
-
# @param _opts [Hash] Plugin options
|
|
65
|
-
#
|
|
66
|
-
# @return [void]
|
|
67
|
-
#
|
|
68
|
-
# @private
|
|
69
|
-
def self.after_load_plugin(serializer_class, **_opts)
|
|
70
|
-
presenter_class = Class.new(Presenter)
|
|
71
|
-
presenter_class.serializer_class = serializer_class
|
|
72
|
-
serializer_class.const_set(:Presenter, presenter_class)
|
|
73
|
-
|
|
74
|
-
# The presenter's unwrap method returns the serialized object itself,
|
|
75
|
-
# not an association — it must never be auto-preloaded.
|
|
76
|
-
config = serializer_class.config
|
|
77
|
-
config.auto_preload_excluded_methods = config.auto_preload_excluded_methods | [:__getobj__]
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Presenter class
|
|
81
|
-
# @private
|
|
82
|
-
class Presenter < SimpleDelegator
|
|
83
|
-
# Presenter instance methods
|
|
84
|
-
# @private
|
|
85
|
-
module InstanceMethods
|
|
86
|
-
#
|
|
87
|
-
# @param object [Object] Serialized object to wrap
|
|
88
|
-
# @param ctx [Hash, nil] Serialization context
|
|
89
|
-
#
|
|
90
|
-
def initialize(object, ctx = nil)
|
|
91
|
-
super(object)
|
|
92
|
-
@__ctx__ = ctx
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
private
|
|
96
|
-
|
|
97
|
-
attr_reader :__ctx__
|
|
98
|
-
|
|
99
|
-
#
|
|
100
|
-
# Delegates all missing methods to serialized object.
|
|
101
|
-
#
|
|
102
|
-
# Creates delegator method after first #method_missing hit to improve
|
|
103
|
-
# performance of following serializations.
|
|
104
|
-
#
|
|
105
|
-
def method_missing(name, *_args, &_block) # rubocop:disable Style/MissingRespondToMissing -- base SimpleDelegator class has this method
|
|
106
|
-
super.tap do
|
|
107
|
-
self.class.def_delegator :__getobj__, name
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
extend SeregaHelpers::SerializerClassHelper
|
|
113
|
-
extend Forwardable
|
|
114
|
-
include InstanceMethods
|
|
115
|
-
|
|
116
|
-
# Tracks whether user code was added to the Presenter class.
|
|
117
|
-
#
|
|
118
|
-
# These singleton methods are defined after the base class body above,
|
|
119
|
-
# so the plugin's own includes do not mark the base class as modified.
|
|
120
|
-
# Lazy delegators defined by #method_missing do mark the class, but
|
|
121
|
-
# they can appear only on presenters that are already wrapping.
|
|
122
|
-
class << self
|
|
123
|
-
#
|
|
124
|
-
# Checks if this Presenter class (or an inherited one) was extended
|
|
125
|
-
# with custom user code and therefore objects must be wrapped
|
|
126
|
-
#
|
|
127
|
-
# @return [Boolean] whether custom presenter methods were defined
|
|
128
|
-
#
|
|
129
|
-
def modified?
|
|
130
|
-
return true if defined?(@modified)
|
|
131
|
-
return false if equal?(Presenter) # the plugin's base class — the walk stops here
|
|
132
|
-
|
|
133
|
-
superclass.modified?
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Marks the class as modified, then includes the module
|
|
137
|
-
# @return [void]
|
|
138
|
-
def include(*modules)
|
|
139
|
-
@modified = true
|
|
140
|
-
super
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Marks the class as modified, then prepends the module
|
|
144
|
-
# @return [void]
|
|
145
|
-
def prepend(*modules)
|
|
146
|
-
@modified = true
|
|
147
|
-
super
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
private
|
|
151
|
-
|
|
152
|
-
def method_added(name)
|
|
153
|
-
@modified = true
|
|
154
|
-
super
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
#
|
|
160
|
-
# Serega additional/patched class methods
|
|
161
|
-
#
|
|
162
|
-
# @see Serega
|
|
163
|
-
#
|
|
164
|
-
module ClassMethods
|
|
165
|
-
#
|
|
166
|
-
# Defines presenter methods — evaluates the block inside the
|
|
167
|
-
# serializer's own Presenter class. Multiple blocks accumulate.
|
|
168
|
-
#
|
|
169
|
-
# presenter do
|
|
170
|
-
# def name
|
|
171
|
-
# [first_name, last_name].compact.join(" ")
|
|
172
|
-
# end
|
|
173
|
-
# end
|
|
174
|
-
#
|
|
175
|
-
# @return [void]
|
|
176
|
-
#
|
|
177
|
-
def presenter(&block)
|
|
178
|
-
raise SeregaError, "Provide a block with presenter methods: `presenter do ... end`" unless block
|
|
179
|
-
|
|
180
|
-
self::Presenter.class_exec(&block)
|
|
181
|
-
nil
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
#
|
|
185
|
-
# Checks if the serializer's Presenter class (or an inherited one) was
|
|
186
|
-
# extended with custom user code. When it was not, serialized objects
|
|
187
|
-
# are not wrapped in the Presenter at all.
|
|
188
|
-
#
|
|
189
|
-
# @return [Boolean] whether custom presenter methods were defined
|
|
190
|
-
#
|
|
191
|
-
# @private
|
|
192
|
-
def custom_presenter?
|
|
193
|
-
self::Presenter.modified?
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
private
|
|
197
|
-
|
|
198
|
-
def inherited(subclass)
|
|
199
|
-
super
|
|
200
|
-
|
|
201
|
-
presenter_class = Class.new(self::Presenter)
|
|
202
|
-
presenter_class.serializer_class = subclass
|
|
203
|
-
subclass.const_set(:Presenter, presenter_class)
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
#
|
|
208
|
-
# SeregaObjectSerializer additional/patched class methods
|
|
209
|
-
#
|
|
210
|
-
# @see Serega::SeregaObjectSerializer
|
|
211
|
-
#
|
|
212
|
-
# @private
|
|
213
|
-
module SeregaObjectSerializerInstanceMethods
|
|
214
|
-
# The custom-presenter check is made once per object serializer here
|
|
215
|
-
# and its result is reused for every enqueued chunk of the level.
|
|
216
|
-
def initialize(**opts)
|
|
217
|
-
super
|
|
218
|
-
@wrap_in_presenter = self.class.serializer_class.custom_presenter?
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
private
|
|
222
|
-
|
|
223
|
-
#
|
|
224
|
-
# Wraps each serialized object in Presenter.new(object, ctx) before it is
|
|
225
|
-
# enqueued, so the whole level — value resolution and batch loaders alike —
|
|
226
|
-
# sees presenters. Objects are not wrapped when the Presenter class has
|
|
227
|
-
# no custom methods — such wrapping would only add overhead and break
|
|
228
|
-
# class checks (object.is_a?, Hash === object) without changing anything.
|
|
229
|
-
#
|
|
230
|
-
def enqueue(objects)
|
|
231
|
-
return super unless @wrap_in_presenter
|
|
232
|
-
|
|
233
|
-
presenter = self.class.serializer_class::Presenter
|
|
234
|
-
presenters = objects.map { |object| presenter.new(object, context) }
|
|
235
|
-
super(presenters)
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
register_plugin(Presenter.plugin_name, Presenter)
|
|
241
|
-
end
|
|
242
|
-
end
|