serega 0.38.0 → 0.40.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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +198 -28
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +3 -3
  5. data/lib/serega/attribute_normalizer.rb +140 -9
  6. data/lib/serega/attribute_value_resolvers/batch.rb +4 -0
  7. data/lib/serega/attribute_value_resolvers/hash_access.rb +116 -0
  8. data/lib/serega/config.rb +107 -1
  9. data/lib/serega/object_serializer.rb +3 -3
  10. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +2 -0
  11. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +3 -2
  12. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +6 -4
  13. data/lib/serega/plugins/presenter/presenter.rb +99 -2
  14. data/lib/serega/plugins/root/root.rb +1 -1
  15. data/lib/serega/utils/collection_detector.rb +26 -0
  16. data/lib/serega/validations/attribute/check_block.rb +20 -55
  17. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +37 -0
  18. data/lib/serega/validations/attribute/check_opt_batch.rb +6 -6
  19. data/lib/serega/validations/attribute/check_opt_const.rb +3 -4
  20. data/lib/serega/validations/attribute/check_opt_delegate.rb +45 -5
  21. data/lib/serega/validations/attribute/check_opt_hash_access.rb +78 -0
  22. data/lib/serega/validations/attribute/check_opt_many.rb +8 -7
  23. data/lib/serega/validations/attribute/check_opt_method.rb +3 -4
  24. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -1
  25. data/lib/serega/validations/attribute/check_opt_value.rb +3 -4
  26. data/lib/serega/validations/check_attribute_params.rb +9 -7
  27. data/lib/serega.rb +22 -6
  28. metadata +5 -1
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module AttributeValueResolvers
5
+ #
6
+ # Builds value resolver for attributes with the :hash_access option
7
+ #
8
+ class HashAccessResolver
9
+ # Allowed hash access modes
10
+ MODES = %i[symbol string].freeze
11
+
12
+ #
13
+ # Creates resolver that reads a key from Hash records
14
+ #
15
+ # @param name [Symbol, String] hash key
16
+ # @param mode [Symbol] hash access mode (:symbol, :string)
17
+ # @param allow_missing_key [Boolean] whether a missing key is read via `record[key]` rather than raising
18
+ #
19
+ # @return [HashAccessKeyword] resolver instance
20
+ #
21
+ def self.get(name, mode, allow_missing_key)
22
+ HashAccessKeyword.new(name, mode, allow_missing_key)
23
+ end
24
+ end
25
+
26
+ #
27
+ # Builds value resolver for attributes with the :delegate option using
28
+ # hash access on any of its steps
29
+ #
30
+ class HashAccessDelegateResolver
31
+ #
32
+ # Creates resolver that delegates through the provided step readers
33
+ #
34
+ # @param to_step [#call] reader of the intermediate object
35
+ # @param final_step [#call] reader of the final value
36
+ # @param delegate_allow_nil [Boolean] whether a nil intermediate object resolves to nil
37
+ #
38
+ # @return [HashAccessDelegate, HashAccessDelegateAllowNil] resolver instance
39
+ #
40
+ def self.get(to_step, final_step, delegate_allow_nil)
41
+ delegate_allow_nil ? HashAccessDelegateAllowNil.new(to_step, final_step) : HashAccessDelegate.new(to_step, final_step)
42
+ end
43
+ end
44
+
45
+ #
46
+ # Value resolver for attributes with the :hash_access option
47
+ #
48
+ class HashAccessKeyword
49
+ def initialize(name, mode, allow_missing_key)
50
+ @key = (mode == :symbol) ? name.to_sym : name.to_s
51
+ @allow_missing_key = allow_missing_key
52
+ end
53
+
54
+ #
55
+ # Reads the key from the record
56
+ #
57
+ # @param object [Object] serialized object or delegation step value
58
+ # @return [Object] the value found
59
+ #
60
+ def call(object)
61
+ return object[@key] if @allow_missing_key
62
+
63
+ object.fetch(@key) do
64
+ default = object[@key]
65
+ next default unless default.nil?
66
+
67
+ raise KeyError.new("key not found: #{@key.inspect}", key: @key, receiver: object)
68
+ end
69
+ end
70
+ end
71
+
72
+ #
73
+ # Value resolver for attributes with :hash_access and :delegate (without :allow_nil) options
74
+ #
75
+ class HashAccessDelegate
76
+ def initialize(to_step, final_step)
77
+ @to_step = to_step
78
+ @final_step = final_step
79
+ end
80
+
81
+ #
82
+ # Delegates the value reading through the intermediate object
83
+ #
84
+ # @param object [Object] serialized object
85
+ # @return [Object] the value found
86
+ #
87
+ def call(object)
88
+ @final_step.call(@to_step.call(object))
89
+ end
90
+ end
91
+
92
+ #
93
+ # Value resolver for attributes with :hash_access and :delegate (with :allow_nil) options
94
+ #
95
+ class HashAccessDelegateAllowNil
96
+ def initialize(to_step, final_step)
97
+ @to_step = to_step
98
+ @final_step = final_step
99
+ end
100
+
101
+ #
102
+ # Delegates the value reading through the intermediate object,
103
+ # resolving a nil intermediate to nil
104
+ #
105
+ # @param object [Object] serialized object
106
+ # @return [Object, nil] the value found
107
+ #
108
+ def call(object)
109
+ intermediate = @to_step.call(object)
110
+ return if intermediate.nil?
111
+
112
+ @final_step.call(intermediate)
113
+ end
114
+ end
115
+ end
116
+ end
data/lib/serega/config.rb CHANGED
@@ -24,6 +24,8 @@ class Serega
24
24
  default
