serega 0.42.0 → 0.50.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +124 -1207
  3. data/VERSION +1 -1
  4. data/lib/serega/attribute.rb +186 -131
  5. data/lib/serega/attribute_normalizer.rb +164 -345
  6. data/lib/serega/attribute_normalizers/batch.rb +45 -0
  7. data/lib/serega/attribute_normalizers/block.rb +81 -0
  8. data/lib/serega/attribute_normalizers/delegate.rb +34 -0
  9. data/lib/serega/attribute_normalizers/hash_access.rb +37 -0
  10. data/lib/serega/attribute_normalizers/preloads.rb +46 -0
  11. data/lib/serega/attribute_value_resolvers/batch.rb +5 -54
  12. data/lib/serega/attribute_value_resolvers/const.rb +2 -24
  13. data/lib/serega/attribute_value_resolvers/delegate.rb +17 -28
  14. data/lib/serega/attribute_value_resolvers/hash_access.rb +34 -52
  15. data/lib/serega/attribute_value_resolvers/keyword.rb +10 -25
  16. data/lib/serega/attribute_values.rb +46 -0
  17. data/lib/serega/batch_loader.rb +37 -0
  18. data/lib/serega/conditions.rb +56 -0
  19. data/lib/serega/config.rb +105 -144
  20. data/lib/serega/engine/pulls.rb +64 -0
  21. data/lib/serega/engine/run.rb +106 -0
  22. data/lib/serega/errors.rb +36 -8
  23. data/lib/serega/formatter.rb +98 -0
  24. data/lib/serega/plan/cache.rb +96 -0
  25. data/lib/serega/plan/per_ractor_cache.rb +23 -0
  26. data/lib/serega/plan.rb +101 -128
  27. data/lib/serega/plan_point.rb +44 -151
  28. data/lib/serega/plugins/activerecord_preloads/activerecord_preloads.rb +13 -80
  29. data/lib/serega/plugins/activerecord_preloads/lib/preloader.rb +8 -75
  30. data/lib/serega/plugins/context_metadata/context_metadata.rb +40 -123
  31. data/lib/serega/plugins/metadata/check_params.rb +80 -0
  32. data/lib/serega/plugins/metadata/meta_attribute.rb +40 -79
  33. data/lib/serega/plugins/metadata/metadata.rb +40 -168
  34. data/lib/serega/plugins/root/root.rb +91 -235
  35. data/lib/serega/plugins.rb +16 -49
  36. data/lib/serega/presenter.rb +61 -19
  37. data/lib/serega/result_builder.rb +68 -0
  38. data/lib/serega/result_code.rb +203 -0
  39. data/lib/serega/shareable.rb +52 -0
  40. data/lib/serega/source_group.rb +187 -0
  41. data/lib/serega/utils/collection_detector.rb +19 -13
  42. data/lib/serega/utils/deep_merge.rb +26 -0
  43. data/lib/serega/utils/enum_deep_dup.rb +10 -16
  44. data/lib/serega/utils/enum_deep_freeze.rb +8 -14
  45. data/lib/serega/utils/method_signature.rb +6 -15
  46. data/lib/serega/utils/parse_string_modifiers.rb +63 -0
  47. data/lib/serega/utils/ractor_local.rb +36 -0
  48. data/lib/serega/utils/serialized_attribute_error.rb +60 -20
  49. data/lib/serega/utils/to_hash.rb +5 -26
  50. data/lib/serega/validations/attribute/check_block.rb +3 -15
  51. data/lib/serega/validations/attribute/check_name.rb +4 -15
  52. data/lib/serega/validations/attribute/check_opt_base_serializer.rb +3 -15
  53. data/lib/serega/validations/attribute/check_opt_batch.rb +26 -56
  54. data/lib/serega/validations/attribute/check_opt_condition.rb +55 -0
  55. data/lib/serega/validations/attribute/check_opt_delegate.rb +10 -63
  56. data/lib/serega/validations/attribute/check_opt_format.rb +36 -0
  57. data/lib/serega/validations/attribute/check_opt_hash_access.rb +11 -59
  58. data/lib/serega/validations/attribute/check_opt_hide.rb +3 -14
  59. data/lib/serega/validations/attribute/check_opt_many.rb +18 -20
  60. data/lib/serega/validations/attribute/check_opt_method.rb +3 -25
  61. data/lib/serega/validations/attribute/check_opt_preload.rb +3 -26
  62. data/lib/serega/validations/attribute/check_opt_serializer.rb +4 -18
  63. data/lib/serega/validations/attribute/check_opt_value.rb +3 -21
  64. data/lib/serega/validations/check_attribute_params.rb +92 -71
  65. data/lib/serega/validations/check_batch_loader_params.rb +43 -62
  66. data/lib/serega/validations/check_formatter.rb +80 -0
  67. data/lib/serega/validations/check_serialize_keywords.rb +23 -0
  68. data/lib/serega/validations/initiate/check_modifiers.rb +14 -20
  69. data/lib/serega/validations/utils/check_allowed_keys.rb +3 -13
  70. data/lib/serega/validations/utils/check_hash_access_value.rb +44 -0
  71. data/lib/serega/validations/utils/check_opt_is_bool.rb +3 -13
  72. data/lib/serega/validations/utils/check_opt_is_hash.rb +3 -13
  73. data/lib/serega/validations/utils/check_opt_is_string_or_symbol.rb +3 -13
  74. data/lib/serega/version.rb +1 -4
  75. data/lib/serega.rb +459 -388
  76. data/sig/serega/plugins.rbs +49 -0
  77. data/sig/serega.rbs +142 -0
  78. metadata +42 -38
  79. data/lib/serega/data_builder.rb +0 -54
  80. data/lib/serega/engine/level.rb +0 -59
  81. data/lib/serega/engine/level_queue.rb +0 -56
  82. data/lib/serega/engine/loader.rb +0 -70
  83. data/lib/serega/helpers/serializer_class_helper.rb +0 -19
  84. data/lib/serega/object_serializer.rb +0 -125
  85. data/lib/serega/plugins/camel_case/camel_case.rb +0 -213
  86. data/lib/serega/plugins/depth_limit/depth_limit.rb +0 -206
  87. data/lib/serega/plugins/explicit_many_option/explicit_many_option.rb +0 -69
  88. data/lib/serega/plugins/explicit_many_option/validations/check_opt_many.rb +0 -38
  89. data/lib/serega/plugins/formatters/formatters.rb +0 -322
  90. data/lib/serega/plugins/if/if.rb +0 -287
  91. data/lib/serega/plugins/if/validations/check_opt_if.rb +0 -72
  92. data/lib/serega/plugins/if/validations/check_opt_if_value.rb +0 -77
  93. data/lib/serega/plugins/if/validations/check_opt_unless.rb +0 -72
  94. data/lib/serega/plugins/if/validations/check_opt_unless_value.rb +0 -77
  95. data/lib/serega/plugins/metadata/validations/check_block.rb +0 -47
  96. data/lib/serega/plugins/metadata/validations/check_opt_const.rb +0 -39
  97. data/lib/serega/plugins/metadata/validations/check_opt_hide_empty.rb +0 -35
  98. data/lib/serega/plugins/metadata/validations/check_opt_hide_nil.rb +0 -35
  99. data/lib/serega/plugins/metadata/validations/check_opt_value.rb +0 -61
  100. data/lib/serega/plugins/metadata/validations/check_opts.rb +0 -59
  101. data/lib/serega/plugins/metadata/validations/check_path.rb +0 -50
  102. data/lib/serega/plugins/string_modifiers/parse_string_modifiers.rb +0 -111
  103. data/lib/serega/plugins/string_modifiers/string_modifiers.rb +0 -63
  104. data/lib/serega/utils/symbol_name.rb +0 -39
  105. data/lib/serega/validations/attribute/check_opt_const.rb +0 -40
  106. data/lib/serega/validations/check_initiate_params.rb +0 -57
  107. data/lib/serega/validations/check_serialize_params.rb +0 -54
