serega 0.41.0 → 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 +61 -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.rb +2 -2
- data/lib/serega/presenter.rb +66 -0
- data/lib/serega.rb +41 -4
- 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
|
@@ -20,7 +20,7 @@ It has some great features:
|
|
|
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
|
+
- Built-in object presenter ([presenter][presenter])
|
|
24
24
|
- Adding custom metadata (via [metadata][metadata] or
|
|
25
25
|
[context_metadata][context_metadata] plugins)
|
|
26
26
|
- Value formatters ([formatters][formatters] plugin) helps to transform
|
|
@@ -49,13 +49,13 @@ It has some great features:
|
|
|
49
49
|
- [Serializing the same object in association](#serializing-the-same-object-in-association)
|
|
50
50
|
- [Custom preloading](#custom-preloading)
|
|
51
51
|
- [Serializing Hash records](#serializing-hash-records)
|
|
52
|
+
- [Presenter](#presenter)
|
|
52
53
|
- [Plugins](#plugins)
|
|
53
54
|
- [Plugin :activerecord_preloads](#plugin-activerecord_preloads)
|
|
54
55
|
- [Plugin :root](#plugin-root)
|
|
55
56
|
- [Plugin :metadata](#plugin-metadata)
|
|
56
57
|
- [Plugin :context_metadata](#plugin-context_metadata)
|
|
57
58
|
- [Plugin :formatters](#plugin-formatters)
|
|
58
|
-
- [Plugin :presenter](#plugin-presenter)
|
|
59
59
|
- [Plugin :string_modifiers](#plugin-string_modifiers)
|
|
60
60
|
- [Plugin :if](#plugin-if)
|
|
61
61
|
- [Plugin :camel_case](#plugin-camel_case)
|
|
@@ -128,8 +128,8 @@ class UserSerializer < Serega
|
|
|
128
128
|
config.base_serializer = Serega
|
|
129
129
|
|
|
130
130
|
# Method :itself is handy to serialize the same object with a nested set of
|
|
131
|
-
# attributes. Note: with
|
|
132
|
-
# the
|
|
131
|
+
# attributes. Note: with a custom presenter the serialized value will be
|
|
132
|
+
# the presenter instance itself; use `method: :__getobj__` to serialize the
|
|
133
133
|
# original unwrapped object instead.
|
|
134
134
|
attribute :statistics, method: :itself do
|
|
135
135
|
attribute :likes_count
|
|
@@ -849,6 +849,62 @@ attribute :city,
|
|
|
849
849
|
}
|
|
850
850
|
```
|
|
851
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
|
+
|
|
852
908
|
## Plugins
|
|
853
909
|
|
|
854
910
|
### Plugin :activerecord_preloads
|
|
@@ -1049,61 +1105,6 @@ class UserSerializer < Serega
|
|
|
1049
1105
|
end
|
|
1050
1106
|
```
|
|
1051
1107
|
|
|
1052
|
-
### Plugin :presenter
|
|
1053
|
-
|
|
1054
|
-
Moves computed attribute logic out of blocks and into a dedicated `Presenter` class,
|
|
1055
|
-
keeping serializers readable as schemas rather than bags of lambdas.
|
|
1056
|
-
|
|
1057
|
-
Without the plugin, computed attributes live as inline blocks:
|
|
1058
|
-
|
|
1059
|
-
```ruby
|
|
1060
|
-
class UserSerializer < Serega
|
|
1061
|
-
attribute :name, value: proc { |u| [u.first_name, u.last_name].compact.join(' ') }
|
|
1062
|
-
attribute :role, value: proc { |u, ctx| u == ctx[:current_user] ? :self : :other }
|
|
1063
|
-
end
|
|
1064
|
-
```
|
|
1065
|
-
|
|
1066
|
-
With the plugin, they move into a `presenter do ... end` block:
|
|
1067
|
-
|
|
1068
|
-
```ruby
|
|
1069
|
-
class UserSerializer < Serega
|
|
1070
|
-
plugin :presenter
|
|
1071
|
-
|
|
1072
|
-
attribute :name
|
|
1073
|
-
attribute :role
|
|
1074
|
-
|
|
1075
|
-
presenter do
|
|
1076
|
-
def name
|
|
1077
|
-
[first_name, last_name].compact.join(' ')
|
|
1078
|
-
end
|
|
1079
|
-
|
|
1080
|
-
def role
|
|
1081
|
-
id == __ctx__[:current_user_id] ? :self : :other
|
|
1082
|
-
end
|
|
1083
|
-
end
|
|
1084
|
-
end
|
|
1085
|
-
```
|
|
1086
|
-
|
|
1087
|
-
The block is evaluated inside the serializer's own `Presenter` class, so
|
|
1088
|
-
multiple `presenter` blocks accumulate.
|
|
1089
|
-
|
|
1090
|
-
`Presenter` inherits from `SimpleDelegator`, so every method of the serialized
|
|
1091
|
-
object is available directly inside presenter methods. Any method not explicitly
|
|
1092
|
-
defined on `Presenter` is resolved via `method_missing` on the first call, which
|
|
1093
|
-
also defines a real delegator method — so all subsequent serializations call it
|
|
1094
|
-
directly, without going through `method_missing` again.
|
|
1095
|
-
|
|
1096
|
-
The original wrapped object is accessible via `__getobj__` (standard
|
|
1097
|
-
`SimpleDelegator` API) when you need an unambiguous reference to it.
|
|
1098
|
-
|
|
1099
|
-
The serialization context is accessible via the private method `__ctx__`.
|
|
1100
|
-
|
|
1101
|
-
Objects are wrapped in the `Presenter` only when the serializer's `Presenter`
|
|
1102
|
-
class (or an inherited one) actually defines custom methods. Loading the
|
|
1103
|
-
plugin in a base serializer adds no overhead to serializers that don't
|
|
1104
|
-
customize their presenters — their attribute values, batch loaders and value
|
|
1105
|
-
callables keep receiving the raw objects.
|
|
1106
|
-
|
|
1107
1108
|
### Plugin :string_modifiers
|
|
1108
1109
|
|
|
1109
1110
|
Allows `:only`, `:except` and `:with` to be given as a single comma-separated
|
|
@@ -1298,7 +1299,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
1298
1299
|
[hash_access]: #serializing-hash-records
|
|
1299
1300
|
[metadata]: #plugin-metadata
|
|
1300
1301
|
[preloads]: #preloads
|
|
1301
|
-
[presenter]: #
|
|
1302
|
+
[presenter]: #presenter
|
|
1302
1303
|
[root]: #plugin-root
|
|
1303
1304
|
[string_modifiers]: #plugin-string_modifiers
|
|
1304
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)
|
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.
|
|
@@ -378,7 +411,6 @@ class Serega
|
|
|
378
411
|
|
|
379
412
|
# Patched in:
|
|
380
413
|
# - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
|
|
381
|
-
# - plugin :presenter (defines Presenter)
|
|
382
414
|
def inherited(subclass)
|
|
383
415
|
config_class = Class.new(self::SeregaConfig)
|
|
384
416
|
config_class.serializer_class = subclass
|
|
@@ -446,6 +478,11 @@ class Serega
|
|
|
446
478
|
# Assign same initial objects handler
|
|
447
479
|
subclass.prepare_initial_objects(prepare_initial_objects) if prepare_initial_objects
|
|
448
480
|
|
|
481
|
+
# Assign same presenter blocks
|
|
482
|
+
presenter_blocks.each do |presenter_block|
|
|
483
|
+
subclass.presenter(&presenter_block)
|
|
484
|
+
end
|
|
485
|
+
|
|
449
486
|
super
|
|
450
487
|
end
|
|
451
488
|
|
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
|