cmdx 1.18.0 → 1.20.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/.DS_Store +0 -0
- data/CHANGELOG.md +73 -9
- data/README.md +1 -1
- data/lib/cmdx/attribute.rb +88 -20
- data/lib/cmdx/attribute_registry.rb +79 -8
- data/lib/cmdx/attribute_value.rb +8 -3
- data/lib/cmdx/callback_registry.rb +60 -26
- data/lib/cmdx/chain.rb +47 -4
- data/lib/cmdx/coercion_registry.rb +42 -20
- data/lib/cmdx/coercions/array.rb +8 -3
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +6 -2
- data/lib/cmdx/coercions/complex.rb +1 -1
- data/lib/cmdx/coercions/date.rb +2 -7
- data/lib/cmdx/coercions/date_time.rb +2 -7
- data/lib/cmdx/coercions/float.rb +1 -1
- data/lib/cmdx/coercions/hash.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +4 -5
- data/lib/cmdx/coercions/rational.rb +1 -1
- data/lib/cmdx/coercions/string.rb +1 -1
- data/lib/cmdx/coercions/symbol.rb +1 -1
- data/lib/cmdx/coercions/time.rb +1 -7
- data/lib/cmdx/configuration.rb +26 -0
- data/lib/cmdx/context.rb +9 -6
- data/lib/cmdx/deprecator.rb +27 -14
- data/lib/cmdx/errors.rb +3 -4
- data/lib/cmdx/exception.rb +7 -0
- data/lib/cmdx/executor.rb +77 -54
- data/lib/cmdx/identifier.rb +4 -6
- data/lib/cmdx/locale.rb +32 -9
- data/lib/cmdx/middleware_registry.rb +43 -23
- data/lib/cmdx/middlewares/correlate.rb +4 -2
- data/lib/cmdx/middlewares/timeout.rb +11 -10
- data/lib/cmdx/parallelizer.rb +100 -0
- data/lib/cmdx/pipeline.rb +42 -23
- data/lib/cmdx/railtie.rb +1 -1
- data/lib/cmdx/result.rb +27 -11
- data/lib/cmdx/retry.rb +166 -0
- data/lib/cmdx/settings.rb +222 -0
- data/lib/cmdx/task.rb +53 -61
- data/lib/cmdx/utils/format.rb +17 -1
- data/lib/cmdx/utils/normalize.rb +52 -0
- data/lib/cmdx/utils/wrap.rb +38 -0
- data/lib/cmdx/validator_registry.rb +45 -20
- data/lib/cmdx/validators/absence.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +2 -2
- data/lib/cmdx/validators/format.rb +1 -1
- data/lib/cmdx/validators/inclusion.rb +2 -2
- data/lib/cmdx/validators/length.rb +1 -1
- data/lib/cmdx/validators/numeric.rb +1 -1
- data/lib/cmdx/validators/presence.rb +1 -1
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx.rb +12 -0
- data/lib/generators/cmdx/templates/install.rb +11 -0
- data/mkdocs.yml +5 -1
- metadata +6 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34ebd45d6a27296d5960995fc1b313b808f1d3835df82ce688ae1a6dc4faa313
|
|
4
|
+
data.tar.gz: 01ccc099ffd40950e4085c850ba3959db59bddbb5df39e8b540a6310607f7a59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9cadc2e6c98e819512a62eb484c7ffb5ed2ea29bf135851441cb78fa4bb64421b844337d489976c4148c0e471144159640257dc20d574a802e90619d833c2316
|
|
7
|
+
data.tar.gz: 3edd420426ccc2ed48b1bc3f972984111eb10ec1f41bd835de478318cb5dba169cb440c5ff24b202533a942040a62aaf9897d88f03229cc97285ea69af11a377
|
data/.DS_Store
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -6,14 +6,76 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [UNRELEASED]
|
|
8
8
|
|
|
9
|
-
## [1.
|
|
9
|
+
## [1.20.0] - 2026-03-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Add `CallbackRegistry#empty?` for fast callback presence checking
|
|
13
|
+
- Add `Parallelizer` class for bounded thread pool execution
|
|
14
|
+
- Add `Configuration#default_locale` setting (defaults to `"en"`)
|
|
15
|
+
- Add `Task.type` to return task mechanics
|
|
16
|
+
- Add `Utils::Normalize` module for exception and status normalization
|
|
17
|
+
- Add `Utils::Wrap` module for array value normalization
|
|
18
|
+
- Add `Retry` class for retry logic, state tracking, and jitter computation
|
|
19
|
+
- Add `Settings` object with method-based access
|
|
20
|
+
- Add `freeze_results` configuration option to replace `SKIP_CMDX_FREEZING` env var
|
|
21
|
+
- Add `any?`, `clear`, and `size` delegators to `Errors`
|
|
22
|
+
- Add `Context#respond_to_missing?` for setter methods
|
|
23
|
+
- Add `Attribute#clear_task_tree!` to prevent stale task instance retention
|
|
24
|
+
- Add thread-safe `Chain#push` and `Chain#index` via `Mutex`
|
|
25
|
+
- Add identity-aware `Executor#clear_chain!` for parallel execution safety
|
|
26
|
+
- Add `Executor#verify_middleware_yield!` to detect non-yielding middlewares
|
|
27
|
+
- Add copy-on-write semantics to `MiddlewareRegistry`, `CallbackRegistry`, `CoercionRegistry`, and `ValidatorRegistry`
|
|
28
|
+
- Add `Attribute#allocation_name` for task-free reader name resolution
|
|
29
|
+
- Add `AttributeRegistry#define_readers_on!` and `#undefine_readers_on!` for eager reader definition
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Short-circuit `Executor#post_execution!` when callback registry is empty
|
|
33
|
+
- Optimize `Context#method_missing` to avoid `String` allocation on getter path
|
|
34
|
+
- Replace `parallel` gem with native `Parallelizer` thread pool
|
|
35
|
+
- Rename `in_threads` option to `pool_size`
|
|
36
|
+
- Move `TimeoutError` to `exception.rb` for Zeitwerk autoloading
|
|
37
|
+
- Update Rails initializer install script
|
|
38
|
+
- Dup attributes in `AttributeRegistry#define_and_verify` for thread-safe concurrent execution
|
|
39
|
+
- Default `retry_on` to `[StandardError, CMDx::TimeoutError]`
|
|
40
|
+
- Replace hash-based `settings[:]` with method-based `settings.` access
|
|
41
|
+
- Lazy-load locale translations instead of eager-loading
|
|
42
|
+
- Use compile-time method definition for `Identifier#generate` and `Chain`/`Correlate` `thread_or_fiber`
|
|
43
|
+
- Use `define_method` on task class for attribute readers
|
|
44
|
+
- Tighten `Deprecator` regex to exact word boundaries
|
|
45
|
+
- Use `public_send` instead of `send` in `Result` for state/status checks
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
- Fix `Attribute#source` and `#method_name` memoization without a task
|
|
49
|
+
- Fix `execute!` to call `executed!` before `post_execution!` on non-halt path
|
|
50
|
+
- Clear `task.errors` before each retry attempt
|
|
51
|
+
- Fix `Pipeline#execute_tasks_in_parallel` to snapshot context per thread
|
|
52
|
+
- Reject `in_processes` and `in_reactors` options in parallel tasks
|
|
53
|
+
|
|
54
|
+
### Removed
|
|
55
|
+
- Remove `SKIP_CMDX_FREEZING` env var in favor of `CMDx.configuration.freeze_results`
|
|
56
|
+
|
|
57
|
+
## [1.19.0] - 2026-03-09
|
|
58
|
+
|
|
59
|
+
### Changed
|
|
60
|
+
- Fall back attribute `source` to `:context` when no task is given
|
|
61
|
+
- Improve falsy attribute derived hash value lookup
|
|
62
|
+
- Freeze chain results
|
|
63
|
+
- Use `to_date`, `to_time`, `to_datetime` for date/time coercion checks
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
- Fix missing fault cause `NoMethodError`
|
|
67
|
+
- Fix validator `allow_nil` inverted logic
|
|
68
|
+
- Fix array coercion JSON parse error to return `CoercionError`
|
|
69
|
+
- Fix boolean coercions to return `false` for `nil` and `""`
|
|
70
|
+
|
|
71
|
+
## [1.18.0] - 2026-03-09
|
|
10
72
|
|
|
11
73
|
### Changed
|
|
12
74
|
- Use `Fiber.storage` instead of `Thread.current` for `Chain` and `Correlate` storage, with fallback to `Thread.current` for Ruby < 3.2, making them thread and fiber safe
|
|
13
75
|
- Clone shared logger in `Task#logger` when `log_level` or `log_formatter` is customized to prevent mutation of the shared instance
|
|
14
76
|
- Derive attribute values from source objects that respond to the attribute name (via `send`) as fallback when the source is not callable
|
|
15
77
|
|
|
16
|
-
## [1.17.0] -
|
|
78
|
+
## [1.17.0] - 2026-02-23
|
|
17
79
|
|
|
18
80
|
### Added
|
|
19
81
|
- Add `returns` macro for context output validation after task execution
|
|
@@ -22,26 +84,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
22
84
|
- Add hash coercion for JSON `"null"` string as empty hash
|
|
23
85
|
- Add attribute sourcing to support both string and symbol keys when sourcing/deriving from Hash
|
|
24
86
|
|
|
25
|
-
|
|
26
|
-
- Do not fail coercion if nil on optional attributes
|
|
87
|
+
### Changed
|
|
27
88
|
- Include the source method in the required attribute error message
|
|
28
89
|
|
|
29
|
-
|
|
90
|
+
### Fixed
|
|
91
|
+
- Fix coercion to not fail on `nil` for optional attributes
|
|
92
|
+
|
|
93
|
+
## [1.16.0] - 2026-02-06
|
|
30
94
|
|
|
31
95
|
### Added
|
|
32
96
|
- Add `CMDx::Exception` alias for `CMDx::Error`
|
|
33
97
|
|
|
34
98
|
### Changed
|
|
35
|
-
-
|
|
36
|
-
-
|
|
99
|
+
- Rename `exceptions.rb` file to `exception.rb` (zeitwerk compatibility)
|
|
100
|
+
- Rename `faults.rb` file to `fault.rb` (zeitwerk compatibility)
|
|
37
101
|
|
|
38
|
-
## [1.15.0] -
|
|
102
|
+
## [1.15.0] - 2026-01-21
|
|
39
103
|
|
|
40
104
|
### Added
|
|
41
105
|
- Add attribute `Absence` validator
|
|
42
106
|
- Add attribute `:description` option
|
|
43
107
|
|
|
44
|
-
## [1.14.0] -
|
|
108
|
+
## [1.14.0] - 2026-01-09
|
|
45
109
|
|
|
46
110
|
### Added
|
|
47
111
|
- Add Ruby 4.0 compatibility
|
data/README.md
CHANGED
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
[Changelog](./CHANGELOG.md) ·
|
|
13
13
|
[Report Bug](https://github.com/drexed/cmdx/issues) ·
|
|
14
14
|
[Request Feature](https://github.com/drexed/cmdx/issues) ·
|
|
15
|
+
[AI Skills](https://github.com/drexed/cmdx/blob/main/skills) ·
|
|
15
16
|
[llms.txt](https://drexed.github.io/cmdx/llms.txt) ·
|
|
16
17
|
[llms-full.txt](https://drexed.github.io/cmdx/llms-full.txt) ·
|
|
17
|
-
[SKILL.md](https://github.com/drexed/cmdx/blob/main/docs/SKILL.md)
|
|
18
18
|
|
|
19
19
|
<img alt="Version" src="https://img.shields.io/gem/v/cmdx">
|
|
20
20
|
<img alt="Build" src="https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg">
|
data/lib/cmdx/attribute.rb
CHANGED
|
@@ -108,7 +108,7 @@ module CMDx
|
|
|
108
108
|
def initialize(name, options = {}, &)
|
|
109
109
|
@parent = options.delete(:parent)
|
|
110
110
|
@required = options.delete(:required) || false
|
|
111
|
-
@types =
|
|
111
|
+
@types = Utils::Wrap.array(options.delete(:types) || options.delete(:type))
|
|
112
112
|
@description = options.delete(:description) || options.delete(:desc)
|
|
113
113
|
|
|
114
114
|
@name = name.to_sym
|
|
@@ -118,6 +118,21 @@ module CMDx
|
|
|
118
118
|
instance_eval(&) if block_given?
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# Deep-copies children and clears task-dependent memoization so that
|
|
122
|
+
# duped attributes can be safely bound to a task without mutating the
|
|
123
|
+
# class-level originals. This makes concurrent execution thread-safe.
|
|
124
|
+
#
|
|
125
|
+
# @param source [Attribute] The attribute being duplicated
|
|
126
|
+
#
|
|
127
|
+
# @rbs (Attribute source) -> void
|
|
128
|
+
def initialize_dup(source)
|
|
129
|
+
super
|
|
130
|
+
@children = source.children.map(&:dup)
|
|
131
|
+
remove_instance_variable(:@source) if defined?(@source)
|
|
132
|
+
remove_instance_variable(:@method_name) if defined?(@method_name)
|
|
133
|
+
@task = nil
|
|
134
|
+
end
|
|
135
|
+
|
|
121
136
|
class << self
|
|
122
137
|
|
|
123
138
|
# Builds multiple attributes with the same configuration.
|
|
@@ -208,7 +223,8 @@ module CMDx
|
|
|
208
223
|
!!@required
|
|
209
224
|
end
|
|
210
225
|
|
|
211
|
-
# Determines the source of the attribute value.
|
|
226
|
+
# Determines the source of the attribute value. Returns :context
|
|
227
|
+
# as a safe fallback when task is not yet set (e.g., schema introspection).
|
|
212
228
|
#
|
|
213
229
|
# @return [Symbol] The source identifier for the attribute value
|
|
214
230
|
#
|
|
@@ -217,15 +233,47 @@ module CMDx
|
|
|
217
233
|
#
|
|
218
234
|
# @rbs () -> untyped
|
|
219
235
|
def source
|
|
220
|
-
@source
|
|
236
|
+
return @source if defined?(@source)
|
|
237
|
+
|
|
238
|
+
parent&.method_name || begin
|
|
221
239
|
value = options[:source]
|
|
222
240
|
|
|
223
241
|
if value.is_a?(Proc)
|
|
224
|
-
task.instance_eval(&value)
|
|
242
|
+
task ? @source = task.instance_eval(&value) : :context
|
|
225
243
|
elsif value.respond_to?(:call)
|
|
226
|
-
value.call(task)
|
|
244
|
+
task ? @source = value.call(task) : :context
|
|
227
245
|
else
|
|
228
|
-
value || :context
|
|
246
|
+
@source = value || :context
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Returns the method name for this attribute when it can be resolved
|
|
252
|
+
# statically (without a task instance). Returns nil for Proc/callable
|
|
253
|
+
# sources whose method name depends on runtime evaluation.
|
|
254
|
+
#
|
|
255
|
+
# @return [Symbol, nil] The static method name, or nil if dynamic
|
|
256
|
+
#
|
|
257
|
+
# @example
|
|
258
|
+
# attribute.allocation_name # => :user_name
|
|
259
|
+
#
|
|
260
|
+
# @rbs () -> Symbol?
|
|
261
|
+
def allocation_name
|
|
262
|
+
return @allocation_name if defined?(@allocation_name)
|
|
263
|
+
|
|
264
|
+
@allocation_name = options[:as] || begin
|
|
265
|
+
src = options[:source]
|
|
266
|
+
source_name =
|
|
267
|
+
if parent
|
|
268
|
+
parent.allocation_name
|
|
269
|
+
elsif !src.is_a?(Proc) && !src.respond_to?(:call)
|
|
270
|
+
src || :context
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if source_name.is_a?(Symbol)
|
|
274
|
+
prefix = AFFIX.call(options[:prefix]) { "#{source_name}_" }
|
|
275
|
+
suffix = AFFIX.call(options[:suffix]) { "_#{source_name}" }
|
|
276
|
+
:"#{prefix}#{name}#{suffix}"
|
|
229
277
|
end
|
|
230
278
|
end
|
|
231
279
|
end
|
|
@@ -239,12 +287,19 @@ module CMDx
|
|
|
239
287
|
#
|
|
240
288
|
# @rbs () -> Symbol
|
|
241
289
|
def method_name
|
|
242
|
-
@method_name
|
|
290
|
+
return @method_name if defined?(@method_name)
|
|
291
|
+
|
|
292
|
+
result = options[:as] || begin
|
|
243
293
|
prefix = AFFIX.call(options[:prefix]) { "#{source}_" }
|
|
244
294
|
suffix = AFFIX.call(options[:suffix]) { "_#{source}" }
|
|
245
|
-
|
|
246
295
|
:"#{prefix}#{name}#{suffix}"
|
|
247
296
|
end
|
|
297
|
+
|
|
298
|
+
# Only memoize if @source is defined to avoid memoizing method
|
|
299
|
+
# name when no task is present.
|
|
300
|
+
return result unless defined?(@source)
|
|
301
|
+
|
|
302
|
+
@method_name = result
|
|
248
303
|
end
|
|
249
304
|
|
|
250
305
|
# Defines and verifies the entire attribute tree including nested children.
|
|
@@ -259,6 +314,15 @@ module CMDx
|
|
|
259
314
|
end
|
|
260
315
|
end
|
|
261
316
|
|
|
317
|
+
# Recursively clears the task reference from this attribute and all children.
|
|
318
|
+
# Prevents the class-level attribute from retaining the last-executed task instance.
|
|
319
|
+
#
|
|
320
|
+
# @rbs () -> void
|
|
321
|
+
def clear_task_tree!
|
|
322
|
+
@task = nil
|
|
323
|
+
children.each(&:clear_task_tree!)
|
|
324
|
+
end
|
|
325
|
+
|
|
262
326
|
# @return [Hash] A hash representation of the attribute
|
|
263
327
|
#
|
|
264
328
|
# @example
|
|
@@ -343,29 +407,33 @@ module CMDx
|
|
|
343
407
|
attributes(*names, **options.merge(required: true), &)
|
|
344
408
|
end
|
|
345
409
|
|
|
346
|
-
# Defines the attribute
|
|
410
|
+
# Defines the attribute reader on the task class (once) and
|
|
411
|
+
# generates/validates the per-instance value (every execution).
|
|
347
412
|
#
|
|
348
413
|
# @raise [RuntimeError] When the method name is already defined on the task
|
|
349
414
|
#
|
|
350
415
|
# @rbs () -> void
|
|
351
416
|
def define_and_verify
|
|
352
|
-
|
|
353
|
-
raise <<~MESSAGE
|
|
354
|
-
The method #{method_name.inspect} is already defined on the #{task.class.name} task.
|
|
355
|
-
This may be due conflicts with one of the task's user defined or internal methods/attributes.
|
|
417
|
+
name_of_method = method_name
|
|
356
418
|
|
|
357
|
-
|
|
358
|
-
|
|
419
|
+
unless task.class.method_defined?(name_of_method)
|
|
420
|
+
if task.respond_to?(name_of_method, true)
|
|
421
|
+
raise <<~MESSAGE
|
|
422
|
+
The method #{name_of_method.inspect} is already defined on the #{task.class.name} task.
|
|
423
|
+
This may be due conflicts with one of the task's user defined or internal methods/attributes.
|
|
424
|
+
|
|
425
|
+
Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
|
|
426
|
+
MESSAGE
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
task.class.define_method(name_of_method) do
|
|
430
|
+
attributes[name_of_method]
|
|
431
|
+
end
|
|
359
432
|
end
|
|
360
433
|
|
|
361
434
|
attribute_value = AttributeValue.new(self)
|
|
362
435
|
attribute_value.generate
|
|
363
436
|
attribute_value.validate
|
|
364
|
-
|
|
365
|
-
name_of_method = method_name
|
|
366
|
-
task.define_singleton_method(name_of_method) do
|
|
367
|
-
attributes[name_of_method]
|
|
368
|
-
end
|
|
369
437
|
end
|
|
370
438
|
|
|
371
439
|
end
|
|
@@ -56,7 +56,7 @@ module CMDx
|
|
|
56
56
|
#
|
|
57
57
|
# @rbs (Attribute | Array[Attribute] attributes) -> self
|
|
58
58
|
def register(attributes)
|
|
59
|
-
@registry.concat(
|
|
59
|
+
@registry.concat(Utils::Wrap.array(attributes))
|
|
60
60
|
self
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -73,29 +73,100 @@ module CMDx
|
|
|
73
73
|
#
|
|
74
74
|
# @rbs ((Symbol | String | Array[Symbol | String]) names) -> self
|
|
75
75
|
def deregister(names)
|
|
76
|
-
|
|
76
|
+
Utils::Wrap.array(names).each do |name|
|
|
77
77
|
@registry.reject! { |attribute| matches_attribute_tree?(attribute, name.to_sym) }
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
self
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
83
|
+
# Eagerly defines attribute reader methods on the task class for all
|
|
84
|
+
# attributes whose method names can be statically resolved. Called at
|
|
85
|
+
# class definition time so readers are defined once, not per-execution.
|
|
86
86
|
#
|
|
87
|
-
# @param
|
|
87
|
+
# @param task_class [Class] The task class to define readers on
|
|
88
|
+
# @param attrs [Array<Attribute>] Attributes to process (defaults to all)
|
|
89
|
+
#
|
|
90
|
+
# @rbs (Class task_class, ?Array[Attribute] attrs) -> void
|
|
91
|
+
def define_readers_on!(task_class, attrs = registry)
|
|
92
|
+
attrs.each { |attr| define_reader_tree!(task_class, attr) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Removes eagerly defined reader methods from the task class for
|
|
96
|
+
# attributes about to be deregistered. Must be called before {#deregister}.
|
|
97
|
+
#
|
|
98
|
+
# @param task_class [Class] The task class to undefine readers on
|
|
99
|
+
# @param names [Array<Symbol, String>] Attribute method names being removed
|
|
100
|
+
#
|
|
101
|
+
# @rbs (Class task_class, (Symbol | String | Array[Symbol | String]) names) -> void
|
|
102
|
+
def undefine_readers_on!(task_class, names)
|
|
103
|
+
Utils::Wrap.array(names).each do |name|
|
|
104
|
+
sym = name.to_sym
|
|
105
|
+
|
|
106
|
+
registry.each do |attribute|
|
|
107
|
+
next unless matches_attribute_tree?(attribute, sym)
|
|
108
|
+
|
|
109
|
+
undefine_reader_tree!(task_class, attribute)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Verifies attribute definitions against a task instance.
|
|
115
|
+
# Each attribute is duped before binding so the class-level originals
|
|
116
|
+
# are never mutated — this eliminates the concurrency hazard of
|
|
117
|
+
# shared mutable @task, @source, and @method_name state.
|
|
118
|
+
#
|
|
119
|
+
# @param task [Task] The task to verify attributes against
|
|
88
120
|
#
|
|
89
121
|
# @rbs (Task task) -> void
|
|
90
122
|
def define_and_verify(task)
|
|
91
123
|
registry.each do |attribute|
|
|
92
|
-
|
|
93
|
-
|
|
124
|
+
duplicate = attribute.dup
|
|
125
|
+
duplicate.task = task
|
|
126
|
+
duplicate.define_and_verify_tree
|
|
94
127
|
end
|
|
95
128
|
end
|
|
96
129
|
|
|
97
130
|
private
|
|
98
131
|
|
|
132
|
+
# Recursively defines reader methods for an attribute and its children
|
|
133
|
+
# when the method name is statically resolvable.
|
|
134
|
+
#
|
|
135
|
+
# @param task_class [Class] The task class to define readers on
|
|
136
|
+
# @param attribute [Attribute] The attribute to define a reader for
|
|
137
|
+
#
|
|
138
|
+
# @rbs (Class task_class, Attribute attribute) -> void
|
|
139
|
+
def define_reader_tree!(task_class, attribute)
|
|
140
|
+
name = attribute.allocation_name
|
|
141
|
+
|
|
142
|
+
if name && !task_class.method_defined?(name)
|
|
143
|
+
if task_class.private_method_defined?(name)
|
|
144
|
+
raise <<~MESSAGE
|
|
145
|
+
The method #{name.inspect} is already defined on the #{task_class.name} task.
|
|
146
|
+
This may be due conflicts with one of the task's user defined or internal methods/attributes.
|
|
147
|
+
|
|
148
|
+
Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
|
|
149
|
+
MESSAGE
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
task_class.define_method(name) { attributes[name] }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
attribute.children.each { |child| define_reader_tree!(task_class, child) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Recursively removes reader methods for an attribute and its children.
|
|
159
|
+
#
|
|
160
|
+
# @param task_class [Class] The task class to undefine readers on
|
|
161
|
+
# @param attribute [Attribute] The attribute whose readers to remove
|
|
162
|
+
#
|
|
163
|
+
# @rbs (Class task_class, Attribute attribute) -> void
|
|
164
|
+
def undefine_reader_tree!(task_class, attribute)
|
|
165
|
+
name = attribute.allocation_name
|
|
166
|
+
task_class.remove_method(name) if name && task_class.method_defined?(name, false)
|
|
167
|
+
attribute.children.each { |child| undefine_reader_tree!(task_class, child) }
|
|
168
|
+
end
|
|
169
|
+
|
|
99
170
|
# Recursively checks if an attribute or any of its children match the given name.
|
|
100
171
|
#
|
|
101
172
|
# @param attribute [Attribute] The attribute to check
|
data/lib/cmdx/attribute_value.rb
CHANGED
|
@@ -81,7 +81,7 @@ module CMDx
|
|
|
81
81
|
#
|
|
82
82
|
# @rbs () -> void
|
|
83
83
|
def validate
|
|
84
|
-
registry = task.class.settings
|
|
84
|
+
registry = task.class.settings.validators
|
|
85
85
|
|
|
86
86
|
options.slice(*registry.keys).each do |type, opts|
|
|
87
87
|
registry.validate(type, task, value, opts)
|
|
@@ -166,7 +166,12 @@ module CMDx
|
|
|
166
166
|
derived_value =
|
|
167
167
|
case source_value
|
|
168
168
|
when Context then source_value[name]
|
|
169
|
-
when Hash
|
|
169
|
+
when Hash
|
|
170
|
+
if source_value.key?(name.to_s)
|
|
171
|
+
source_value[name.to_s]
|
|
172
|
+
elsif source_value.key?(name.to_sym)
|
|
173
|
+
source_value[name.to_sym]
|
|
174
|
+
end
|
|
170
175
|
when Symbol then source_value.send(name)
|
|
171
176
|
when Proc then task.instance_exec(name, &source_value)
|
|
172
177
|
else
|
|
@@ -221,7 +226,7 @@ module CMDx
|
|
|
221
226
|
def coerce_value(transformed_value)
|
|
222
227
|
return transformed_value if types.empty?
|
|
223
228
|
|
|
224
|
-
registry = task.class.settings
|
|
229
|
+
registry = task.class.settings.coercions
|
|
225
230
|
last_idx = types.size - 1
|
|
226
231
|
|
|
227
232
|
types.find.with_index do |type, i|
|
|
@@ -5,8 +5,13 @@ module CMDx
|
|
|
5
5
|
#
|
|
6
6
|
# Callbacks are organized by type and can be registered with optional conditions and options.
|
|
7
7
|
# Each callback type represents a specific execution phase or outcome.
|
|
8
|
+
#
|
|
9
|
+
# Supports copy-on-write semantics: a duped registry shares the parent's
|
|
10
|
+
# data until a write operation triggers materialization.
|
|
8
11
|
class CallbackRegistry
|
|
9
12
|
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
10
15
|
# @rbs TYPES: Array[Symbol]
|
|
11
16
|
TYPES = %i[
|
|
12
17
|
before_validation
|
|
@@ -21,32 +26,43 @@ module CMDx
|
|
|
21
26
|
on_bad
|
|
22
27
|
].freeze
|
|
23
28
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
# @rbs TYPES_SET: Set[Symbol]
|
|
30
|
+
TYPES_SET = TYPES.to_set.freeze
|
|
31
|
+
private_constant :TYPES_SET
|
|
32
|
+
|
|
33
|
+
def_delegators :registry, :empty?
|
|
34
|
+
|
|
35
|
+
# @param registry [Hash, nil] Initial registry hash, defaults to empty
|
|
30
36
|
#
|
|
31
|
-
# @rbs
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
# @rbs (?Hash[Symbol, Set[Array[untyped]]]? registry) -> void
|
|
38
|
+
def initialize(registry = nil)
|
|
39
|
+
@registry = registry || {}
|
|
40
|
+
end
|
|
34
41
|
|
|
35
|
-
#
|
|
42
|
+
# Sets up copy-on-write state when duplicated via dup.
|
|
43
|
+
#
|
|
44
|
+
# @param source [CallbackRegistry] The registry being duplicated
|
|
36
45
|
#
|
|
37
|
-
# @rbs (
|
|
38
|
-
def
|
|
39
|
-
@
|
|
46
|
+
# @rbs (CallbackRegistry source) -> void
|
|
47
|
+
def initialize_dup(source)
|
|
48
|
+
@parent = source
|
|
49
|
+
@registry = nil
|
|
50
|
+
super
|
|
40
51
|
end
|
|
41
52
|
|
|
42
|
-
#
|
|
53
|
+
# Returns the internal registry of callbacks organized by type.
|
|
54
|
+
# Delegates to the parent registry when not yet materialized.
|
|
43
55
|
#
|
|
44
|
-
# @return [
|
|
56
|
+
# @return [Hash{Symbol => Set<Array>}] Hash mapping callback types to their registered callables
|
|
45
57
|
#
|
|
46
|
-
# @
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
# @example
|
|
59
|
+
# registry.registry # => { before_execution: #<Set: [[[:validate], {}]]> }
|
|
60
|
+
#
|
|
61
|
+
# @rbs () -> Hash[Symbol, Set[Array[untyped]]]
|
|
62
|
+
def registry
|
|
63
|
+
@registry || @parent.registry
|
|
49
64
|
end
|
|
65
|
+
alias to_h registry
|
|
50
66
|
|
|
51
67
|
# Registers one or more callables for a specific callback type
|
|
52
68
|
#
|
|
@@ -70,10 +86,12 @@ module CMDx
|
|
|
70
86
|
#
|
|
71
87
|
# @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
|
|
72
88
|
def register(type, *callables, **options, &block)
|
|
89
|
+
materialize!
|
|
90
|
+
|
|
73
91
|
callables << block if block_given?
|
|
74
92
|
|
|
75
|
-
registry[type] ||= Set.new
|
|
76
|
-
registry[type] << [callables, options]
|
|
93
|
+
@registry[type] ||= Set.new
|
|
94
|
+
@registry[type] << [callables, options]
|
|
77
95
|
self
|
|
78
96
|
end
|
|
79
97
|
|
|
@@ -92,11 +110,13 @@ module CMDx
|
|
|
92
110
|
#
|
|
93
111
|
# @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
|
|
94
112
|
def deregister(type, *callables, **options, &block)
|
|
113
|
+
materialize!
|
|
114
|
+
|
|
95
115
|
callables << block if block_given?
|
|
96
|
-
return self unless registry[type]
|
|
116
|
+
return self unless @registry[type]
|
|
97
117
|
|
|
98
|
-
registry[type].delete([callables, options])
|
|
99
|
-
registry.delete(type) if registry[type].empty?
|
|
118
|
+
@registry[type].delete([callables, options])
|
|
119
|
+
@registry.delete(type) if @registry[type].empty?
|
|
100
120
|
self
|
|
101
121
|
end
|
|
102
122
|
|
|
@@ -112,12 +132,13 @@ module CMDx
|
|
|
112
132
|
#
|
|
113
133
|
# @rbs (Symbol type, Task task) -> void
|
|
114
134
|
def invoke(type, task)
|
|
115
|
-
raise TypeError, "unknown callback type #{type.inspect}" unless
|
|
135
|
+
raise TypeError, "unknown callback type #{type.inspect}" unless TYPES_SET.include?(type)
|
|
136
|
+
return unless registry[type]
|
|
116
137
|
|
|
117
|
-
|
|
138
|
+
registry[type].each do |callables, options|
|
|
118
139
|
next unless Utils::Condition.evaluate(task, options)
|
|
119
140
|
|
|
120
|
-
|
|
141
|
+
Utils::Wrap.array(callables).each do |callable|
|
|
121
142
|
if callable.is_a?(Symbol)
|
|
122
143
|
task.send(callable)
|
|
123
144
|
elsif callable.is_a?(Proc)
|
|
@@ -131,5 +152,18 @@ module CMDx
|
|
|
131
152
|
end
|
|
132
153
|
end
|
|
133
154
|
|
|
155
|
+
private
|
|
156
|
+
|
|
157
|
+
# Copies the parent's registry data into this instance,
|
|
158
|
+
# severing the copy-on-write link.
|
|
159
|
+
#
|
|
160
|
+
# @rbs () -> void
|
|
161
|
+
def materialize!
|
|
162
|
+
return if @registry
|
|
163
|
+
|
|
164
|
+
@registry = @parent.registry.transform_values(&:dup)
|
|
165
|
+
@parent = nil
|
|
166
|
+
end
|
|
167
|
+
|
|
134
168
|
end
|
|
135
169
|
end
|