@@ -4,398 +4,217 @@ class Serega
4
4
  #
5
5
  # Prepares provided attribute options
6
6
  #
7
- # @private
8
- class SeregaAttributeNormalizer
9
- #
10
- # AttributeNormalizer instance methods
11
- #
12
- # @private
13
- module AttributeNormalizerInstanceMethods
14
- # Attribute initial params
15
- # @return [Hash] Attribute initial params
16
- attr_reader :init_name, :init_opts, :init_block
17
-
18
- #
19
- # Instantiates attribute options normalizer
20
- #
21
- # @param initials [Hash] new attribute options
22
- #
23
- # @return [SeregaAttributeNormalizer] Instantiated attribute options normalizer
24
- #
25
- def initialize(initials)
26
- @init_name = initials[:name]
27
- @init_opts = initials[:opts]
28
- @init_block = initials[:block]
29
- end
30
-
31
- #
32
- # Symbolized initial attribute name
33
- #
34
- # @return [Symbol] Attribute normalized name
35
- #
36
- def name
37
- @name ||= prepare_name
38
- end
39
-
40
- #
41
- # Symbolized initial attribute method name
42
- #
43
- # @return [Symbol] Attribute normalized method name
44
- #
45
- def method_name
46
- @method_name ||= prepare_method_name
47
- end
48
-
49
- #
50
- # Combines all options to return single block that will be used to find
51
- # attribute value during serialization
52
- #
53
- # @return [#call] Attribute normalized callable value block
54
- #
55
- def value_block
56
- @value_block ||= prepare_value_block
57
- end
58
-
59
- #
60
- # Detects value block parameters signature
61
- #
62
- # @return [String] value block parameters signature
63
- #
64
- def value_block_signature
65
- @value_block_signature ||= SeregaUtils::MethodSignature.call(value_block, pos_limit: 2, keyword_args: [:ctx])
66
- end
67
-
68
- #
69
- # Shows if attribute is specified to be hidden
70
- #
71
- # @return [Boolean, nil] if attribute must be hidden by default
72
- #
73
- def hide
74
- return @hide if instance_variable_defined?(:@hide)
75
-
76
- @hide = prepare_hide
77
- end
78
-
79
- #
80
- # Shows if attribute is specified to be a one-to-many relationship
81
- #
82
- # @return [Boolean, nil] if attribute is specified to be a one-to-many relationship
83
- #
84
- def many
85
- return @many if instance_variable_defined?(:@many)
7
+ class SeregaAttributeNormalizer # :nodoc:
8
+ # Attribute initial params
9
+ attr_reader :init_name, :init_opts, :init_block
10
+
11
+ # Instantiates attribute options normalizer
12
+ def initialize(serializer_class, initials)
13
+ @serializer_class = serializer_class
14
+ @init_name = initials[:name]
15
+ @init_opts = initials[:opts]
16
+ @init_block = initials[:block]
17
+ @method_name = nil
18
+ end
86
19
 
