serega 0.34.0 → 0.36.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 +41 -6
- data/VERSION +1 -1
- data/lib/serega/batch/attribute_loaders.rb +1 -1
- data/lib/serega/data_builder.rb +52 -0
- data/lib/serega/plan.rb +33 -3
- data/lib/serega/plugins/if/if.rb +23 -0
- data/lib/serega/plugins/if/validations/check_opt_if.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_if_value.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_unless.rb +7 -4
- data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +7 -4
- data/lib/serega/plugins/presenter/presenter.rb +30 -5
- data/lib/serega/plugins/root/root.rb +80 -0
- data/lib/serega.rb +74 -20
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 461e4c9b6b9dcaf1c93cc490c8a4270bcc8ae576eed523ef14ee3bc1e6be0944
|
|
4
|
+
data.tar.gz: 7ac9f86f0952070deb5c6a5a812ec9384d9cadab9f783fa5ae5e705ff6cd3116
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: addb468352c4adc023c4f04e30dc6533fefbf7faba9755e02cd243e9f4631c30a0b3b4dd65b160a0f5c012e76717e2d67ddf3c0ac712e5cd0b3008ea3cd8df7d
|
|
7
|
+
data.tar.gz: da936fb8dd2fac9bafe4e670da795f3b3075d591b3c88411b306898d9a59f944354dc8b69941e19b7b2012bda6a6111a25f9dd0347d5ad8c76b56f310c0353ec
|
data/README.md
CHANGED
|
@@ -141,7 +141,7 @@ end
|
|
|
141
141
|
|
|
142
142
|
### Serializing
|
|
143
143
|
|
|
144
|
-
We can serialize objects using class method `.call` aliased
|
|
144
|
+
We can serialize objects using class method `.call` aliased as `.to_h` and
|
|
145
145
|
same instance methods `#call` and its alias `#to_h`.
|
|
146
146
|
|
|
147
147
|
```ruby
|
|
@@ -155,6 +155,18 @@ UserSerializer.to_h(user) # => {username: "serega"}
|
|
|
155
155
|
UserSerializer.to_h([user]) # => [{username: "serega"}]
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
Use `.to_data` / `#to_data` to get the same result as Ruby `Data` objects
|
|
159
|
+
(immutable value objects, Ruby 3.2+). Nested serialized relations are also
|
|
160
|
+
converted to `Data` objects.
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
result = UserSerializer.to_data(user)
|
|
164
|
+
result # => #<data username="serega">
|
|
165
|
+
result.username # => "serega"
|
|
166
|
+
|
|
167
|
+
UserSerializer.to_data([user]) # => [#<data username="serega">]
|
|
168
|
+
```
|
|
169
|
+
|
|
158
170
|
If serialized fields are constant, then it's a good idea to initiate the
|
|
159
171
|
serializer and reuse it.
|
|
160
172
|
It will be a bit faster (the serialization plan will be prepared only once).
|
|
@@ -761,27 +773,50 @@ end
|
|
|
761
773
|
|
|
762
774
|
### Plugin :presenter
|
|
763
775
|
|
|
764
|
-
|
|
776
|
+
Moves computed attribute logic out of blocks and into a dedicated `Presenter` class,
|
|
777
|
+
keeping serializers readable as schemas rather than bags of lambdas.
|
|
778
|
+
|
|
779
|
+
Without the plugin, computed attributes live as inline blocks:
|
|
780
|
+
|
|
781
|
+
```ruby
|
|
782
|
+
class UserSerializer < Serega
|
|
783
|
+
attribute :name, value: proc { |u| [u.first_name, u.last_name].compact.join(' ') }
|
|
784
|
+
attribute :role, value: proc { |u, ctx| u == ctx[:current_user] ? :self : :other }
|
|
785
|
+
end
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
With the plugin, they move into a clean class:
|
|
765
789
|
|
|
766
790
|
```ruby
|
|
767
791
|
class UserSerializer < Serega
|
|
768
792
|
plugin :presenter
|
|
769
793
|
|
|
770
794
|
attribute :name
|
|
771
|
-
attribute :
|
|
795
|
+
attribute :role
|
|
772
796
|
|
|
773
797
|
class Presenter
|
|
774
798
|
def name
|
|
775
|
-
[first_name, last_name].
|
|
799
|
+
[first_name, last_name].compact.join(' ')
|
|
776
800
|
end
|
|
777
801
|
|
|
778
|
-
def
|
|
779
|
-
[
|
|
802
|
+
def role
|
|
803
|
+
id == __ctx__[:current_user_id] ? :self : :other
|
|
780
804
|
end
|
|
781
805
|
end
|
|
782
806
|
end
|
|
783
807
|
```
|
|
784
808
|
|
|
809
|
+
`Presenter` inherits from `SimpleDelegator`, so every method of the serialized
|
|
810
|
+
object is available directly inside presenter methods. Any method not explicitly
|
|
811
|
+
defined on `Presenter` is resolved via `method_missing` on the first call, which
|
|
812
|
+
also defines a real delegator method — so all subsequent serializations call it
|
|
813
|
+
directly, without going through `method_missing` again.
|
|
814
|
+
|
|
815
|
+
The original wrapped object is accessible via `__getobj__` (standard
|
|
816
|
+
`SimpleDelegator` API) when you need an unambiguous reference to it.
|
|
817
|
+
|
|
818
|
+
The serialization context is accessible via the private method `__ctx__`.
|
|
819
|
+
|
|
785
820
|
### Plugin :string_modifiers
|
|
786
821
|
|
|
787
822
|
Allows to specify modifiers as strings.
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.36.0
|
|
@@ -18,7 +18,7 @@ class Serega
|
|
|
18
18
|
@point_index = {}.compare_by_identity
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
# Remembers data for batch
|
|
21
|
+
# Remembers data for batch serialization:
|
|
22
22
|
#
|
|
23
23
|
# @param point [SeregaPlanPoint] Serialization plan point
|
|
24
24
|
# @param object [Object] Serialized object
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
#
|
|
5
|
+
# Builds Data objects from serialized hashes.
|
|
6
|
+
# Used by `#to_data` / `.to_data`.
|
|
7
|
+
#
|
|
8
|
+
class SeregaDataBuilder
|
|
9
|
+
# SeregaDataBuilder class methods
|
|
10
|
+
module SeregaDataBuilderClassMethods
|
|
11
|
+
#
|
|
12
|
+
# Converts serialized Hash/Array result into a tree of Ruby Data objects
|
|
13
|
+
# following the serializer's plan structure.
|
|
14
|
+
#
|
|
15
|
+
# @param serializer [Serega] Serializer instance carrying the plan
|
|
16
|
+
# @param serialized [Hash, Array, nil] Serialized output from `#to_h`
|
|
17
|
+
#
|
|
18
|
+
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
19
|
+
#
|
|
20
|
+
def call(serializer, serialized)
|
|
21
|
+
build(serialized, serializer.plan)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build(serialized, plan)
|
|
27
|
+
case serialized
|
|
28
|
+
when Array then serialized.map { |item| hash_to_data(item, plan) }
|
|
29
|
+
when Hash then hash_to_data(serialized, plan)
|
|
30
|
+
else serialized
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def hash_to_data(hash, plan)
|
|
35
|
+
hash_data = hash.to_h do |key, value|
|
|
36
|
+
child_plan = plan.points_hash.fetch(key).child_plan
|
|
37
|
+
value = build(value, child_plan) if child_plan
|
|
38
|
+
[key, value]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
build_data_object(plan, hash_data)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build_data_object(plan, hash_data)
|
|
45
|
+
plan.data_class.new(**hash_data)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
extend SeregaHelpers::SerializerClassHelper
|
|
50
|
+
extend SeregaDataBuilderClassMethods
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/serega/plan.rb
CHANGED
|
@@ -27,6 +27,18 @@ class Serega
|
|
|
27
27
|
cached_plan_for(opts, max_cache_size)
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
#
|
|
31
|
+
# Returns (and caches) the Data class for the given set of field names.
|
|
32
|
+
# Uses the Array as cache key so the same Data class is reused across
|
|
33
|
+
# all plan instances with identical fields.
|
|
34
|
+
#
|
|
35
|
+
# @param point_names [Array<Symbol>] Attribute names for the Data members
|
|
36
|
+
# @return [Class] Subclass of Data
|
|
37
|
+
#
|
|
38
|
+
def data_class_for(point_names)
|
|
39
|
+
(@data_classes ||= {})[point_names] ||= Data.define(*point_names)
|
|
40
|
+
end
|
|
41
|
+
|
|
30
42
|
private
|
|
31
43
|
|
|
32
44
|
def cached_plan_for(opts, max_cache_size)
|
|
@@ -57,6 +69,10 @@ class Serega
|
|
|
57
69
|
# SeregaPlan instance methods
|
|
58
70
|
#
|
|
59
71
|
module InstanceMethods
|
|
72
|
+
# Shows if plan includes batch points
|
|
73
|
+
# @return [Array<SeregaPlanPoint>] points to serialize
|
|
74
|
+
attr_reader :has_batch_points
|
|
75
|
+
|
|
60
76
|
# Parent plan point
|
|
61
77
|
# @return [SeregaPlanPoint, nil]
|
|
62
78
|
attr_reader :parent_plan_point
|
|
@@ -65,9 +81,9 @@ class Serega
|
|
|
65
81
|
# @return [Array<SeregaPlanPoint>] points to serialize
|
|
66
82
|
attr_reader :points
|
|
67
83
|
|
|
68
|
-
#
|
|
69
|
-
# @return [
|
|
70
|
-
attr_reader :
|
|
84
|
+
# Named serialization points
|
|
85
|
+
# @return [Hash] Named points
|
|
86
|
+
attr_reader :points_hash
|
|
71
87
|
|
|
72
88
|
#
|
|
73
89
|
# Instantiate new serialization plan
|
|
@@ -87,6 +103,7 @@ class Serega
|
|
|
87
103
|
@has_batch_points = false # should be before assigning points, generated points can change this attribute
|
|
88
104
|
@parent_plan_point = parent_plan_point
|
|
89
105
|
@points = attributes_points(modifiers)
|
|
106
|
+
@points_hash = points.to_h { |point| [point.name, point] }
|
|
90
107
|
end
|
|
91
108
|
|
|
92
109
|
#
|
|
@@ -96,6 +113,15 @@ class Serega
|
|
|
96
113
|
self.class.serializer_class
|
|
97
114
|
end
|
|
98
115
|
|
|
116
|
+
# Returns the Data class whose members match this plan's serialized fields.
|
|
117
|
+
# Delegates to the class-level cache so identical field sets share one Data class.
|
|
118
|
+
#
|
|
119
|
+
# @return [Class] Subclass of Data
|
|
120
|
+
#
|
|
121
|
+
def data_class
|
|
122
|
+
@data_class ||= self.class.data_class_for(point_names)
|
|
123
|
+
end
|
|
124
|
+
|
|
99
125
|
#
|
|
100
126
|
# Marks current plan and top-level plans as `has_batch_points`
|
|
101
127
|
# It is needed to initialize Batch Attribute Loaders when serialization starts
|
|
@@ -130,6 +156,10 @@ class Serega
|
|
|
130
156
|
|
|
131
157
|
points.freeze
|
|
132
158
|
end
|
|
159
|
+
|
|
160
|
+
def point_names
|
|
161
|
+
@point_names ||= points.map(&:name)
|
|
162
|
+
end
|
|
133
163
|
end
|
|
134
164
|
|
|
135
165
|
extend ClassMethods
|
data/lib/serega/plugins/if/if.rb
CHANGED
|
@@ -69,6 +69,7 @@ class Serega
|
|
|
69
69
|
serializer_class::SeregaPlanPoint.include(PlanPointInstanceMethods)
|
|
70
70
|
serializer_class::CheckAttributeParams.include(CheckAttributeParamsInstanceMethods)
|
|
71
71
|
serializer_class::SeregaObjectSerializer.include(ObjectSerializerInstanceMethods)
|
|
72
|
+
serializer_class::SeregaDataBuilder.extend(DataBuilderClassMethods)
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
#
|
|
@@ -215,6 +216,7 @@ class Serega
|
|
|
215
216
|
when "1" then condition.call(object)
|
|
216
217
|
when "2" then condition.call(object, context)
|
|
217
218
|
when "1_ctx" then condition.call(object, ctx: context)
|
|
219
|
+
when "2_ctx" then condition.call(object, context, ctx: context)
|
|
218
220
|
else # "0"
|
|
219
221
|
condition.call
|
|
220
222
|
end
|
|
@@ -239,6 +241,27 @@ class Serega
|
|
|
239
241
|
end
|
|
240
242
|
end
|
|
241
243
|
|
|
244
|
+
#
|
|
245
|
+
# SeregaDataBuilder additional/patched class methods
|
|
246
|
+
#
|
|
247
|
+
# Overrides `build_data_object` to handle plans where some keys were skipped
|
|
248
|
+
# by `:if`/`:unless`/`:if_value`/`:unless_value` conditions, so the Data class
|
|
249
|
+
# is built from the actually-present keys rather than the full plan.
|
|
250
|
+
#
|
|
251
|
+
# @see Serega::SeregaDataBuilder
|
|
252
|
+
#
|
|
253
|
+
module DataBuilderClassMethods
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
def build_data_object(plan, hash_data)
|
|
257
|
+
if hash_data.size < plan.points.size
|
|
258
|
+
plan.class.data_class_for(hash_data.keys).new(**hash_data)
|
|
259
|
+
else
|
|
260
|
+
super
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
242
265
|
#
|
|
243
266
|
# SeregaObjectSerializer additional/patched class methods
|
|
244
267
|
#
|
|
@@ -47,6 +47,8 @@ class Serega
|
|
|
47
47
|
true
|
|
48
48
|
when "1_ctx" # (object, :ctx)
|
|
49
49
|
true
|
|
50
|
+
when "2_ctx" # (object, context, :ctx)
|
|
51
|
+
true
|
|
50
52
|
else
|
|
51
53
|
false
|
|
52
54
|
end
|
|
@@ -55,10 +57,11 @@ class Serega
|
|
|
55
57
|
def signature_error
|
|
56
58
|
<<~ERROR.strip
|
|
57
59
|
Invalid attribute option :if parameters, valid parameters signatures:
|
|
58
|
-
- ()
|
|
59
|
-
- (object)
|
|
60
|
-
- (object, context)
|
|
61
|
-
- (object, :ctx)
|
|
60
|
+
- () # no parameters
|
|
61
|
+
- (object) # one positional parameter
|
|
62
|
+
- (object, context) # two positional parameters
|
|
63
|
+
- (object, :ctx) # one positional parameter and :ctx keyword
|
|
64
|
+
- (object, context, :ctx) # two positional parameters and :ctx keyword
|
|
62
65
|
ERROR
|
|
63
66
|
end
|
|
64
67
|
end
|
|
@@ -52,6 +52,8 @@ class Serega
|
|
|
52
52
|
true
|
|
53
53
|
when "1_ctx" # (value, :ctx)
|
|
54
54
|
true
|
|
55
|
+
when "2_ctx" # (value, context, :ctx)
|
|
56
|
+
true
|
|
55
57
|
else
|
|
56
58
|
false
|
|
57
59
|
end
|
|
@@ -60,10 +62,11 @@ class Serega
|
|
|
60
62
|
def signature_error
|
|
61
63
|
<<~ERROR.strip
|
|
62
64
|
Invalid attribute option :if_value parameters, valid parameters signatures:
|
|
63
|
-
- ()
|
|
64
|
-
- (value)
|
|
65
|
-
- (value, context)
|
|
66
|
-
- (value, :ctx)
|
|
65
|
+
- () # no parameters
|
|
66
|
+
- (value) # one positional parameter
|
|
67
|
+
- (value, context) # two positional parameters
|
|
68
|
+
- (value, :ctx) # one positional parameter and :ctx keyword
|
|
69
|
+
- (value, context, :ctx) # two positional parameters and :ctx keyword
|
|
67
70
|
ERROR
|
|
68
71
|
end
|
|
69
72
|
end
|
|
@@ -47,6 +47,8 @@ class Serega
|
|
|
47
47
|
true
|
|
48
48
|
when "1_ctx" # (object, :ctx)
|
|
49
49
|
true
|
|
50
|
+
when "2_ctx" # (object, context, :ctx)
|
|
51
|
+
true
|
|
50
52
|
else
|
|
51
53
|
false
|
|
52
54
|
end
|
|
@@ -55,10 +57,11 @@ class Serega
|
|
|
55
57
|
def signature_error
|
|
56
58
|
<<~ERROR.strip
|
|
57
59
|
Invalid attribute option :unless parameters, valid parameters signatures:
|
|
58
|
-
- ()
|
|
59
|
-
- (object)
|
|
60
|
-
- (object, context)
|
|
61
|
-
- (object, :ctx)
|
|
60
|
+
- () # no parameters
|
|
61
|
+
- (object) # one positional parameter
|
|
62
|
+
- (object, context) # two positional parameters
|
|
63
|
+
- (object, :ctx) # one positional parameter and :ctx keyword
|
|
64
|
+
- (object, context, :ctx) # two positional parameters and :ctx keyword
|
|
62
65
|
ERROR
|
|
63
66
|
end
|
|
64
67
|
end
|
|
@@ -52,6 +52,8 @@ class Serega
|
|
|
52
52
|
true
|
|
53
53
|
when "1_ctx" # (value, :ctx)
|
|
54
54
|
true
|
|
55
|
+
when "2_ctx" # (value, context, :ctx)
|
|
56
|
+
true
|
|
55
57
|
else
|
|
56
58
|
false
|
|
57
59
|
end
|
|
@@ -60,10 +62,11 @@ class Serega
|
|
|
60
62
|
def signature_error
|
|
61
63
|
<<~ERROR.strip
|
|
62
64
|
Invalid attribute option :unless_value parameters, valid parameters signatures:
|
|
63
|
-
- ()
|
|
64
|
-
- (value)
|
|
65
|
-
- (value, context)
|
|
66
|
-
- (value, :ctx)
|
|
65
|
+
- () # no parameters
|
|
66
|
+
- (value) # one positional parameter
|
|
67
|
+
- (value, context) # two positional parameters
|
|
68
|
+
- (value, :ctx) # one positional parameter and :ctx keyword
|
|
69
|
+
- (value, context, :ctx) # two positional parameters and :ctx keyword
|
|
67
70
|
ERROR
|
|
68
71
|
end
|
|
69
72
|
end
|
|
@@ -6,16 +6,28 @@ require "forwardable"
|
|
|
6
6
|
class Serega
|
|
7
7
|
module SeregaPlugins
|
|
8
8
|
#
|
|
9
|
-
# Plugin
|
|
9
|
+
# Plugin :presenter — moves computed attribute logic into a dedicated Presenter class.
|
|
10
10
|
#
|
|
11
|
-
#
|
|
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
|
+
# class UserSerializer < Serega
|
|
12
19
|
# plugin :presenter
|
|
13
20
|
#
|
|
14
21
|
# attribute :name
|
|
22
|
+
# attribute :role
|
|
15
23
|
#
|
|
16
24
|
# class Presenter
|
|
17
25
|
# def name
|
|
18
|
-
# [first_name, last_name].
|
|
26
|
+
# [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# def role
|
|
30
|
+
# id == __ctx__[:current_user_id] ? :self : :other
|
|
19
31
|
# end
|
|
20
32
|
# end
|
|
21
33
|
# end
|
|
@@ -56,6 +68,19 @@ class Serega
|
|
|
56
68
|
class Presenter < SimpleDelegator
|
|
57
69
|
# Presenter instance methods
|
|
58
70
|
module InstanceMethods
|
|
71
|
+
#
|
|
72
|
+
# @param object [Object] Serialized object to wrap
|
|
73
|
+
# @param ctx [Hash, nil] Serialization context
|
|
74
|
+
#
|
|
75
|
+
def initialize(object, ctx = nil)
|
|
76
|
+
super(object)
|
|
77
|
+
@__ctx__ = ctx
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
attr_reader :__ctx__
|
|
83
|
+
|
|
59
84
|
#
|
|
60
85
|
# Delegates all missing methods to serialized object.
|
|
61
86
|
#
|
|
@@ -100,10 +125,10 @@ class Serega
|
|
|
100
125
|
private
|
|
101
126
|
|
|
102
127
|
#
|
|
103
|
-
# Replaces serialized object with Presenter.new(object)
|
|
128
|
+
# Replaces serialized object with Presenter.new(object, ctx)
|
|
104
129
|
#
|
|
105
130
|
def serialize_object(object)
|
|
106
|
-
object = self.class.serializer_class::Presenter.new(object)
|
|
131
|
+
object = self.class.serializer_class::Presenter.new(object, context)
|
|
107
132
|
super
|
|
108
133
|
end
|
|
109
134
|
end
|
|
@@ -92,6 +92,7 @@ class Serega
|
|
|
92
92
|
serializer_class.extend(ClassMethods)
|
|
93
93
|
serializer_class.include(InstanceMethods)
|
|
94
94
|
serializer_class::SeregaConfig.include(ConfigInstanceMethods)
|
|
95
|
+
serializer_class::SeregaDataBuilder.extend(DataBuilderClassMethods)
|
|
95
96
|
end
|
|
96
97
|
|
|
97
98
|
#
|
|
@@ -214,6 +215,22 @@ class Serega
|
|
|
214
215
|
# @see Serega
|
|
215
216
|
#
|
|
216
217
|
module InstanceMethods
|
|
218
|
+
#
|
|
219
|
+
# Serializes provided object to a tree of Ruby Data objects.
|
|
220
|
+
# When a root key is configured, the result is wrapped in an outer Data object
|
|
221
|
+
# whose members include the root key plus any metadata keys.
|
|
222
|
+
#
|
|
223
|
+
# @param object [Object] Serialized object
|
|
224
|
+
# @param opts [Hash, nil] Serializing options (`:root`, `:many`, `:context`, etc.)
|
|
225
|
+
#
|
|
226
|
+
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
227
|
+
#
|
|
228
|
+
def to_data(object, opts = nil)
|
|
229
|
+
opts = prepare_initial_serialization_opts(object, opts)
|
|
230
|
+
serialized_data = serialize(object, opts)
|
|
231
|
+
self.class::SeregaDataBuilder.call(self, serialized_data, opts)
|
|
232
|
+
end
|
|
233
|
+
|
|
217
234
|
private
|
|
218
235
|
|
|
219
236
|
def serialize(object, opts)
|
|
@@ -230,6 +247,69 @@ class Serega
|
|
|
230
247
|
(opts.fetch(:many) { object.is_a?(Enumerable) }) ? root.many : root.one
|
|
231
248
|
end
|
|
232
249
|
end
|
|
250
|
+
|
|
251
|
+
#
|
|
252
|
+
# SeregaDataBuilder additional/patched class methods
|
|
253
|
+
#
|
|
254
|
+
# Overrides `call` to handle the root key: the serialized result is
|
|
255
|
+
# unwrapped from the root key, converted to a Data tree by the base
|
|
256
|
+
# implementation, and then re-wrapped together with any metadata keys
|
|
257
|
+
# in an outer Data object. Metadata values (arbitrary hashes/arrays)
|
|
258
|
+
# are recursively converted to Data objects via `deep_hash_to_data`.
|
|
259
|
+
#
|
|
260
|
+
# When the root key is `nil` (disabled), delegates directly to the base.
|
|
261
|
+
#
|
|
262
|
+
# @see Serega::SeregaDataBuilder
|
|
263
|
+
#
|
|
264
|
+
module DataBuilderClassMethods
|
|
265
|
+
#
|
|
266
|
+
# @param serializer [Serega] Serializer instance carrying the plan
|
|
267
|
+
# @param serialized_result [Hash, Array, nil] Full serialized output (possibly wrapped under root key)
|
|
268
|
+
# @param serialization_opts [Hash] Serialization options used to determine the root key
|
|
269
|
+
#
|
|
270
|
+
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
271
|
+
#
|
|
272
|
+
def call(serializer, serialized_result, serialization_opts)
|
|
273
|
+
root_key = resolve_root_key(serializer, serialization_opts)
|
|
274
|
+
return super(serializer, serialized_result) unless root_key
|
|
275
|
+
|
|
276
|
+
root_data = super(serializer, serialized_result.fetch(root_key))
|
|
277
|
+
build_data_objects(serialized_result, root_key, root_data)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
private
|
|
281
|
+
|
|
282
|
+
def resolve_root_key(serializer, serialization_opts)
|
|
283
|
+
if serialization_opts.key?(:root)
|
|
284
|
+
serialization_opts[:root]
|
|
285
|
+
else
|
|
286
|
+
config_root = serializer.class.config.root
|
|
287
|
+
serialization_opts[:many] ? config_root.many : config_root.one
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def build_data_objects(serialized_result, root_key, root_data)
|
|
292
|
+
converted_hash =
|
|
293
|
+
serialized_result.to_h do |key, val|
|
|
294
|
+
value = (key == root_key) ? root_data : deep_hash_to_data(val)
|
|
295
|
+
[key, value]
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
Data.define(*converted_hash.keys).new(**converted_hash)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def deep_hash_to_data(value)
|
|
302
|
+
case value
|
|
303
|
+
when Hash
|
|
304
|
+
converted = value.transform_values { |nested| deep_hash_to_data(nested) }
|
|
305
|
+
Data.define(*value.keys).new(**converted)
|
|
306
|
+
when Array
|
|
307
|
+
value.map { |item| deep_hash_to_data(item) }
|
|
308
|
+
else
|
|
309
|
+
value
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
233
313
|
end
|
|
234
314
|
|
|
235
315
|
register_plugin(Root.plugin_name, Root)
|
data/lib/serega.rb
CHANGED
|
@@ -11,6 +11,10 @@ class Serega
|
|
|
11
11
|
# Frozen array
|
|
12
12
|
# @return [Array] frozen array
|
|
13
13
|
FROZEN_EMPTY_ARRAY = [].freeze
|
|
14
|
+
|
|
15
|
+
# Empty modifiers/serialization options (used when serializing with no opts provided)
|
|
16
|
+
FROZEN_EMPTY_OPTS = [FROZEN_EMPTY_HASH, nil].freeze
|
|
17
|
+
private_constant :FROZEN_EMPTY_OPTS
|
|
14
18
|
end
|
|
15
19
|
|
|
16
20
|
require_relative "serega/errors"
|
|
@@ -58,6 +62,7 @@ require_relative "serega/config"
|
|
|
58
62
|
require_relative "serega/object_serializer"
|
|
59
63
|
require_relative "serega/plan_point"
|
|
60
64
|
require_relative "serega/plan"
|
|
65
|
+
require_relative "serega/data_builder"
|
|
61
66
|
require_relative "serega/plugins"
|
|
62
67
|
|
|
63
68
|
class Serega
|
|
@@ -230,25 +235,45 @@ class Serega
|
|
|
230
235
|
# @return [Hash] Serialization result
|
|
231
236
|
#
|
|
232
237
|
def call(object, opts = nil)
|
|
233
|
-
opts
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if opts.empty?
|
|
237
|
-
modifiers_opts = FROZEN_EMPTY_HASH
|
|
238
|
-
serialize_opts = nil
|
|
239
|
-
else
|
|
240
|
-
opts.transform_keys!(&:to_sym)
|
|
241
|
-
serialize_opts = opts.except(*initiate_keys)
|
|
242
|
-
modifiers_opts = opts.slice(*initiate_keys)
|
|
243
|
-
end
|
|
244
|
-
|
|
238
|
+
opts = opts&.transform_keys(&:to_sym)
|
|
239
|
+
modifiers_opts = init_modifier_opts(opts)
|
|
240
|
+
serialize_opts = init_serialize_opts(opts)
|
|
245
241
|
new(modifiers_opts).to_h(object, serialize_opts)
|
|
246
242
|
end
|
|
247
243
|
|
|
244
|
+
#
|
|
245
|
+
# Serializes provided object to a tree of Ruby Data objects
|
|
246
|
+
#
|
|
247
|
+
# @param object [Object] Serialized object
|
|
248
|
+
# @param opts [Hash, nil] Serializer modifiers and other instantiating options
|
|
249
|
+
# @option opts [Array, Hash, String, Symbol] :only The only attributes to serialize
|
|
250
|
+
# @option opts [Array, Hash, String, Symbol] :except Attributes to hide
|
|
251
|
+
# @option opts [Array, Hash, String, Symbol] :with Attributes (usually hidden) to serialize additionally
|
|
252
|
+
# @option opts [Boolean] :validate Validates provided modifiers (Default is true)
|
|
253
|
+
# @option opts [Hash] :context Serialization context
|
|
254
|
+
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
|
|
255
|
+
#
|
|
256
|
+
# @return [Data, Array<Data>, nil] Serialization result as Data object(s)
|
|
257
|
+
#
|
|
258
|
+
def to_data(object, opts = nil)
|
|
259
|
+
opts = opts&.transform_keys(&:to_sym)
|
|
260
|
+
modifiers_opts = init_modifier_opts(opts)
|
|
261
|
+
serialize_opts = init_serialize_opts(opts)
|
|
262
|
+
new(modifiers_opts).to_data(object, serialize_opts)
|
|
263
|
+
end
|
|
264
|
+
|
|
248
265
|
alias_method :to_h, :call
|
|
249
266
|
|
|
250
267
|
private
|
|
251
268
|
|
|
269
|
+
def init_modifier_opts(opts)
|
|
270
|
+
(!opts || opts.empty?) ? FROZEN_EMPTY_HASH : opts.slice(*config.initiate_keys)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def init_serialize_opts(opts)
|
|
274
|
+
(!opts || opts.empty?) ? nil : opts.except(*config.initiate_keys)
|
|
275
|
+
end
|
|
276
|
+
|
|
252
277
|
# Patched in:
|
|
253
278
|
# - plugin :metadata (defines MetaAttribute and copies meta_attributes to subclasses)
|
|
254
279
|
# - plugin :presenter (defines Presenter)
|
|
@@ -266,6 +291,10 @@ class Serega
|
|
|
266
291
|
attribute_normalizer_class.serializer_class = subclass
|
|
267
292
|
subclass.const_set(:SeregaAttributeNormalizer, attribute_normalizer_class)
|
|
268
293
|
|
|
294
|
+
data_builder_class = Class.new(self::SeregaDataBuilder)
|
|
295
|
+
data_builder_class.serializer_class = subclass
|
|
296
|
+
subclass.const_set(:SeregaDataBuilder, data_builder_class)
|
|
297
|
+
|
|
269
298
|
plan_class = Class.new(self::SeregaPlan)
|
|
270
299
|
plan_class.serializer_class = subclass
|
|
271
300
|
subclass.const_set(:SeregaPlan, plan_class)
|
|
@@ -355,17 +384,14 @@ class Serega
|
|
|
355
384
|
# Serializes provided object to Hash
|
|
356
385
|
#
|
|
357
386
|
# @param object [Object] Serialized object
|
|
358
|
-
# @param opts [Hash, nil]
|
|
387
|
+
# @param opts [Hash, nil] Serializing options
|
|
359
388
|
# @option opts [Hash] :context Serialization context
|
|
360
389
|
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
|
|
361
390
|
#
|
|
362
391
|
# @return [Hash] Serialization result
|
|
363
392
|
#
|
|
364
393
|
def call(object, opts = nil)
|
|
365
|
-
opts =
|
|
366
|
-
self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
|
|
367
|
-
|
|
368
|
-
opts[:context] ||= {}
|
|
394
|
+
opts = prepare_initial_serialization_opts(object, opts)
|
|
369
395
|
serialize(object, opts)
|
|
370
396
|
end
|
|
371
397
|
|
|
@@ -374,6 +400,24 @@ class Serega
|
|
|
374
400
|
call(object, opts)
|
|
375
401
|
end
|
|
376
402
|
|
|
403
|
+
#
|
|
404
|
+
# Serializes provided object to Data objects
|
|
405
|
+
# Patched in:
|
|
406
|
+
# - plugin :root (adds a data-object for a root level keys)
|
|
407
|
+
#
|
|
408
|
+
# @param object [Object] Serialized object
|
|
409
|
+
# @param opts [Hash, nil] Serializing options
|
|
410
|
+
# @option opts [Hash] :context Serialization context
|
|
411
|
+
# @option opts [Boolean] :many Set true if provided multiple objects (Default `object.is_a?(Enumerable)`)
|
|
412
|
+
#
|
|
413
|
+
# @return [Data] Serialization result
|
|
414
|
+
#
|
|
415
|
+
def to_data(object, opts = nil)
|
|
416
|
+
opts = prepare_initial_serialization_opts(object, opts)
|
|
417
|
+
serialized_data = serialize(object, opts)
|
|
418
|
+
self.class::SeregaDataBuilder.call(self, serialized_data)
|
|
419
|
+
end
|
|
420
|
+
|
|
377
421
|
# @return [Hash] merged preloads of all serialized attributes
|
|
378
422
|
def preloads
|
|
379
423
|
@preloads ||= SeregaUtils::PreloadsConstructor.call(plan)
|
|
@@ -404,15 +448,25 @@ class Serega
|
|
|
404
448
|
SeregaUtils::ToHash.call(value)
|
|
405
449
|
end
|
|
406
450
|
|
|
451
|
+
def prepare_initial_serialization_opts(object, opts)
|
|
452
|
+
opts = opts ? opts.transform_keys(&:to_sym) : {}
|
|
453
|
+
self.class::CheckSerializeParams.new(opts).validate unless opts.empty?
|
|
454
|
+
|
|
455
|
+
opts[:context] ||= {}
|
|
456
|
+
opts[:batch_loaders] = SeregaBatch::AttributeLoaders.new if plan.has_batch_points
|
|
457
|
+
opts[:many] = object.is_a?(Enumerable) unless opts.key?(:many)
|
|
458
|
+
opts[:plan] = plan
|
|
459
|
+
opts
|
|
460
|
+
end
|
|
461
|
+
|
|
407
462
|
# Patched in:
|
|
408
463
|
# - plugin :activerecord_preloads (loads defined :preloads to object)
|
|
409
464
|
# - plugin :root (wraps result `{ root => result }`)
|
|
410
465
|
# - plugin :context_metadata (adds context metadata to final result)
|
|
411
466
|
# - plugin :metadata (adds metadata to final result)
|
|
412
467
|
def serialize(object, opts)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
batch_loaders&.load_all(opts[:context])
|
|
468
|
+
result = self.class::SeregaObjectSerializer.new(**opts).serialize(object)
|
|
469
|
+
opts[:batch_loaders]&.load_all(opts[:context])
|
|
416
470
|
result
|
|
417
471
|
end
|
|
418
472
|
end
|
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.36.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrey Glushkov
|
|
@@ -37,6 +37,7 @@ files:
|
|
|
37
37
|
- lib/serega/batch/attribute_loaders.rb
|
|
38
38
|
- lib/serega/batch/loader.rb
|
|
39
39
|
- lib/serega/config.rb
|
|
40
|
+
- lib/serega/data_builder.rb
|
|
40
41
|
- lib/serega/errors.rb
|
|
41
42
|
- lib/serega/helpers/serializer_class_helper.rb
|
|
42
43
|
- lib/serega/object_serializer.rb
|
|
@@ -114,14 +115,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
114
115
|
requirements:
|
|
115
116
|
- - ">="
|
|
116
117
|
- !ruby/object:Gem::Version
|
|
117
|
-
version: 2.
|
|
118
|
+
version: 3.2.0
|
|
118
119
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
120
|
requirements:
|
|
120
121
|
- - ">="
|
|
121
122
|
- !ruby/object:Gem::Version
|
|
122
123
|
version: '0'
|
|
123
124
|
requirements: []
|
|
124
|
-
rubygems_version: 4.0.
|
|
125
|
+
rubygems_version: 4.0.11
|
|
125
126
|
specification_version: 4
|
|
126
127
|
summary: JSON Serializer
|
|
127
128
|
test_files: []
|