25
25
  preload
26
26
  batch
27
+ base_serializer
28
+ hash_access
27
29
  ].freeze,
28
30
  serialize_keys: %i[context many].freeze,
29
31
  check_attribute_name: true,
@@ -31,8 +33,11 @@ class Serega
31
33
  delegate_default_allow_nil: false,
32
34
  max_cached_plans_per_serializer_count: 0,
33
35
  auto_preload: {has_delegate_option: false, has_serializer_option: false},
36
+ auto_preload_excluded_methods: %i[itself].freeze,
34
37
  hide_by_default: false,
35
- batch_id_option: :id
38
+ batch_id_option: :id,
39
+ base_serializer: nil,
40
+ hash_access: {default_mode: :symbol, default_allow_missing_key: false}
36
41
  }.freeze
37
42
  # :nocov:
38
43
 
@@ -114,6 +119,50 @@ class Serega
114
119
  opts[:delegate_default_allow_nil] = value
115
120
  end
116
121
 
122
+ # Returns :auto_preload_excluded_methods config option — methods that are
123
+ # never auto-preloaded, as they return the serialized object itself and
124
+ # not an association
125
+ # @return [Array<Symbol>] Current :auto_preload_excluded_methods config option
126
+ def auto_preload_excluded_methods
127
+ opts.fetch(:auto_preload_excluded_methods)
128
+ end
129
+
130
+ # Sets :auto_preload_excluded_methods config option — methods that are
131
+ # never auto-preloaded. Applies to the `:method` option of attributes
132
+ # with a serializer and to the `delegate: {to: ...}` option.
133
+ #
134
+ # @param value [Array<Symbol>] Method names to skip in auto-preload
135
+ #
136
+ # @return [Array<Symbol>] New :auto_preload_excluded_methods config option
137
+ def auto_preload_excluded_methods=(value)
138
+ unless value.is_a?(Array) && value.all?(Symbol)
139
+ raise SeregaError, "Must be an Array of Symbols, #{value.inspect} provided"
140
+ end
141
+
142
+ opts[:auto_preload_excluded_methods] = value
143
+ end
144
+
145
+ # Returns :base_serializer config option — the parent class for nested
146
+ # serializers defined with attribute blocks
147
+ # @return [Class, nil] Current :base_serializer config option
148
+ def base_serializer
149
+ opts.fetch(:base_serializer)
150
+ end
151
+
152
+ # Sets :base_serializer config option — the parent class for nested
153
+ # serializers defined with attribute blocks. Usually a settings-only
154
+ # serializer, e.g. `config.base_serializer = self` in an application
155
+ # base serializer class.
156
+ #
157
+ # @param value [Class] Serega or its subclass
158
+ #
159
+ # @return [Class] New :base_serializer config option
160
+ def base_serializer=(value)
161
+ raise SeregaError, "Must be a Serega subclass, #{value.inspect} provided" if !value.is_a?(Class) || !(value <= Serega)
162
+
163
+ opts[:base_serializer] = value
164
+ end
165
+
117
166
  # Returns :hide_by_default config option