87
- @many = prepare_many
88
- end
20
+ # Symbolized initial attribute name
21
+ def name
22
+ @name ||= prepare_name
23
+ end
89
24
 
90
- #
91
- # Shows specified attribute serializer
92
- # @return [Serega, String, #callable, nil] specified serializer
93
- #
94
- def serializer
95
- return @serializer if instance_variable_defined?(:@serializer)
25
+ # Symbolized initial attribute method name
26
+ def method_name
27
+ @method_name ||= prepare_method_name
28
+ end
96
29
 
97
- @serializer = prepare_serializer
98
- end
30
+ # Combines all options to return single block that will be used to find
31
+ # attribute value during serialization
32
+ def value_block
33
+ @value_block ||= prepare_value_block
34
+ end
99
35
 
100
- #
101
- # Shows the default attribute value. It is a value that replaces found nils.
102
- #
103
- # When custom :default is not specified, we set empty array as default when `many: true` specified
104
- #
105
- # @return [Object] Attribute default value
106
- #
107
- def default
108
- return @default if instance_variable_defined?(:@default)
109
-
110
- @default = prepare_default
111
- end
36
+ # Detects value block parameters signature
37
+ def value_block_signature
38
+ @value_block_signature ||= SeregaUtils::MethodSignature.call(value_block, pos_limit: 2, keyword_args: [:ctx])
39
+ end
112
40
 
