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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -26
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +3 -3
  5. data/lib/serega/attribute_normalizer.rb +87 -14
  6. data/lib/serega/attribute_value_resolvers/batch.rb +4 -0
  7. data/lib/serega/config.rb +48 -1
  8. data/lib/serega/engine/level.rb +57 -0
  9. data/lib/serega/engine/level_queue.rb +54 -0
  10. data/lib/serega/{batch → engine}/loader.rb +1 -1
  11. data/lib/serega/object_serializer.rb +61 -56
  12. data/lib/serega/plan.rb +0 -9
  13. data/lib/serega/plan_point.rb +56 -5
  14. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +21 -1
  15. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +3 -2
  16. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +6 -4
  17. data/lib/serega/plugins/if/if.rb +9 -10
  18. data/lib/serega/plugins/presenter/presenter.rb +101 -5
  19. data/lib/serega/plugins/root/root.rb +1 -1
  20. data/lib/serega/utils/collection_detector.rb +26 -0
  21. data/lib/serega/validations/attribute/check_block.rb +20 -55
  22. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +37 -0
  23. data/lib/serega/validations/attribute/check_opt_batch.rb +6 -6
  24. data/lib/serega/validations/attribute/check_opt_const.rb +3 -4
  25. data/lib/serega/validations/attribute/check_opt_delegate.rb +3 -4
  26. data/lib/serega/validations/attribute/check_opt_many.rb +8 -7
  27. data/lib/serega/validations/attribute/check_opt_method.rb +3 -4
  28. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -1
  29. data/lib/serega/validations/attribute/check_opt_value.rb +3 -4
  30. data/lib/serega/validations/check_attribute_params.rb +8 -7
  31. data/lib/serega.rb +33 -29
  32. metadata +6 -5
  33. data/lib/serega/batch/attribute_loader.rb +0 -82
  34. data/lib/serega/batch/attribute_loaders.rb +0 -117
  35. data/lib/serega/batch/level.rb +0 -48
@@ -2,15 +2,19 @@
2
2
 
3
3
  class Serega
4
4
  #
5
- # Low-level class that is used by more high-level SeregaSerializer
6
- # to construct serialized to hash response
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, :batch_loaders
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
- @batch_loaders = opts[:batch_loaders]
30
+ @level_queue = opts[:level_queue]
27
31
  end
28
32
 
29
- # Serializes object(s)
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
- if array?(object, many)
38
- batch_loaders.add_objects(plan, object.to_a) if plan.batch?
39
- serialize_array(object)
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
- private
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
- def serialize_array(objects)
49
- objects.map { |object| serialize_object(object) }
50
- end
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
- # Patched in:
53
- # - plugin :presenter (makes presenter_object and serializes it)
54
- def serialize_object(object)
55
- plan.points.each_with_object({}) do |point, container|
56
- serialize_point(object, point, container)
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 :if (conditionally skips serializing this point)
64
- def serialize_point(object, point, container)
65
- if point.batch?
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
- def attach_value(object, point, container, batches: nil)
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 = final_value(value, point)
77
- attach_final_value(final_value, point, container)
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 (conditionally skips attaching)
82
- def attach_final_value(final_value, point, container)
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
- def final_value(value, point)
87
- point.child_plan ? relation_value(value, point) : value
88
- end
89
-
90
- def relation_value(value, point)
91
- child_serializer(point).serialize(value)
92
- end
93
-
94
- def child_serializer(point)
95
- point.child_object_serializer.new(
96
- context: context,
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
  #
@@ -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
- # @return [SeregaObjectSerializer] object serializer for child plan
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 { |objects, preloads| Preloader.preload(objects, preloads) }
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 specified)
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
- serializer = opts[:serializer]
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
@@ -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
- def serialize_point(object, point, _container)
274
- return KEY_SKIPPED unless point.satisfy_if_conditions?(object, context)
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
- def attach_final_value(value, point, container)
279
- unless point.satisfy_if_value_conditions?(value, context)
280
- container.delete(point.name) # remove reserved placeholder
281
- return KEY_SKIPPED
282
- end
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
- # class Presenter
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
- # Replaces serialized object with Presenter.new(object, ctx)
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 serialize_object(object)
131
- object = self.class.serializer_class::Presenter.new(object, context)
132
- super
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) { object.is_a?(Enumerable) }) ? root.many : root.one
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
- # @example with two arguments
26
- # attribute(:email) { |obj, context| context['is_current'] ? obj.email : nil }
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 with one argument and keyword context
29
- # attribute(:email) { |obj, ctx:| obj.email if ctx[:show] }
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 returns serialized attribute value
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
- check_block(block)
41
- end
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