serega 0.37.2 → 0.39.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 +124 -26
- data/VERSION +1 -1
- data/lib/serega/attribute.rb +3 -3
- data/lib/serega/attribute_normalizer.rb +87 -14
- data/lib/serega/attribute_value_resolvers/batch.rb +4 -0
- data/lib/serega/config.rb +48 -1
- data/lib/serega/engine/level.rb +57 -0
- data/lib/serega/engine/level_queue.rb +54 -0
- data/lib/serega/{batch → engine}/loader.rb +1 -1
- data/lib/serega/object_serializer.rb +61 -56
- data/lib/serega/plan.rb +0 -9
- data/lib/serega/plan_point.rb +56 -5
- data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +21 -1
- data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +3 -2
- data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +6 -4
- data/lib/serega/plugins/if/if.rb +9 -10
- data/lib/serega/plugins/presenter/presenter.rb +101 -5
- data/lib/serega/plugins/root/root.rb +1 -1
- data/lib/serega/utils/collection_detector.rb +26 -0
- data/lib/serega/validations/attribute/check_block.rb +20 -55
- data/lib/serega/validations/attribute/check_opt_base_serializer.rb +37 -0
- data/lib/serega/validations/attribute/check_opt_batch.rb +6 -6
- data/lib/serega/validations/attribute/check_opt_const.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_delegate.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_many.rb +8 -7
- data/lib/serega/validations/attribute/check_opt_method.rb +3 -4
- data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -1
- data/lib/serega/validations/attribute/check_opt_value.rb +3 -4
- data/lib/serega/validations/check_attribute_params.rb +8 -7
- data/lib/serega.rb +33 -29
- metadata +6 -5
- data/lib/serega/batch/attribute_loader.rb +0 -82
- data/lib/serega/batch/attribute_loaders.rb +0 -117
- data/lib/serega/batch/level.rb +0 -48
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
class Serega
|
|
4
4
|
#
|
|
5
|
-
# Low-level class
|
|
6
|
-
#
|
|
5
|
+
# Low-level class used by the serializer to construct the serialized response.
|
|
6
|
+
#
|
|
7
|
+
# Serialization is level-by-level. `#serialize` builds the result container(s)
|
|
8
|
+
# and enqueues this level; the batch queue later calls `#process` for each level,
|
|
9
|
+
# which resolves every attribute for every object and enqueues child levels for
|
|
10
|
+
# relations.
|
|
7
11
|
#
|
|
8
12
|
class SeregaObjectSerializer
|
|
9
13
|
#
|
|
10
14
|
# SeregaObjectSerializer instance methods
|
|
11
15
|
#
|
|
12
16
|
module InstanceMethods
|
|
13
|
-
attr_reader :context, :plan, :many, :opts, :
|
|
17
|
+
attr_reader :context, :plan, :many, :opts, :level_queue
|
|
14
18
|
|
|
15
19
|
# @param plan [SeregaPlan] Serialization plan
|
|
16
20
|
# @param context [Hash] Serialization context
|
|
@@ -23,85 +27,86 @@ class Serega
|
|
|
23
27
|
@plan = plan
|
|
24
28
|
@many = many
|
|
25
29
|
@opts = opts
|
|
26
|
-
@
|
|
30
|
+
@level_queue = opts[:level_queue]
|
|
27
31
|
end
|
|
28
32
|
|
|
29
|
-
#
|
|
33
|
+
# Enqueues this level and returns its result container(s). The containers are
|
|
34
|
+
# returned immediately but filled in place once the queue is processed.
|
|
30
35
|
#
|
|
31
|
-
# @param object [Object] Serialized object
|
|
36
|
+
# @param object [Object] Serialized object(s)
|
|
32
37
|
#
|
|
33
38
|
# @return [Hash, Array<Hash>, nil] Serialized object(s)
|
|
34
39
|
def serialize(object)
|
|
35
40
|
return if object.nil?
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
else
|
|
41
|
-
batch_loaders.add_objects(plan, [object]) if plan.batch?
|
|
42
|
-
serialize_object(object)
|
|
42
|
+
case serialize_mode(object)
|
|
43
|
+
when :many then enqueue(object.to_a)
|
|
44
|
+
when :many_for_one then enqueue([object]) # `many` on, but a sole object was given — wrap it, don't raise
|
|
45
|
+
else enqueue([object])[0] # :one
|
|
43
46
|
end
|
|
44
47
|
end
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
# Resolves every attribute of one level onto its containers. For each point,
|
|
50
|
+
# preloads and batch loaders run once over all of the level's objects; the
|
|
51
|
+
# value is then resolved and assigned per object, in attribute order.
|
|
52
|
+
#
|
|
53
|
+
# @param level [SeregaEngine::Level] level to resolve
|
|
54
|
+
# @return [void]
|
|
55
|
+
def process(level)
|
|
56
|
+
objects = level.objects
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
plan.points.each do |point|
|
|
59
|
+
point.run_preloads(objects)
|
|
60
|
+
batches = point.load_batches(level)
|
|
61
|
+
# One child serializer per relation point per level (reused for every object),
|
|
62
|
+
# instead of one per object — child objects are grouped into one child level anyway.
|
|
63
|
+
child_serializer = point.child_serializer(context: context, **opts)
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
rescue => error
|
|
58
|
-
SeregaUtils::SerializedAttributeError.call(error, point)
|
|
65
|
+
objects.each_with_index do |object, index|
|
|
66
|
+
resolve_point(object, point, level.containers[index], batches, child_serializer)
|
|
67
|
+
rescue => error
|
|
68
|
+
SeregaUtils::SerializedAttributeError.call(error, point)
|
|
69
|
+
end
|
|
59
70
|
end
|
|
60
71
|
end
|
|
61
72
|
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Enqueues this chunk of objects onto the queue and returns their result
|
|
76
|
+
# containers. This is where objects enter their level, so every object a
|
|
77
|
+
# point resolves against and a batch loader receives has the same shape.
|
|
78
|
+
#
|
|
62
79
|
# Patched in:
|
|
63
|
-
# - plugin :
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
attacher = lambda { |obj, batches| attach_value(obj, point, container, batches: batches) }
|
|
67
|
-
batch_loaders.remember(point, object, attacher)
|
|
68
|
-
container[point.name] = nil # Reserve attribute place in resulted hash. We will set correct value later
|
|
69
|
-
else
|
|
70
|
-
attach_value(object, point, container, batches: nil)
|
|
71
|
-
end
|
|
80
|
+
# - plugin :presenter (wraps each object in a Presenter before enqueueing)
|
|
81
|
+
def enqueue(objects)
|
|
82
|
+
level_queue.enqueue(self, objects)
|
|
72
83
|
end
|
|
73
84
|
|
|
74
|
-
|
|
85
|
+
# Patched in:
|
|
86
|
+
# - plugin :if (skips the point for objects failing an :if/:unless condition)
|
|
87
|
+
def resolve_point(object, point, container, batches, child_serializer)
|
|
75
88
|
value = point.value(object, context, batches: batches)
|
|
76
|
-
final_value =
|
|
77
|
-
|
|
89
|
+
final_value = child_serializer ? child_serializer.serialize(value) : value
|
|
90
|
+
write_value(final_value, point, container)
|
|
78
91
|
end
|
|
79
92
|
|
|
80
93
|
# Patched in:
|
|
81
|
-
# - plugin :if (
|
|
82
|
-
def
|
|
94
|
+
# - plugin :if (skips assigning when an :if_value/:unless_value condition fails)
|
|
95
|
+
def write_value(final_value, point, container)
|
|
83
96
|
container[point.name] = final_value
|
|
84
97
|
end
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
plan: point.child_plan,
|
|
98
|
-
many: point.many,
|
|
99
|
-
**opts
|
|
100
|
-
)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def array?(object, many)
|
|
104
|
-
many.nil? ? object.is_a?(Enumerable) : many
|
|
99
|
+
# How to serialize `object`, deciding whether the result is a collection or a
|
|
100
|
+
# single object and checking the object type only once:
|
|
101
|
+
# - :many — `many` is on and the object is a collection
|
|
102
|
+
# - :many_for_one — `many` is on but a sole object was given (wrap it, don't raise)
|
|
103
|
+
# - :one — serialize the object on its own
|
|
104
|
+
def serialize_mode(object)
|
|
105
|
+
case many
|
|
106
|
+
when NilClass then SeregaUtils::CollectionDetector.call(object) ? :many : :one
|
|
107
|
+
when TrueClass then SeregaUtils::CollectionDetector.call(object) ? :many : :many_for_one
|
|
108
|
+
else :one # many == false
|
|
109
|
+
end
|
|
105
110
|
end
|
|
106
111
|
end
|
|
107
112
|
|
data/lib/serega/plan.rb
CHANGED
|
@@ -108,15 +108,6 @@ class Serega
|
|
|
108
108
|
self.class.serializer_class
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
-
#
|
|
112
|
-
# @return [Boolean] whether any point of this plan is batch loaded
|
|
113
|
-
#
|
|
114
|
-
def batch?
|
|
115
|
-
return @batch if defined?(@batch)
|
|
116
|
-
|
|
117
|
-
@batch = points.any?(&:batch?)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
111
|
# Returns the Data class whose members match this plan's serialized fields.
|
|
121
112
|
# Delegates to the class-level cache so identical field sets share one Data class.
|
|
122
113
|
#
|
data/lib/serega/plan_point.rb
CHANGED
|
@@ -68,23 +68,74 @@ class Serega
|
|
|
68
68
|
attribute.serializer
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
-
def batch?
|
|
72
|
-
!attribute.batch_loaders.empty?
|
|
73
|
-
end
|
|
74
|
-
|
|
75
71
|
# Attribute `batch_loaders`
|
|
76
72
|
# @see SeregaAttribute::AttributeInstanceMethods#batch_loaders
|
|
77
73
|
def batch_loaders
|
|
78
74
|
attribute.batch_loaders
|
|
79
75
|
end
|
|
80
76
|
|
|
77
|
+
# Attribute `preloads`
|
|
78
|
+
# @see SeregaAttribute::AttributeInstanceMethods#preloads
|
|
79
|
+
def preloads
|
|
80
|
+
attribute.preloads
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Runs this point's declared preloads over the given objects using the
|
|
84
|
+
# serializer's registered preload handler.
|
|
85
|
+
#
|
|
86
|
+
# @param objects [Array] objects serialized at this point's level
|
|
87
|
+
# @return [void]
|
|
88
|
+
def run_preloads(objects)
|
|
89
|
+
return unless preloads
|
|
90
|
+
|
|
91
|
+
handler = self.class.serializer_class.preload_with
|
|
92
|
+
unless handler
|
|
93
|
+
raise SeregaError, "The :preload option requires a preload handler. Register one with `preload_with` (the :activerecord_preloads plugin does this for you)."
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
handler.call(objects, preloads)
|
|
97
|
+
rescue => error
|
|
98
|
+
SeregaUtils::SerializedAttributeError.call(error, self)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Loads the batch loaders this point's value needs, each once for the whole
|
|
102
|
+
# level, and returns them keyed by loader name for #value to read from.
|
|
81
103
|
#
|
|
82
|
-
# @
|
|
104
|
+
# @param level [SeregaEngine::Level] level whose objects are loaded for
|
|
105
|
+
# @return [Hash, nil] loaded data per loader name, or nil when none are needed
|
|
106
|
+
def load_batches(level)
|
|
107
|
+
names = batch_loaders
|
|
108
|
+
return if names.empty?
|
|
109
|
+
|
|
110
|
+
loaders = self.class.serializer_class.batch_loaders
|
|
111
|
+
names.each_with_object({}) do |name, batches|
|
|
112
|
+
batches[name] = level.fetch(loaders[name])
|
|
113
|
+
end
|
|
114
|
+
rescue => error
|
|
115
|
+
SeregaUtils::SerializedAttributeError.call(error, self)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
#
|
|
119
|
+
# @return [Class<SeregaObjectSerializer>] object serializer class for child plan
|
|
83
120
|
#
|
|
84
121
|
def child_object_serializer
|
|
85
122
|
serializer::SeregaObjectSerializer
|
|
86
123
|
end
|
|
87
124
|
|
|
125
|
+
# Builds the object serializer that serializes this point's relation, or nil
|
|
126
|
+
# when the point has no child plan (a plain attribute). The point owns the
|
|
127
|
+
# static config (child plan, serializer class, `many`); the caller injects the
|
|
128
|
+
# runtime `context` and `opts`.
|
|
129
|
+
#
|
|
130
|
+
# @param context [Hash] serialization context
|
|
131
|
+
# @param opts [Hash] extra object-serializer options (e.g. the level queue)
|
|
132
|
+
# @return [SeregaObjectSerializer, nil] serializer for the child level
|
|
133
|
+
def child_serializer(context:, **opts)
|
|
134
|
+
return unless child_plan
|
|
135
|
+
|
|
136
|
+
child_object_serializer.new(context: context, plan: child_plan, many: many, **opts)
|
|
137
|
+
end
|
|
138
|
+
|
|
88
139
|
private
|
|
89
140
|
|
|
90
141
|
def set_normalized_vars
|
|
@@ -82,7 +82,27 @@ class Serega
|
|
|
82
82
|
# @return [void]
|
|
83
83
|
#
|
|
84
84
|
def self.after_load_plugin(serializer_class, **_opts)
|
|
85
|
-
serializer_class.preload_with
|
|
85
|
+
serializer_class.preload_with do |objects, preloads|
|
|
86
|
+
Preloader.preload(ActiverecordPreloads.records(serializer_class, objects), preloads)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#
|
|
91
|
+
# The underlying records to preload onto. The :presenter plugin wraps every
|
|
92
|
+
# serialized object in a SimpleDelegator, but ActiveRecord's Preloader needs
|
|
93
|
+
# the real records, so unwrap them via #__getobj__ when presenter is used.
|
|
94
|
+
# Objects are wrapped only when the Presenter class has custom methods.
|
|
95
|
+
#
|
|
96
|
+
# @param serializer_class [Class<Serega>] Current serializer class
|
|
97
|
+
# @param objects [Array] objects serialized at the current level
|
|
98
|
+
#
|
|
99
|
+
# @return [Array] the underlying records
|
|
100
|
+
#
|
|
101
|
+
def self.records(serializer_class, objects)
|
|
102
|
+
return objects unless serializer_class.plugin_used?(:presenter)
|
|
103
|
+
return objects unless serializer_class.custom_presenter?
|
|
104
|
+
|
|
105
|
+
objects.map(&:__getobj__)
|
|
86
106
|
end
|
|
87
107
|
end
|
|
88
108
|
|
|
@@ -6,7 +6,8 @@ class Serega
|
|
|
6
6
|
# Plugin :explicit_many_option
|
|
7
7
|
#
|
|
8
8
|
# Plugin requires to add :many option when adding relationships
|
|
9
|
-
# (relationships are attributes with :serializer option
|
|
9
|
+
# (relationships are attributes with the :serializer option or a block
|
|
10
|
+
# defining a nested serializer)
|
|
10
11
|
#
|
|
11
12
|
# Adding this plugin makes it clearer to find if relationship returns array or single object
|
|
12
13
|
#
|
|
@@ -56,7 +57,7 @@ class Serega
|
|
|
56
57
|
def check_opts
|
|
57
58
|
super
|
|
58
59
|
|
|
59
|
-
CheckOptMany.call(opts)
|
|
60
|
+
CheckOptMany.call(opts, block)
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
end
|
|
@@ -10,23 +10,25 @@ class Serega
|
|
|
10
10
|
class << self
|
|
11
11
|
#
|
|
12
12
|
# Checks attribute :many option must be provided with relations
|
|
13
|
+
# (attributes with the :serializer option or a block defining
|
|
14
|
+
# a nested serializer)
|
|
13
15
|
#
|
|
14
16
|
# @param opts [Hash] Attribute options
|
|
17
|
+
# @param block [nil, Proc] Attribute block
|
|
15
18
|
#
|
|
16
19
|
# @raise [SeregaError] Attribute validation error
|
|
17
20
|
#
|
|
18
21
|
# @return [void]
|
|
19
22
|
#
|
|
20
|
-
def call(opts)
|
|
21
|
-
|
|
22
|
-
return unless serializer
|
|
23
|
+
def call(opts, block = nil)
|
|
24
|
+
return if !opts[:serializer] && !block
|
|
23
25
|
|
|
24
26
|
many_option_exists = opts.key?(:many)
|
|
25
27
|
return if many_option_exists
|
|
26
28
|
|
|
27
29
|
raise SeregaError,
|
|
28
30
|
"Attribute option :many [Boolean] must be provided" \
|
|
29
|
-
" for attributes with :serializer option"
|
|
31
|
+
" for attributes with :serializer option or a block"
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
end
|
data/lib/serega/plugins/if/if.rb
CHANGED
|
@@ -42,9 +42,6 @@ class Serega
|
|
|
42
42
|
# end
|
|
43
43
|
#
|
|
44
44
|
module If
|
|
45
|
-
# This value must be returned to identify that serialization key was skipped
|
|
46
|
-
KEY_SKIPPED = :_key_skipped_with_serega_if_plugin
|
|
47
|
-
|
|
48
45
|
# @return [Symbol] Plugin name
|
|
49
46
|
def self.plugin_name
|
|
50
47
|
:if
|
|
@@ -270,16 +267,18 @@ class Serega
|
|
|
270
267
|
module ObjectSerializerInstanceMethods
|
|
271
268
|
private
|
|
272
269
|
|
|
273
|
-
|
|
274
|
-
|
|
270
|
+
# Skip the whole point for this object (its value is never resolved).
|
|
271
|
+
def resolve_point(object, point, _container, _batches, _child_plan)
|
|
272
|
+
return unless point.satisfy_if_conditions?(object, context)
|
|
273
|
+
|
|
275
274
|
super
|
|
276
275
|
end
|
|
277
276
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
277
|
+
# Skip only the assignment based on the resolved value. Levels resolve
|
|
278
|
+
# points in attribute order, so simply not assigning keeps the remaining
|
|
279
|
+
# keys in declared order — no slot to reserve or delete.
|
|
280
|
+
def write_value(value, point, _container)
|
|
281
|
+
return unless point.satisfy_if_value_conditions?(value, context)
|
|
283
282
|
|
|
284
283
|
super
|
|
285
284
|
end
|
|
@@ -15,13 +15,19 @@ class Serega
|
|
|
15
15
|
# - The original object is accessible via __getobj__ (standard SimpleDelegator API).
|
|
16
16
|
# - The serialization context is accessible via the private method __ctx__.
|
|
17
17
|
#
|
|
18
|
+
# Objects are wrapped in the Presenter only when the serializer's Presenter
|
|
19
|
+
# class (or an inherited one) was actually extended with custom methods —
|
|
20
|
+
# a bare `plugin :presenter` adds no wrapping overhead. The check is
|
|
21
|
+
# denormalized: `SerializerClass.custom_presenter?` is asked once per
|
|
22
|
+
# object serializer and the result is reused for the whole level.
|
|
23
|
+
#
|
|
18
24
|
# class UserSerializer < Serega
|
|
19
25
|
# plugin :presenter
|
|
20
26
|
#
|
|
21
27
|
# attribute :name
|
|
22
28
|
# attribute :role
|
|
23
29
|
#
|
|
24
|
-
#
|
|
30
|
+
# presenter do
|
|
25
31
|
# def name
|
|
26
32
|
# [first_name, last_name].compact.join(' ') # first_name/last_name delegated to object
|
|
27
33
|
# end
|
|
@@ -31,6 +37,9 @@ class Serega
|
|
|
31
37
|
# end
|
|
32
38
|
# end
|
|
33
39
|
# end
|
|
40
|
+
#
|
|
41
|
+
# The `presenter do ... end` block is evaluated inside the serializer's own
|
|
42
|
+
# Presenter class, so multiple blocks accumulate.
|
|
34
43
|
module Presenter
|
|
35
44
|
# @return [Symbol] Plugin name
|
|
36
45
|
def self.plugin_name
|
|
@@ -62,6 +71,11 @@ class Serega
|
|
|
62
71
|
presenter_class = Class.new(Presenter)
|
|
63
72
|
presenter_class.serializer_class = serializer_class
|
|
64
73
|
serializer_class.const_set(:Presenter, presenter_class)
|
|
74
|
+
|
|
75
|
+
# The presenter's unwrap method returns the serialized object itself,
|
|
76
|
+
# not an association — it must never be auto-preloaded.
|
|
77
|
+
config = serializer_class.config
|
|
78
|
+
config.auto_preload_excluded_methods = config.auto_preload_excluded_methods | [:__getobj__]
|
|
65
79
|
end
|
|
66
80
|
|
|
67
81
|
# Presenter class
|
|
@@ -97,6 +111,44 @@ class Serega
|
|
|
97
111
|
extend SeregaHelpers::SerializerClassHelper
|
|
98
112
|
extend Forwardable
|
|
99
113
|
include InstanceMethods
|
|
114
|
+
|
|
115
|
+
# Tracks whether user code was added to the Presenter class.
|
|
116
|
+
#
|
|
117
|
+
# These singleton methods are defined after the base class body above,
|
|
118
|
+
# so the plugin's own includes do not mark the base class as modified.
|
|
119
|
+
# Lazy delegators defined by #method_missing do mark the class, but
|
|
120
|
+
# they can appear only on presenters that are already wrapping.
|
|
121
|
+
class << self
|
|
122
|
+
#
|
|
123
|
+
# Checks if this Presenter class (or an inherited one) was extended
|
|
124
|
+
# with custom user code and therefore objects must be wrapped
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean] whether custom presenter methods were defined
|
|
127
|
+
#
|
|
128
|
+
def modified?
|
|
129
|
+
return true if defined?(@modified)
|
|
130
|
+
return false if equal?(Presenter) # the plugin's base class — the walk stops here
|
|
131
|
+
|
|
132
|
+
superclass.modified?
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def include(*modules)
|
|
136
|
+
@modified = true
|
|
137
|
+
super
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def prepend(*modules)
|
|
141
|
+
@modified = true
|
|
142
|
+
super
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def method_added(name)
|
|
148
|
+
@modified = true
|
|
149
|
+
super
|
|
150
|
+
end
|
|
151
|
+
end
|
|
100
152
|
end
|
|
101
153
|
|
|
102
154
|
#
|
|
@@ -105,6 +157,36 @@ class Serega
|
|
|
105
157
|
# @see Serega
|
|
106
158
|
#
|
|
107
159
|
module ClassMethods
|
|
160
|
+
#
|
|
161
|
+
# Defines presenter methods — evaluates the block inside the
|
|
162
|
+
# serializer's own Presenter class. Multiple blocks accumulate.
|
|
163
|
+
#
|
|
164
|
+
# presenter do
|
|
165
|
+
# def name
|
|
166
|
+
# [first_name, last_name].compact.join(" ")
|
|
167
|
+
# end
|
|
168
|
+
# end
|
|
169
|
+
#
|
|
170
|
+
# @return [void]
|
|
171
|
+
#
|
|
172
|
+
def presenter(&block)
|
|
173
|
+
raise SeregaError, "Provide a block with presenter methods: `presenter do ... end`" unless block
|
|
174
|
+
|
|
175
|
+
self::Presenter.class_exec(&block)
|
|
176
|
+
nil
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
#
|
|
180
|
+
# Checks if the serializer's Presenter class (or an inherited one) was
|
|
181
|
+
# extended with custom user code. When it was not, serialized objects
|
|
182
|
+
# are not wrapped in the Presenter at all.
|
|
183
|
+
#
|
|
184
|
+
# @return [Boolean] whether custom presenter methods were defined
|
|
185
|
+
#
|
|
186
|
+
def custom_presenter?
|
|
187
|
+
self::Presenter.modified?
|
|
188
|
+
end
|
|
189
|
+
|
|
108
190
|
private
|
|
109
191
|
|
|
110
192
|
def inherited(subclass)
|
|
@@ -122,14 +204,28 @@ class Serega
|
|
|
122
204
|
# @see Serega::SeregaObjectSerializer
|
|
123
205
|
#
|
|
124
206
|
module SeregaObjectSerializerInstanceMethods
|
|
207
|
+
# The custom-presenter check is made once per object serializer here
|
|
208
|
+
# and its result is reused for every enqueued chunk of the level.
|
|
209
|
+
def initialize(**opts)
|
|
210
|
+
super
|
|
211
|
+
@wrap_in_presenter = self.class.serializer_class.custom_presenter?
|
|
212
|
+
end
|
|
213
|
+
|
|
125
214
|
private
|
|
126
215
|
|
|
127
216
|
#
|
|
128
|
-
#
|
|
217
|
+
# Wraps each serialized object in Presenter.new(object, ctx) before it is
|
|
218
|
+
# enqueued, so the whole level — value resolution and batch loaders alike —
|
|
219
|
+
# sees presenters. Objects are not wrapped when the Presenter class has
|
|
220
|
+
# no custom methods — such wrapping would only add overhead and break
|
|
221
|
+
# class checks (object.is_a?, Hash === object) without changing anything.
|
|
129
222
|
#
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
223
|
+
def enqueue(objects)
|
|
224
|
+
return super unless @wrap_in_presenter
|
|
225
|
+
|
|
226
|
+
presenter = self.class.serializer_class::Presenter
|
|
227
|
+
presenters = objects.map { |object| presenter.new(object, context) }
|
|
228
|
+
super(presenters)
|
|
133
229
|
end
|
|
134
230
|
end
|
|
135
231
|
end
|
|
@@ -244,7 +244,7 @@ class Serega
|
|
|
244
244
|
return opts[:root] if opts.key?(:root)
|
|
245
245
|
|
|
246
246
|
root = self.class.config.root
|
|
247
|
-
(opts.fetch(:many) {
|
|
247
|
+
(opts.fetch(:many) { SeregaUtils::CollectionDetector.call(object) }) ? root.many : root.one
|
|
248
248
|
end
|
|
249
249
|
end
|
|
250
250
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Serega
|
|
4
|
+
module SeregaUtils
|
|
5
|
+
#
|
|
6
|
+
# Utility to check if an object should be serialized as a collection.
|
|
7
|
+
#
|
|
8
|
+
# Hashes and Structs are Enumerable, but enumerate their own member
|
|
9
|
+
# values, so they are treated as single objects.
|
|
10
|
+
#
|
|
11
|
+
class CollectionDetector
|
|
12
|
+
class << self
|
|
13
|
+
#
|
|
14
|
+
# Checks if provided object is a collection of objects
|
|
15
|
+
#
|
|
16
|
+
# @param object [Object] Serialized object
|
|
17
|
+
#
|
|
18
|
+
# @return [Boolean] whether object should be serialized as a collection
|
|
19
|
+
#
|
|
20
|
+
def call(object)
|
|
21
|
+
object.is_a?(Enumerable) && !object.is_a?(Hash) && !object.is_a?(Struct)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -10,25 +10,30 @@ class Serega
|
|
|
10
10
|
# Attribute `block` parameter validator
|
|
11
11
|
#
|
|
12
12
|
class CheckBlock
|
|
13
|
+
# Explains the changed attribute block behavior. Shown when the block
|
|
14
|
+
# looks like an old-style value block — it accepts parameters or
|
|
15
|
+
# defines no attributes.
|
|
16
|
+
ERROR_MESSAGE =
|
|
17
|
+
"Attribute block now defines a nested serializer:" \
|
|
18
|
+
" it is executed in the context of a new serializer class and must define its attributes." \
|
|
19
|
+
" Defining the attribute value with a block is not supported anymore," \
|
|
20
|
+
" use the `value: <callable>` option instead."
|
|
21
|
+
|
|
13
22
|
class << self
|
|
14
23
|
#
|
|
15
24
|
# Checks block parameter provided with attribute.
|
|
16
|
-
# Must have up to two arguments - object and context. Context can be
|
|
17
|
-
# also provided as keyword argument :ctx.
|
|
18
|
-
#
|
|
19
|
-
# @example without arguments
|
|
20
|
-
# attribute(:email) { CONSTANT_EMAIL }
|
|
21
|
-
#
|
|
22
|
-
# @example with one argument
|
|
23
|
-
# attribute(:email) { |obj| obj.confirmed_email }
|
|
24
25
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
26
|
+
# The block defines attributes of a nested anonymous serializer, so
|
|
27
|
+
# it is executed in the context of that serializer and must accept
|
|
28
|
+
# no parameters.
|
|
27
29
|
#
|
|
28
|
-
# @example
|
|
29
|
-
# attribute
|
|
30
|
+
# @example
|
|
31
|
+
# attribute :statistics, method: :itself do
|
|
32
|
+
# attribute :likes_count
|
|
33
|
+
# attribute :comments_count
|
|
34
|
+
# end
|
|
30
35
|
#
|
|
31
|
-
# @param block [Proc] Block that
|
|
36
|
+
# @param block [Proc] Block that defines nested serializer attributes
|
|
32
37
|
#
|
|
33
38
|
# @raise [SeregaError] SeregaError that block has invalid arguments
|
|
34
39
|
#
|
|
@@ -37,48 +42,8 @@ class Serega
|
|
|
37
42
|
def call(block)
|
|
38
43
|
return unless block
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def check_block(block)
|
|
46
|
-
signature = SeregaUtils::MethodSignature.call(block, pos_limit: 2, keyword_args: [:ctx, :batches])
|
|
47
|
-
|
|
48
|
-
raise SeregaError, signature_error unless valid_signature?(signature)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def valid_signature?(signature)
|
|
52
|
-
case signature
|
|
53
|
-
when "0" # no parameters
|
|
54
|
-
true
|
|
55
|
-
when "1" # call(object)
|
|
56
|
-
true
|
|
57
|
-
when "1_ctx" # call(object, ctx:)
|
|
58
|
-
true
|
|
59
|
-
when "1_batches" # call(object, batches:)
|
|
60
|
-
true
|
|
61
|
-
when "1_batches_ctx" # call(object, batches:, ctx:)
|
|
62
|
-
true
|
|
63
|
-
when "2" # call(object, context)
|
|
64
|
-
true
|
|
65
|
-
when "2_batches_ctx" # call(object, context, batches:, ctx:) (proc with no params)
|
|
66
|
-
true
|
|
67
|
-
else
|
|
68
|
-
false
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def signature_error
|
|
73
|
-
<<~ERROR.strip
|
|
74
|
-
Invalid attribute block parameters, valid parameters signatures:
|
|
75
|
-
- () # no parameters
|
|
76
|
-
- (object) # one positional parameter
|
|
77
|
-
- (object, ctx:) # one positional parameter and :ctx keyword
|
|
78
|
-
- (object, batches:) # one positional parameter and :batches keyword
|
|
79
|
-
- (object, ctx:, batches:) # one positional parameter, :ctx, and :batches keywords
|
|
80
|
-
- (object, context) # two positional parameters
|
|
81
|
-
ERROR
|
|
45
|
+
signature = SeregaUtils::MethodSignature.call(block, pos_limit: 0)
|
|
46
|
+
raise SeregaError, ERROR_MESSAGE unless signature == "0"
|
|
82
47
|
end
|
|
83
48
|
end
|
|
84
49
|
end
|