113
- #
114
- # Preloads declared for this attribute, passed through as provided.
115
- #
116
- # @return [Object, nil] declared preloads, or nil when the attribute has none
117
- #
118
- def preloads
119
- return @preloads if instance_variable_defined?(:@preloads)
41
+ # Shows if attribute is specified to be hidden
42
+ def hide
43
+ return @hide if instance_variable_defined?(:@hide)
120
44
 
121
- @preloads = prepare_preloads
122
- end
45
+ @hide = prepare_hide
46
+ end
123
47
 
124
- # Shows specified batch loaders names
125
- # @return [Array<Symbol>] specified serializer
126
- #
127
- def batch_loaders
128
- @batch_loaders ||= prepare_batch_loaders
129
- end
48
+ # Shows if attribute is specified to be a one-to-many relationship
49
+ def many
50
+ return @many if instance_variable_defined?(:@many)
130
51
 
131
- private
52
+ @many = prepare_many
53
+ end
132
54
 
133
- def prepare_name
134
- init_name.to_sym
135
- end
55
+ # Shows specified attribute serializer
56
+ def serializer
57
+ return @serializer if instance_variable_defined?(:@serializer)
136
58
 
137
- def prepare_value_block
138
- init_opts[:value] \
139
- || prepare_const_block \
140
- || prepare_delegate_block \
141
- || prepare_batch_loader_block \
142
- || prepare_keyword_block
143
- end
59
+ @serializer = prepare_serializer
60
+ end
144
61
 
145
- def prepare_hide
146
- # Return provided directly value
147
- hide = init_opts[:hide]
148
- return hide if (hide == true) || (hide == false)
62
+ # Shows the default attribute value. It is a value that replaces found nils.
63
+ #
64
+ # When custom :default is not specified, we set empty array as default when `many: true` specified
65
+ def default
66
+ return @default if instance_variable_defined?(:@default)
149
67
 
150
- hide_setting = config.hide_by_default
151
- return true if hide_setting == true
152
- return true if hide_setting == :auto && preload_or_batch?
68
+ @default = prepare_default
69
+ end
153
70
 
154
- # Return nil for undefined value which means "not hide" but allows
155
- # to change this value by plugins
156
- nil
157
- end
71
+ # Preloads declared for this attribute, passed through as provided.
72
+ def preloads
73
+ return @preloads if instance_variable_defined?(:@preloads)
158
74
 
159
- # The `hide_by_default = :auto` criterion: hide attributes that fetch extra
160
- # data on demand — those declared with `:preload` or an explicit `:batch`.
161
- #
162
- # This is intentionally narrower than a "goes through the batch mechanism"
163
- # check (`SeregaPlanPoint#batch?`): a plain relation (`serializer:` without
164
- # `:preload`) is batch-processed too, but it only serializes an already-loaded
165
- # nested object, so it stays visible by default.
166
- def preload_or_batch?
167
- !!(preloads || init_opts.key?(:batch))
168
- end
75
+ @preloads = prepare_preloads
76
+ end
169
77
 
170
- def config
171
- self.class.serializer_class.config
172
- end
78
+ # Shows specified batch loaders names
79
+ def batch_loaders
80
+ @batch_loaders ||= prepare_batch_loaders
81
+ end
173
82
 
174
- def prepare_many
175
- init_opts[:many]
176
- end
83
+ # Batch loader given directly as the :batch option. It is named after the
84
+ # attribute and belongs to the attribute only.
85
+ def inline_batch_loader
86
+ batch = batch_option
87
+ return unless batch&.inline?
177
88
 
