cmdx 1.19.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 +69 -16
- data/README.md +1 -1
- data/lib/cmdx/attribute.rb +82 -19
- data/lib/cmdx/attribute_registry.rb +79 -8
- data/lib/cmdx/attribute_value.rb +2 -2
- data/lib/cmdx/callback_registry.rb +60 -26
- data/lib/cmdx/chain.rb +33 -4
- data/lib/cmdx/coercion_registry.rb +42 -20
- data/lib/cmdx/coercions/array.rb +2 -2
- data/lib/cmdx/coercions/big_decimal.rb +1 -1
- data/lib/cmdx/coercions/boolean.rb +2 -2
- data/lib/cmdx/coercions/complex.rb +1 -1
- data/lib/cmdx/coercions/date.rb +1 -1
- data/lib/cmdx/coercions/date_time.rb +1 -1
- data/lib/cmdx/coercions/float.rb +1 -1
- data/lib/cmdx/coercions/hash.rb +1 -1
- data/lib/cmdx/coercions/integer.rb +1 -1
- 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 -1
- 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 +75 -52
- 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 -63
- 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 +44 -19
- 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,25 +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.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
|
+
|
|
9
31
|
### Changed
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
|
12
62
|
- Freeze chain results
|
|
13
|
-
-
|
|
14
|
-
|
|
63
|
+
- Use `to_date`, `to_time`, `to_datetime` for date/time coercion checks
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
- Fix missing fault cause `NoMethodError`
|
|
15
67
|
- Fix validator `allow_nil` inverted logic
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Coerce anaglous date, datetime, and time class checks to rely on `to_date`, `to_time`, `to_datetime` methods
|
|
68
|
+
- Fix array coercion JSON parse error to return `CoercionError`
|
|
69
|
+
- Fix boolean coercions to return `false` for `nil` and `""`
|
|
19
70
|
|
|
20
|
-
## [1.18.0] -
|
|
71
|
+
## [1.18.0] - 2026-03-09
|
|
21
72
|
|
|
22
73
|
### Changed
|
|
23
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
|
|
24
75
|
- Clone shared logger in `Task#logger` when `log_level` or `log_formatter` is customized to prevent mutation of the shared instance
|
|
25
76
|
- Derive attribute values from source objects that respond to the attribute name (via `send`) as fallback when the source is not callable
|
|
26
77
|
|
|
27
|
-
## [1.17.0] -
|
|
78
|
+
## [1.17.0] - 2026-02-23
|
|
28
79
|
|
|
29
80
|
### Added
|
|
30
81
|
- Add `returns` macro for context output validation after task execution
|
|
@@ -33,26 +84,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
33
84
|
- Add hash coercion for JSON `"null"` string as empty hash
|
|
34
85
|
- Add attribute sourcing to support both string and symbol keys when sourcing/deriving from Hash
|
|
35
86
|
|
|
36
|
-
|
|
37
|
-
- Do not fail coercion if nil on optional attributes
|
|
87
|
+
### Changed
|
|
38
88
|
- Include the source method in the required attribute error message
|
|
39
89
|
|
|
40
|
-
|
|
90
|
+
### Fixed
|
|
91
|
+
- Fix coercion to not fail on `nil` for optional attributes
|
|
92
|
+
|
|
93
|
+
## [1.16.0] - 2026-02-06
|
|
41
94
|
|
|
42
95
|
### Added
|
|
43
96
|
- Add `CMDx::Exception` alias for `CMDx::Error`
|
|
44
97
|
|
|
45
98
|
### Changed
|
|
46
|
-
-
|
|
47
|
-
-
|
|
99
|
+
- Rename `exceptions.rb` file to `exception.rb` (zeitwerk compatibility)
|
|
100
|
+
- Rename `faults.rb` file to `fault.rb` (zeitwerk compatibility)
|
|
48
101
|
|
|
49
|
-
## [1.15.0] -
|
|
102
|
+
## [1.15.0] - 2026-01-21
|
|
50
103
|
|
|
51
104
|
### Added
|
|
52
105
|
- Add attribute `Absence` validator
|
|
53
106
|
- Add attribute `:description` option
|
|
54
107
|
|
|
55
|
-
## [1.14.0] -
|
|
108
|
+
## [1.14.0] - 2026-01-09
|
|
56
109
|
|
|
57
110
|
### Added
|
|
58
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.
|
|
@@ -220,15 +235,45 @@ module CMDx
|
|
|
220
235
|
def source
|
|
221
236
|
return @source if defined?(@source)
|
|
222
237
|
|
|
223
|
-
|
|
238
|
+
parent&.method_name || begin
|
|
224
239
|
value = options[:source]
|
|
225
240
|
|
|
226
241
|
if value.is_a?(Proc)
|
|
227
|
-
task ? task.instance_eval(&value) : :context
|
|
242
|
+
task ? @source = task.instance_eval(&value) : :context
|
|
228
243
|
elsif value.respond_to?(:call)
|
|
229
|
-
task ? value.call(task) : :context
|
|
244
|
+
task ? @source = value.call(task) : :context
|
|
230
245
|
else
|
|
231
|
-
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}"
|
|
232
277
|
end
|
|
233
278
|
end
|
|
234
279
|
end
|
|
@@ -244,12 +289,17 @@ module CMDx
|
|
|
244
289
|
def method_name
|
|
245
290
|
return @method_name if defined?(@method_name)
|
|
246
291
|
|
|
247
|
-
|
|
292
|
+
result = options[:as] || begin
|
|
248
293
|
prefix = AFFIX.call(options[:prefix]) { "#{source}_" }
|
|
249
294
|
suffix = AFFIX.call(options[:suffix]) { "_#{source}" }
|
|
250
|
-
|
|
251
295
|
:"#{prefix}#{name}#{suffix}"
|
|
252
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
|
|
253
303
|
end
|
|
254
304
|
|
|
255
305
|
# Defines and verifies the entire attribute tree including nested children.
|
|
@@ -264,6 +314,15 @@ module CMDx
|
|
|
264
314
|
end
|
|
265
315
|
end
|
|
266
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
|
+
|
|
267
326
|
# @return [Hash] A hash representation of the attribute
|
|
268
327
|
#
|
|
269
328
|
# @example
|
|
@@ -348,29 +407,33 @@ module CMDx
|
|
|
348
407
|
attributes(*names, **options.merge(required: true), &)
|
|
349
408
|
end
|
|
350
409
|
|
|
351
|
-
# Defines the attribute
|
|
410
|
+
# Defines the attribute reader on the task class (once) and
|
|
411
|
+
# generates/validates the per-instance value (every execution).
|
|
352
412
|
#
|
|
353
413
|
# @raise [RuntimeError] When the method name is already defined on the task
|
|
354
414
|
#
|
|
355
415
|
# @rbs () -> void
|
|
356
416
|
def define_and_verify
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
417
|
+
name_of_method = method_name
|
|
418
|
+
|
|
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.
|
|
361
424
|
|
|
362
|
-
|
|
363
|
-
|
|
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
|
|
364
432
|
end
|
|
365
433
|
|
|
366
434
|
attribute_value = AttributeValue.new(self)
|
|
367
435
|
attribute_value.generate
|
|
368
436
|
attribute_value.validate
|
|
369
|
-
|
|
370
|
-
name_of_method = method_name
|
|
371
|
-
task.define_singleton_method(name_of_method) do
|
|
372
|
-
attributes[name_of_method]
|
|
373
|
-
end
|
|
374
437
|
end
|
|
375
438
|
|
|
376
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)
|
|
@@ -226,7 +226,7 @@ module CMDx
|
|
|
226
226
|
def coerce_value(transformed_value)
|
|
227
227
|
return transformed_value if types.empty?
|
|
228
228
|
|
|
229
|
-
registry = task.class.settings
|
|
229
|
+
registry = task.class.settings.coercions
|
|
230
230
|
last_idx = types.size - 1
|
|
231
231
|
|
|
232
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
|
data/lib/cmdx/chain.rb
CHANGED
|
@@ -31,7 +31,7 @@ module CMDx
|
|
|
31
31
|
# @rbs @results: Array[Result]
|
|
32
32
|
attr_reader :results
|
|
33
33
|
|
|
34
|
-
def_delegators :results, :
|
|
34
|
+
def_delegators :results, :first, :last, :size
|
|
35
35
|
def_delegators :first, :state, :status, :outcome, :runtime
|
|
36
36
|
|
|
37
37
|
# Creates a new chain with a unique identifier and empty results collection.
|
|
@@ -40,6 +40,7 @@ module CMDx
|
|
|
40
40
|
#
|
|
41
41
|
# @rbs () -> void
|
|
42
42
|
def initialize(dry_run: false)
|
|
43
|
+
@mutex = Mutex.new
|
|
43
44
|
@id = Identifier.generate
|
|
44
45
|
@results = []
|
|
45
46
|
@dry_run = !!dry_run
|
|
@@ -107,7 +108,7 @@ module CMDx
|
|
|
107
108
|
raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
|
|
108
109
|
|
|
109
110
|
self.current ||= new(dry_run:)
|
|
110
|
-
current.
|
|
111
|
+
current.push(result)
|
|
111
112
|
current
|
|
112
113
|
end
|
|
113
114
|
|
|
@@ -118,12 +119,40 @@ module CMDx
|
|
|
118
119
|
# @return [Hash] The thread or fiber storage
|
|
119
120
|
#
|
|
120
121
|
# @rbs () -> Hash
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
if Fiber.respond_to?(:storage)
|
|
123
|
+
def thread_or_fiber = Fiber.storage
|
|
124
|
+
else
|
|
125
|
+
def thread_or_fiber = Thread.current
|
|
123
126
|
end
|
|
124
127
|
|
|
125
128
|
end
|
|
126
129
|
|
|
130
|
+
# Thread-safe append of a result to the chain.
|
|
131
|
+
# Caches the result's index to avoid repeated O(n) lookups.
|
|
132
|
+
#
|
|
133
|
+
# @param result [Result] The result to append
|
|
134
|
+
#
|
|
135
|
+
# @return [Array<Result>] The updated results array
|
|
136
|
+
#
|
|
137
|
+
# @rbs (Result result) -> Array[Result]
|
|
138
|
+
def push(result)
|
|
139
|
+
@mutex.synchronize do
|
|
140
|
+
result.instance_variable_set(:@chain_index, @results.size)
|
|
141
|
+
@results << result
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Thread-safe lookup of a result's position in the chain.
|
|
146
|
+
#
|
|
147
|
+
# @param result [Result] The result to find
|
|
148
|
+
#
|
|
149
|
+
# @return [Integer, nil] The zero-based index or nil if not found
|
|
150
|
+
#
|
|
151
|
+
# @rbs (Result result) -> Integer?
|
|
152
|
+
def index(result)
|
|
153
|
+
@mutex.synchronize { @results.index(result) }
|
|
154
|
+
end
|
|
155
|
+
|
|
127
156
|
# Returns whether the chain is running in dry-run mode.
|
|
128
157
|
#
|
|
129
158
|
# @return [Boolean] Whether the chain is running in dry-run mode
|