118
167
  # @return [Boolean, Symbol] Current :hide_by_default config option
119
168
  def hide_by_default
@@ -158,6 +207,12 @@ class Serega
158
207
  end
159
208
  end
160
209
 
210
+ # Returns the hash_access config object
211
+ # @return [Serega::SeregaConfig::HashAccessConfig] hash_access config object
212
+ def hash_access
213
+ @hash_access ||= HashAccessConfig.new(opts.fetch(:hash_access))
214
+ end
215
+
161
216
  # Returns :max_cached_plans_per_serializer_count config option
162
217
  # @return [Boolean] Current :max_cached_plans_per_serializer_count config option
163
218
  def max_cached_plans_per_serializer_count
@@ -205,6 +260,57 @@ class Serega
205
260
  end
206
261
  end
207
262
 
263
+ #
264
+ # Config for the `hash_access:` attribute option
265
+ #
266
+ class HashAccessConfig
267
+ # @return [Hash] hash_access config options
268
+ attr_reader :opts
269
+
270
+ #
271
+ # Initializes HashAccessConfig object
272
+ #
273
+ # @param opts [Hash] hash_access config options
274
+ #
275
+ # @return [Serega::SeregaConfig::HashAccessConfig]
276
+ #
277
+ def initialize(opts)
278
+ @opts = opts
279
+ end
280
+
281
+ # @return [Symbol] mode used by `hash_access: true` (default :symbol)
282
+ def default_mode
283
+ opts.fetch(:default_mode)
284
+ end
285
+
286
+ # Sets the mode used by `hash_access: true`
287
+ # @param value [Symbol] one of :symbol, :string
288
+ # @return [Symbol] new default mode
289
+ def default_mode=(value)
290
+ unless AttributeValueResolvers::HashAccessResolver::MODES.include?(value)
291
+ raise SeregaError, "Invalid hash_access default_mode #{value.inspect}. Allowed modes: :symbol, :string"
292
+ end
293
+
294
+ opts[:default_mode] = value
295
+ end
296
+
297
+ # @return [Boolean] allow_missing_key used when an attribute omits it
298
+ def default_allow_missing_key
299
+ opts.fetch(:default_allow_missing_key)
300
+ end
301
+
302
+ # Sets the allow_missing_key used when an attribute omits it
303
+ # @param value [Boolean]
304
+ # @return [Boolean] new default allow_missing_key
305
+ def default_allow_missing_key=(value)
306
+ unless value == true || value == false
307
+ raise SeregaError, "Invalid hash_access default_allow_missing_key #{value.inspect}. Must be a Boolean"
308
+ end
309
+
310
+ opts[:default_allow_missing_key] = value
311
+ end
312
+ end
313
+
208
314
  include SeregaConfigInstanceMethods
209
315
  extend Serega::SeregaHelpers::SerializerClassHelper
210
316
  end
@@ -97,14 +97,14 @@ class Serega
97
97
  end
98
98
 
99
99
  # How to serialize `object`, deciding whether the result is a collection or a
100
- # single object and reading `Enumerable` only once:
100
+ # single object and checking the object type only once:
101
101
  # - :many — `many` is on and the object is a collection
102
102
  # - :many_for_one — `many` is on but a sole object was given (wrap it, don't raise)
103
103
  # - :one — serialize the object on its own
104
104
  def serialize_mode(object)
105
105
  case many
106
- when NilClass then object.is_a?(Enumerable) ? :many : :one
107
- when TrueClass then object.is_a?(Enumerable) ? :many : :many_for_one
106
+ when NilClass then SeregaUtils::CollectionDetector.call(object) ? :many : :one
107
+ when TrueClass then SeregaUtils::CollectionDetector.call(object) ? :many : :many_for_one
108
108
  else :one # many == false
109
109
  end
110
110
  end
@@ -91,6 +91,7 @@ class Serega
91
91
  # The underlying records to preload onto. The :presenter plugin wraps every
92
92
  # serialized object in a SimpleDelegator, but ActiveRecord's Preloader needs
93
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.
94
95
  #
95
96
  # @param serializer_class [Class<Serega>] Current serializer class
96
97
  # @param objects [Array] objects serialized at the current level
@@ -99,6 +100,7 @@ class Serega
99
100
  #
100
101
  def self.records(serializer_class, objects)