178
- def prepare_serializer
179
- block = init_block
180
- return init_opts[:serializer] unless block
89
+ SeregaBatchLoader.new(name: name, block: batch.use)
90
+ end
181
91
 
182
- prepare_block_serializer(block)
183
- end
92
+ # Callable :if, :unless, :if_value and :unless_value conditions. A Symbol
93
+ # condition calls the method of this name.
94
+ def conditions
95
+ conditions = init_opts.slice(:if, :unless, :if_value, :unless_value)
96
+ return if conditions.empty?
184
97
 
185
- # Builds an anonymous nested serializer from the attribute block.
186
- #
187
- # The serializer is a regular subclass of an explicitly chosen base —
188
- # the :base_serializer attribute option or `config.base_serializer` —
189
- # so it inherits everything the base has: plugins, config, attributes,
190
- # batch loaders, the preload handler. Nested values can be any objects,
191
- # which is why the base is chosen explicitly instead of inheriting from
192
- # the current serializer. Its `inspect` is overridden to
193
- # `CurrentSerializer.<attribute_name>` so error messages and debugging
194
- # output point back to the defining attribute.
195
- # Guards against cyclic definitions before building: inheriting the base
196
- # serializer copies its attributes, and copying a block attribute builds
197
- # its nested serializer — meeting the same block again while it is being
198
- # built means the base serializer (transitively) contains the block
199
- # attribute being built, so inheriting from it would recurse forever.
200
- # The "in progress" mark is kept on the block itself — its identity is
201
- # what defines the cycle (the same Proc object is shared by all copies
202
- # of the attribute).
203
- def prepare_block_serializer(block)
204
- if block.instance_variable_defined?(:@serega_building_nested_serializer)
205
- raise SeregaError,
206
- "Can not define a nested serializer for attribute :#{name} —" \
207
- " its base serializer #{block_base_serializer.inspect} (transitively)" \
208
- " contains this same block attribute (cyclic definition)"
209
- end
210
-
211
- block.instance_variable_set(:@serega_building_nested_serializer, true)
212
- begin
213
- build_block_serializer(block)
214
- ensure
215
- block.remove_instance_variable(:@serega_building_nested_serializer)
216
- end
98
+ conditions.transform_values do |condition|
99
+ condition.is_a?(Symbol) ? AttributeValueResolvers::Keyword.new(condition) : condition
217
100
  end
101
+ end
218
102
 
219
- def build_block_serializer(block)
220
- label = "#{self.class.serializer_class.inspect}.<#{name}>"
103
+ # Formatter of the :format option. A Symbol option is the name of a
104
+ # formatter defined with `Serega.formatter`.
105
+ def formatter
106
+ format = init_opts[:format]
107
+ return unless format
221
108
 
222
- serializer = Class.new(block_base_serializer) do
223
- define_singleton_method(:inspect) { label }
224
- define_singleton_method(:to_s) { label }
225
- instance_exec(&block)
226
- end
109
+ format.is_a?(Symbol) ? @serializer_class.formatters.fetch(format) : SeregaFormatter.new(format)
110
+ end
227
111
 
228
- # An empty nested serializer means the block was intended as an
229
- # old-style value block — raise the explaining error.
230
- raise SeregaError, SeregaValidations::Attribute::CheckBlock::ERROR_MESSAGE if serializer.attributes.empty?
112
+ private
231
113
 
232
- serializer
233
- end
114
+ def prepare_name
115
+ init_name.to_sym
116
+ end
234
117
 
235
- # Base class for the nested serializer. Must be chosen explicitly —
236
- # usually a settings-only serializer holding plugins and configuration
237
- # (e.g. `config.base_serializer = self` in an application base class).
238
- def block_base_serializer
239
- base_serializer = init_opts[:base_serializer] || config.base_serializer
240
- return base_serializer if base_serializer
241
-
242
- raise SeregaError,
243
- "Attribute block requires a base serializer for the nested serializer." \
244
- " Provide the `base_serializer: <SerializerClass>` attribute option" \
245
- " or set `config.base_serializer = <SerializerClass>`"
246
- end
118
+ def prepare_value_block
119
+ init_opts[:value] \
120
+ || prepare_const_block \
121
+ || prepare_delegate_block \
122
+ || prepare_batch_loader_block \
123
+ || prepare_keyword_block
124
+ end
247
125
 
248
- def prepare_method_name
249
- (init_opts[:method] || init_name).to_sym
250
- end
126
+ def prepare_hide
127
+ # Return provided directly value
128
+ hide = init_opts[:hide]
129
+ return hide if (hide == true) || (hide == false)
251
130
 
252
- def prepare_const_block
253
- return unless init_opts.key?(:const)
131
+ hide_setting = config.hide_by_default
132
+ return true if hide_setting == true
133
+ return true if hide_setting == :auto && preload_or_batch?
254
134
 
255
- AttributeValueResolvers::ConstResolver.get(init_opts[:const])
256
- end
257
-
258
- def prepare_keyword_block
259
- mode, allow_nil = hash_access
260
- return AttributeValueResolvers::KeywordResolver.get(method_name) unless mode
135
+ # Return nil for undefined value which means "not hide" but allows
136
+ # to change this value by plugins
137
+ nil
138
+ end
261
139
 
262
- AttributeValueResolvers::HashAccessResolver.get(method_name, mode, allow_nil)
263
- end
140
+ # The `hide_by_default = :auto` criterion: hide attributes that fetch extra
141
+ # data on demand — those declared with `:preload` or an explicit `:batch`.
142
+ #
143
+ # This is intentionally narrower than a "goes through the batch mechanism"
144
+ # check (`SeregaPlanPoint#batch?`): a plain relation (`serializer:` without
145
+ # `:preload`) is batch-processed too, but it only serializes an already-loaded
146
+ # nested object, so it stays visible by default.
147
+ def preload_or_batch?
148
+ !!(preloads || init_opts.key?(:batch))
149
+ end
264
150
 
265
- def prepare_batch_loader_block
266
- batch_opt = init_opts[:batch]
267
- return unless batch_opt
151
+ def config
152
+ @serializer_class.config
153
+ end
268
154
 
269
- AttributeValueResolvers::BatchResolver.get(self.class.serializer_class, name, batch_opt)
270
- end
155
+ def prepare_many
156
+ init_opts[:many]
157
+ end
271
158
 
272
- # Batch loader names whose loaded data this attribute's value needs. Only
273
- # explicit `:batch` attributes have any — everything else resolves its value
274
- # from the record itself, so the list is empty (no loader to run).
275
- def prepare_batch_loaders
276
- batch_opt = init_opts[:batch]
277
- return explicit_batch_loaders(batch_opt) if batch_opt
159
+ def prepare_serializer
160
+ block = init_block
161
+ return init_opts[:serializer] unless block
278
162
 
279
- FROZEN_EMPTY_ARRAY
280
- end
163
+ AttributeNormalizers::Block.call(@serializer_class, name, block, init_opts[:base_serializer])
164
+ end
281
165
 
282
- def explicit_batch_loaders(batch_opt)
283
- if batch_opt == true || batch_opt.respond_to?(:call)
284
- [name]
285
- elsif batch_opt.is_a?(Symbol)
286
- [batch_opt]
287
- elsif batch_opt.is_a?(String)
288
- [batch_opt.to_sym]
289
- else
290
- use_opt = batch_opt.fetch(:use)
291
- use_opt.respond_to?(:call) ? [name] : Array(use_opt).map(&:to_sym)
292
- end
293
- end
166
+ def prepare_method_name
167
+ (init_opts[:method] || init_name).to_sym
168
+ end
294
169
 