101
102
  return objects unless serializer_class.plugin_used?(:presenter)
103
+ return objects unless serializer_class.custom_presenter?
102
104
 
103
105
  objects.map(&:__getobj__)
104
106
  end
@@ -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
@@ -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,48 @@ 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
+ # Marks the class as modified, then includes the module
136
+ # @return [void]
137
+ def include(*modules)
138
+ @modified = true
139
+ super
140
+ end
141
+
142
+ # Marks the class as modified, then prepends the module
143
+ # @return [void]
144
+ def prepend(*modules)
145
+ @modified = true
146
+ super
147
+ end
148
+
149
+ private
150
+
151
+ def method_added(name)
152
+ @modified = true
153
+ super
154
+ end
155
+ end
100
156
  end
101
157
 
102
158
  #
@@ -105,6 +161,36 @@ class Serega
105
161
  # @see Serega
106
162
  #
107
163
  module ClassMethods
164
+ #
165
+ # Defines presenter methods — evaluates the block inside the
166
+ # serializer's own Presenter class. Multiple blocks accumulate.
167
+ #
168
+ # presenter do
169
+ # def name
170
+ # [first_name, last_name].compact.join(" ")
171
+ # end
172
+ # end
173
+ #
174
+ # @return [void]
175
+ #
176
+ def presenter(&block)
177
+ raise SeregaError, "Provide a block with presenter methods: `presenter do ... end`" unless block
178
+
179
+ self::Presenter.class_exec(&block)
180
+ nil
181
+ end
182
+
183
+ #
184
+ # Checks if the serializer's Presenter class (or an inherited one) was
185
+ # extended with custom user code. When it was not, serialized objects
186
+ # are not wrapped in the Presenter at all.
187
+ #
188
+ # @return [Boolean] whether custom presenter methods were defined
189
+ #
190
+ def custom_presenter?
191
+ self::Presenter.modified?
192
+ end
193
+
108
194
  private
109
195
 
110
196
  def inherited(subclass)
@@ -122,14 +208,25 @@ class Serega
122
208
  # @see Serega::SeregaObjectSerializer
123
209
  #
124
210
  module SeregaObjectSerializerInstanceMethods
211
+ # The custom-presenter check is made once per object serializer here
212
+ # and its result is reused for every enqueued chunk of the level.
213
+ def initialize(**opts)
214
+ super
215
+ @wrap_in_presenter = self.class.serializer_class.custom_presenter?
216
+ end
217
+
125
218
  private
126
219
 
127
220
  #
128
221
  # Wraps each serialized object in Presenter.new(object, ctx) before it is
129
222
  # enqueued, so the whole level — value resolution and batch loaders alike —
130
- # sees presenters.
223
+ # sees presenters. Objects are not wrapped when the Presenter class has
224
+ # no custom methods — such wrapping would only add overhead and break
225
+ # class checks (object.is_a?, Hash === object) without changing anything.
131
226
  #
132
227
  def enqueue(objects)
228
+ return super unless @wrap_in_presenter
229
+
133
230
  presenter = self.class.serializer_class::Presenter
134
231
  presenters = objects.map { |object| presenter.new(object, context) }
135
232
  super(presenters)
@@ -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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ module SeregaValidations
5
+ module Attribute
6
+ #
7
+ # Attribute `:base_serializer` option validator
8
+ #
9
+ class CheckOptBaseSerializer
10
+ class << self
11
+ #
12
+ # Checks attribute :base_serializer option. It specifies the parent
13
+ # class for the nested serializer defined with the attribute block,
14
+ # so it makes sense only when a block is provided.
15
+ #
16
+ # @param opts [Hash] Attribute options
17
+ # @param block [nil, Proc] Attribute block (defines a nested serializer)
18
+ #
19
+ # @raise [SeregaError] SeregaError that option has invalid value
20
+ #
21
+ # @return [void]
22
+ #
23
+ def call(opts, block = nil)
24
+ return unless opts.key?(:base_serializer)
25
+
26
+ raise SeregaError, "Option :base_serializer can be used only with a block" unless block
27
+
28
+ value = opts[:base_serializer]
29
+ return if value.is_a?(Class) && (value <= Serega)
30
+
31
+ raise SeregaError, "Invalid option :base_serializer => #{value.inspect}. Must be a Serega subclass"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end