295
- def prepare_default
296
- init_opts.fetch(:default) { many ? FROZEN_EMPTY_ARRAY : nil }
297
- end
170
+ def prepare_const_block
171
+ return unless init_opts.key?(:const)
298
172
 
299
- # `delegate: {to_hash_access: ...}` configures hash access for the
300
- # intermediate (:to) read, `delegate: {hash_access: ...}` for the
301
- # final (:method) read. A step without its sub-option keeps a plain
302
- # method read.
303
- def prepare_delegate_block
304
- delegate = init_opts[:delegate]
305
- return unless delegate
306
-
307
- key_method_name = delegate[:method] || method_name
308
- delegate_to = delegate[:to]
309
- delegate_allow_nil = delegate.fetch(:allow_nil) { config.delegate_default_allow_nil }
310
-
311
- to_access = delegate[:to_hash_access]
312
- final_access = delegate[:hash_access]
313
- unless to_access || final_access
314
- return AttributeValueResolvers::DelegateResolver.get(delegate_to, key_method_name, delegate_allow_nil)
315
- end
316
-
317
- to_step = delegate_to_step(delegate_to, to_access)
318
- final_step = delegate_final_step(key_method_name, final_access)
319
- AttributeValueResolvers::HashAccessDelegateResolver.get(to_step, final_step, delegate_allow_nil)
320
- end
173
+ AttributeValueResolvers::Const.new(init_opts[:const])
174
+ end
321
175
 
322
- def delegate_to_step(delegate_to, access)
323
- return AttributeValueResolvers::Keyword.new(delegate_to) unless access
176
+ def prepare_keyword_block
177
+ AttributeNormalizers::HashAccess.call(init_opts[:hash_access], method_name, config.hash_access)
178
+ end
324
179
 
325
- mode, allow_missing_key = parse_hash_access(access)
326
- AttributeValueResolvers::HashAccessKeyword.new(delegate_to, mode, allow_missing_key)
327
- end
180
+ # Reads the value from the loaded batch of the only batch loader
181
+ def prepare_batch_loader_block
182
+ batch = batch_option
183
+ return unless batch
328
184
 
329
- def delegate_final_step(key_method_name, access)
330
- return AttributeValueResolvers::Keyword.new(key_method_name) unless access
185
+ AttributeValueResolvers::Batch.new(batch_loaders.first, batch.id || config.batch_id_option)
186
+ end
331
187
 
332
- mode, allow_missing_key = parse_hash_access(access)
333
- AttributeValueResolvers::HashAccessKeyword.new(key_method_name, mode, allow_missing_key)
334
- end
188
+ # Batch loader names whose loaded data this attribute's value needs. An
189
+ # attribute without the :batch option has none.
190
+ def prepare_batch_loaders
191
+ batch = batch_option
192
+ return FROZEN_EMPTY_ARRAY unless batch
335
193
 
336
- # Resolves the :hash_access option of plain attributes
337
- # @return [Array(Symbol, Boolean), nil] mode and allow_missing_key pair,
338
- # or nil when hash access is not enabled for this attribute
339
- def hash_access
340
- option = init_opts[:hash_access]
341
- return unless option
194
+ batch.loader_names
195
+ end
342
196
 
343
- parse_hash_access(option)
344
- end
197
+ # The parsed :batch option, `nil` without it
198
+ def batch_option
199
+ return @batch_option if instance_variable_defined?(:@batch_option)
345
200
 
346
- # Resolves a :hash_access value (`true`, a Symbol mode or a
347
- # `{mode:, allow_missing_key:}` Hash) into a mode and allow_missing_key
348
- # pair. `true` and a Hash omitting :mode resolve to
349
- # `config.hash_access.default_mode`.
350
- def parse_hash_access(option)
351
- defaults = config.hash_access
352
-
353
- case option
354
- when true then [defaults.default_mode, defaults.default_allow_missing_key]
355
- when Symbol then [option, defaults.default_allow_missing_key]
356
- else
357
- mode = option.fetch(:mode) { defaults.default_mode }
358
- allow_missing_key = option.fetch(:allow_missing_key) { defaults.default_allow_missing_key }
359
- [mode, allow_missing_key]
360
- end
361
- end
201
+ batch = init_opts[:batch]
202
+ @batch_option = batch && AttributeNormalizers::Batch.new(batch, name)
203
+ end
362
204
 
363
- # Prepares preloads for this attribute.
364
- #
365
- # The value is passed through as provided (Symbol, Array, Hash, or any
366
- # custom value an ORM understands) — it is the data handed to the
367
- # serializer's `preload_with` handler.
368
- #
369
- # @return [Object, nil] preloads as provided, or nil when the attribute has none
370
- def prepare_preloads
371
- # Explicit preload option (false or nil disables preloading)
372
- return init_opts[:preload] || nil if init_opts.key?(:preload)
373
-
374
- # Auto-preload the delegated association
375
- if config.auto_preload.fetch(:has_delegate_option) && init_opts[:delegate]
376
- return auto_preload_value(init_opts[:delegate][:to])
377
- end
378
-
379
- # Auto-preload the nested serializer's association
380
- # (a block defines a nested serializer, same as the :serializer option)
381
- if config.auto_preload.fetch(:has_serializer_option) && (init_opts[:serializer] || init_block) && !init_opts.key?(:batch)
382
- return auto_preload_value(method_name)
383
- end
384
-
385
- nil
386
- end
205
+ def prepare_default
206
+ init_opts.fetch(:default) { many ? FROZEN_EMPTY_ARRAY : nil }
207
+ end
387
208
 
388
- # Skips auto-preloading of methods that return the serialized object
389
- # itself (:itself by default) — they are not associations,
390
- # so preloading them would fail or make no sense.
391
- def auto_preload_value(preload_method)
392
- return nil if config.auto_preload_excluded_methods.include?(preload_method.to_sym)
209
+ def prepare_delegate_block
210
+ delegate = init_opts[:delegate]
211
+ return unless delegate
393
212
 
394
- preload_method
395
- end
213
+ AttributeNormalizers::Delegate.call(delegate, method_name, config)
396
214
  end
397
215
 
398
- extend Serega::SeregaHelpers::SerializerClassHelper
399
- include AttributeNormalizerInstanceMethods
216
+ def prepare_preloads
217
+ AttributeNormalizers::Preloads.call(init_opts, init_block, method_name, config)
218
+ end
400
219
  end
401
220
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Serega
4
+ #
5
+ # Parsers of attribute options, used by SeregaAttributeNormalizer
6
+ #
7
+ module AttributeNormalizers # :nodoc:
8
+ #
9
+ # The attribute :batch option, parsed into the loaders and the id method.
10
+ # `true`, a loader name, a callable or a Hash with `:use` and `:id`; `true`
11
+ # and a Hash without `:use` use the loader named after the attribute.
12
+ #
13
+ class Batch # :nodoc:
14
+ # Name of the attribute with the option
15
+ attr_reader :attribute_name
16
+
17
+ # Loader name, Array of loader names, or a callable inline loader
18
+ attr_reader :use
19
+
20
+ # Method that gives the id of an object, `nil` when not given
21
+ attr_reader :id
22
+
23
+ # Parses the :batch option of the attribute
24
+ def initialize(batch, attribute_name)
25
+ batch = {use: batch} unless batch.is_a?(Hash)
26
+ use = batch.fetch(:use, true)
27
+
28
+ @attribute_name = attribute_name
29
+ @use = (use == true) ? attribute_name : use
30
+ @id = batch[:id]
31
+ end
32
+
33
+ # Whether the option is the loader itself, a callable
34
+ def inline?
35
+ use.respond_to?(:call)
36
+ end
37
+
38
+ # Names of the batch loaders the attribute value needs. An inline loader
39
+ # is named after the attribute.
40
+ def loader_names
41
+ inline? ? [attribute_name.to_sym] : Array(use).map(&:to_sym)
42
+ end
43
+ end
44
+ end
45
+ end