cmdx 1.21.0 → 2.0.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/CHANGELOG.md +118 -1
- data/README.md +37 -24
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callbacks.rb +179 -0
- data/lib/cmdx/chain.rb +78 -175
- data/lib/cmdx/coercions/array.rb +19 -33
- data/lib/cmdx/coercions/big_decimal.rb +12 -29
- data/lib/cmdx/coercions/boolean.rb +25 -45
- data/lib/cmdx/coercions/coerce.rb +32 -0
- data/lib/cmdx/coercions/complex.rb +12 -27
- data/lib/cmdx/coercions/date.rb +29 -33
- data/lib/cmdx/coercions/date_time.rb +29 -33
- data/lib/cmdx/coercions/float.rb +8 -29
- data/lib/cmdx/coercions/hash.rb +17 -43
- data/lib/cmdx/coercions/integer.rb +8 -32
- data/lib/cmdx/coercions/rational.rb +12 -33
- data/lib/cmdx/coercions/string.rb +6 -24
- data/lib/cmdx/coercions/symbol.rb +12 -26
- data/lib/cmdx/coercions/time.rb +31 -35
- data/lib/cmdx/coercions.rb +174 -0
- data/lib/cmdx/configuration.rb +45 -237
- data/lib/cmdx/context.rb +264 -243
- data/lib/cmdx/deprecation.rb +67 -0
- data/lib/cmdx/deprecators/error.rb +22 -0
- data/lib/cmdx/deprecators/log.rb +22 -0
- data/lib/cmdx/deprecators/warn.rb +21 -0
- data/lib/cmdx/deprecators.rb +101 -0
- data/lib/cmdx/errors.rb +145 -79
- data/lib/cmdx/executors/fiber.rb +42 -0
- data/lib/cmdx/executors/thread.rb +36 -0
- data/lib/cmdx/executors.rb +95 -0
- data/lib/cmdx/fault.rb +85 -78
- data/lib/cmdx/i18n_proxy.rb +104 -0
- data/lib/cmdx/input.rb +294 -0
- data/lib/cmdx/inputs.rb +218 -0
- data/lib/cmdx/log_formatters/json.rb +9 -20
- data/lib/cmdx/log_formatters/key_value.rb +10 -21
- data/lib/cmdx/log_formatters/line.rb +7 -19
- data/lib/cmdx/log_formatters/logstash.rb +8 -21
- data/lib/cmdx/log_formatters/raw.rb +8 -20
- data/lib/cmdx/logger_proxy.rb +30 -0
- data/lib/cmdx/mergers/deep_merge.rb +23 -0
- data/lib/cmdx/mergers/last_write_wins.rb +23 -0
- data/lib/cmdx/mergers/no_merge.rb +20 -0
- data/lib/cmdx/mergers.rb +95 -0
- data/lib/cmdx/middlewares.rb +128 -0
- data/lib/cmdx/output.rb +115 -0
- data/lib/cmdx/outputs.rb +66 -0
- data/lib/cmdx/pipeline.rb +144 -131
- data/lib/cmdx/railtie.rb +10 -36
- data/lib/cmdx/result.rb +247 -524
- data/lib/cmdx/retriers/bounded_random.rb +24 -0
- data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
- data/lib/cmdx/retriers/exponential.rb +23 -0
- data/lib/cmdx/retriers/fibonacci.rb +39 -0
- data/lib/cmdx/retriers/full_random.rb +23 -0
- data/lib/cmdx/retriers/half_random.rb +24 -0
- data/lib/cmdx/retriers/linear.rb +23 -0
- data/lib/cmdx/retriers.rb +106 -0
- data/lib/cmdx/retry.rb +117 -138
- data/lib/cmdx/runtime.rb +251 -0
- data/lib/cmdx/settings.rb +68 -200
- data/lib/cmdx/signal.rb +165 -0
- data/lib/cmdx/task.rb +443 -343
- data/lib/cmdx/telemetry.rb +108 -0
- data/lib/cmdx/util.rb +73 -0
- data/lib/cmdx/validators/absence.rb +10 -39
- data/lib/cmdx/validators/exclusion.rb +33 -52
- data/lib/cmdx/validators/format.rb +19 -49
- data/lib/cmdx/validators/inclusion.rb +33 -54
- data/lib/cmdx/validators/length.rb +125 -127
- data/lib/cmdx/validators/numeric.rb +123 -123
- data/lib/cmdx/validators/presence.rb +10 -39
- data/lib/cmdx/validators/validate.rb +31 -0
- data/lib/cmdx/validators.rb +161 -0
- data/lib/cmdx/version.rb +2 -4
- data/lib/cmdx/workflow.rb +71 -96
- data/lib/cmdx.rb +111 -42
- data/lib/generators/cmdx/install_generator.rb +7 -17
- data/lib/generators/cmdx/task_generator.rb +12 -29
- data/lib/generators/cmdx/templates/install.rb +120 -48
- data/lib/generators/cmdx/templates/task.rb.tt +1 -1
- data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
- data/lib/generators/cmdx/workflow_generator.rb +12 -29
- data/lib/locales/en.yml +8 -7
- data/mkdocs.yml +25 -23
- metadata +39 -138
- data/lib/cmdx/attribute.rb +0 -440
- data/lib/cmdx/attribute_registry.rb +0 -185
- data/lib/cmdx/attribute_value.rb +0 -252
- data/lib/cmdx/callback_registry.rb +0 -169
- data/lib/cmdx/coercion_registry.rb +0 -138
- data/lib/cmdx/deprecator.rb +0 -77
- data/lib/cmdx/exception.rb +0 -46
- data/lib/cmdx/executor.rb +0 -378
- data/lib/cmdx/identifier.rb +0 -30
- data/lib/cmdx/locale.rb +0 -78
- data/lib/cmdx/middleware_registry.rb +0 -148
- data/lib/cmdx/middlewares/correlate.rb +0 -140
- data/lib/cmdx/middlewares/runtime.rb +0 -77
- data/lib/cmdx/middlewares/timeout.rb +0 -78
- data/lib/cmdx/parallelizer.rb +0 -100
- data/lib/cmdx/utils/call.rb +0 -53
- data/lib/cmdx/utils/condition.rb +0 -71
- data/lib/cmdx/utils/format.rb +0 -82
- data/lib/cmdx/utils/normalize.rb +0 -52
- data/lib/cmdx/utils/wrap.rb +0 -38
- data/lib/cmdx/validator_registry.rb +0 -143
- data/lib/generators/cmdx/locale_generator.rb +0 -39
- data/lib/locales/af.yml +0 -55
- data/lib/locales/ar.yml +0 -55
- data/lib/locales/az.yml +0 -55
- data/lib/locales/be.yml +0 -55
- data/lib/locales/bg.yml +0 -55
- data/lib/locales/bn.yml +0 -55
- data/lib/locales/bs.yml +0 -55
- data/lib/locales/ca.yml +0 -55
- data/lib/locales/cnr.yml +0 -55
- data/lib/locales/cs.yml +0 -55
- data/lib/locales/cy.yml +0 -55
- data/lib/locales/da.yml +0 -55
- data/lib/locales/de.yml +0 -55
- data/lib/locales/dz.yml +0 -55
- data/lib/locales/el.yml +0 -55
- data/lib/locales/eo.yml +0 -55
- data/lib/locales/es.yml +0 -55
- data/lib/locales/et.yml +0 -55
- data/lib/locales/eu.yml +0 -55
- data/lib/locales/fa.yml +0 -55
- data/lib/locales/fi.yml +0 -55
- data/lib/locales/fr.yml +0 -55
- data/lib/locales/fy.yml +0 -55
- data/lib/locales/gd.yml +0 -55
- data/lib/locales/gl.yml +0 -55
- data/lib/locales/he.yml +0 -55
- data/lib/locales/hi.yml +0 -55
- data/lib/locales/hr.yml +0 -55
- data/lib/locales/hu.yml +0 -55
- data/lib/locales/hy.yml +0 -55
- data/lib/locales/id.yml +0 -55
- data/lib/locales/is.yml +0 -55
- data/lib/locales/it.yml +0 -55
- data/lib/locales/ja.yml +0 -55
- data/lib/locales/ka.yml +0 -55
- data/lib/locales/kk.yml +0 -55
- data/lib/locales/km.yml +0 -55
- data/lib/locales/kn.yml +0 -55
- data/lib/locales/ko.yml +0 -55
- data/lib/locales/lb.yml +0 -55
- data/lib/locales/lo.yml +0 -55
- data/lib/locales/lt.yml +0 -55
- data/lib/locales/lv.yml +0 -55
- data/lib/locales/mg.yml +0 -55
- data/lib/locales/mk.yml +0 -55
- data/lib/locales/ml.yml +0 -55
- data/lib/locales/mn.yml +0 -55
- data/lib/locales/mr-IN.yml +0 -55
- data/lib/locales/ms.yml +0 -55
- data/lib/locales/nb.yml +0 -55
- data/lib/locales/ne.yml +0 -55
- data/lib/locales/nl.yml +0 -55
- data/lib/locales/nn.yml +0 -55
- data/lib/locales/oc.yml +0 -55
- data/lib/locales/or.yml +0 -55
- data/lib/locales/pa.yml +0 -55
- data/lib/locales/pl.yml +0 -55
- data/lib/locales/pt.yml +0 -55
- data/lib/locales/rm.yml +0 -55
- data/lib/locales/ro.yml +0 -55
- data/lib/locales/ru.yml +0 -55
- data/lib/locales/sc.yml +0 -55
- data/lib/locales/sk.yml +0 -55
- data/lib/locales/sl.yml +0 -55
- data/lib/locales/sq.yml +0 -55
- data/lib/locales/sr.yml +0 -55
- data/lib/locales/st.yml +0 -55
- data/lib/locales/sv.yml +0 -55
- data/lib/locales/sw.yml +0 -55
- data/lib/locales/ta.yml +0 -55
- data/lib/locales/te.yml +0 -55
- data/lib/locales/th.yml +0 -55
- data/lib/locales/tl.yml +0 -55
- data/lib/locales/tr.yml +0 -55
- data/lib/locales/tt.yml +0 -55
- data/lib/locales/ug.yml +0 -55
- data/lib/locales/uk.yml +0 -55
- data/lib/locales/ur.yml +0 -55
- data/lib/locales/uz.yml +0 -55
- data/lib/locales/vi.yml +0 -55
- data/lib/locales/wo.yml +0 -55
- data/lib/locales/zh-CN.yml +0 -55
- data/lib/locales/zh-HK.yml +0 -55
- data/lib/locales/zh-TW.yml +0 -55
- data/lib/locales/zh-YUE.yml +0 -55
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b679253afacaaa6d2b1ef842026e3b535ee1b0378167bfa0ebfe5b8c93508e83
|
|
4
|
+
data.tar.gz: 1751b980c3af7d2e4950d6f1447d17883f94ba1e26ff1a550d90043fbb30b956
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6998f0a2d795797ca412008afb29eb8c7b8240eeda91a6f88cccae9b77c3250b1b8616d67d661bb82aab6f8d8c2b7f32b9cbc5e099fe1989eb7efd3d927d457
|
|
7
|
+
data.tar.gz: a4240ac375f5488dc3b312f19d0f556f8f1e78ed84189288de3fea8d82a3eda1586094d2ef45722450c95426ec007f400d926c327f5c2ddb8ad25415f2246187
|
data/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,124 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [UNRELEASED
|
|
7
|
+
## [2.0.0] - UNRELEASED
|
|
8
|
+
|
|
9
|
+
Full runtime rewrite: the v1 state-machine plus Zeitwerk architecture is replaced by an explicit signal-based runtime, immutable results, fiber-local chains, and a slimmer registry surface. See [docs/v2-migration.md](docs/v2-migration.md) for the full upgrade guide.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Add `CMDx::I18nProxy.tr` helper that translates a `reason` through the i18n layer when a matching key is present and falls back to the literal reason (or the `cmdx.reasons.unspecified` default when nil). `Fault#initialize` and `Pipeline` failure aggregation now route `result.reason` through it, so failure messages can be localized via translation keys without breaking literal-string reasons.
|
|
13
|
+
- Add `xid` correlation id on `Chain`, `Result`, and `Telemetry::Event`, sourced once per root execution from `CMDx.configuration.correlation_id` (a callable). Useful for threading external ids like Rails `request_id` through every task in a chain so they can be filtered together in logs/telemetry.
|
|
14
|
+
- Add `Context#as_json` / `Context#to_json` — JSON serialization delegating to `#to_h`
|
|
15
|
+
- Add `Errors#as_json` / `Errors#to_json` — JSON serialization delegating to `#to_h`
|
|
16
|
+
- Add `Result#as_json` / `Result#to_json` — JSON serialization delegating to the memoized `#to_h`
|
|
17
|
+
- Add `Input#as_json` / `Input#to_json` — JSON serialization delegating to `#to_h`
|
|
18
|
+
- Add `Output#as_json` / `Output#to_json` — JSON serialization delegating to `#to_h`
|
|
19
|
+
- Add `CMDx::Signal` halt token thrown via `catch(Signal::TAG)` (`:cmdx_signal`)
|
|
20
|
+
- Add `Signal#ok?` / `Signal#ko?` predicates
|
|
21
|
+
- Add `CMDx::Runtime` orchestrating the full task lifecycle and building the final `Result`
|
|
22
|
+
- Add `CMDx::Telemetry` pub/sub for `:task_started`, `:task_deprecated`, `:task_retried`, `:task_rolled_back`, `:task_executed`; emits `Telemetry::Event` data objects with `cid`, `root`, `type`, `task`, `tid`, `name`, `payload`, `timestamp`
|
|
23
|
+
- Add `CMDx::Deprecation` for declarative class-level deprecation (`:log`, `:warn`, `:error`, Symbol, Proc, callable) with `:if` / `:unless` gating
|
|
24
|
+
- Add `CMDx::Input` / `CMDx::Inputs` (replaces `Attribute` / `AttributeRegistry` / `AttributeValue`) supporting `:source`, `:default`, `:transform`, `:as`, `:prefix` / `:suffix`, and nested children via DSL block
|
|
25
|
+
- Add `CMDx::Output` / `CMDx::Outputs` for first-class declared outputs verified against `task.context` after `work`. Every declared output is implicitly required — there is no `:required` option. Surface is intentionally minimal: `:default` (literal/Symbol/Proc/`#call(task)`-able fallback applied when the value is `nil`, satisfies the implicit required check), `:if` / `:unless` guards, and `:description`. Coercion, transformation, validation, and nested children are intentionally not supported on outputs — use inputs (or compute in `work`) when you need any of those
|
|
26
|
+
- Add `CMDx::Util` single conditional-evaluation module (`evaluate`, `if?`, `unless?`, `satisfied?`) consolidating the v1 `Utils::*` modules
|
|
27
|
+
- Add `CMDx::I18nProxy` translation façade that delegates to `I18n` when available, otherwise loads the bundled YAML and percent-interpolates with memoization
|
|
28
|
+
- Add `CMDx::LoggerProxy` returning a per-task logger, `dup`-ing the base only when the task overrides `log_level` or `log_formatter`
|
|
29
|
+
- Add `around_execution` callback that wraps `before_validation`, `Task#work`, any `#rollback`, and `after_execution` in a single hook. Symbol callbacks receive the continuation as their block (use `yield`); Procs/blocks/callables receive `(task, continuation)` and must invoke `continuation.call`. Multiple hooks nest in declaration order; failure to invoke the continuation raises `CMDx::CallbackError`. Sits inside middlewares but outside the state/status callbacks, so it's the right place for symmetric concerns (transactions, instrumentation, per-task logging context) without bolting them onto `middlewares.register`
|
|
30
|
+
- Add new exception classes: `DefinitionError`, `DeprecationError`, `ImplementationError`, `MiddlewareError`, `CallbackError`
|
|
31
|
+
- Add `Task#work` abstract method (raises `ImplementationError` when not defined)
|
|
32
|
+
- Add `Task#rollback` lifecycle hook, auto-invoked by Runtime on failed results when defined; surfaced via `Result#rolled_back?` and the `:task_rolled_back` event
|
|
33
|
+
- Add saga-style pipeline rollback: when a `Workflow` halts, `Pipeline` walks every previously executed task instance whose result is `success?` in reverse execution order and invokes `#rollback` (when defined), then flips that result's `rolled_back?` to `true`. Skipped tasks are excluded; the failing task is rolled back by Runtime and not re-invoked. Exceptions raised inside a compensator propagate — handling them is the developer's responsibility. Applies across groups, within `continue_on_failure: true` groups, and to `:parallel` groups (compensators see the per-task `deep_dup`'d context)
|
|
34
|
+
- Add `Task#success!` for signaling a successful halt, joining `skip!` / `fail!` / `throw!`
|
|
35
|
+
- Add `Task.execute` / `Task.execute!` as the execution entry points (aliased as `call` / `call!` for backward compatibility)
|
|
36
|
+
- Add `Task#execute(strict:)` instance method (aliased as `#call`)
|
|
37
|
+
- Add `Result#on(:success, :failed, ...)` chainable predicate-dispatch helper
|
|
38
|
+
- Add `Result#deconstruct` / `Result#deconstruct_keys` for pattern matching; `deconstruct` returns `#to_h.to_a` pairs and `deconstruct_keys(keys)` slices `#to_h` (`nil` returns the full hash)
|
|
39
|
+
- Add `Result#strict?`, `Result#deprecated?`, `Result#duration`, `Result#index`, `Result#root?`, `Result#backtrace`, `Result#errors`, `Result#tags`, `Result#origin`, and `Result#ctx` alias
|
|
40
|
+
- Add `Signal#origin` / `Result#origin` — upstream `Result` a signal/result was echoed from (`nil` for locally originated failures); set by `Task#throw!`, `Pipeline` when propagating workflow failures, and `Runtime` when rescuing a `Fault` inside `work`
|
|
41
|
+
- Add `Chain#unshift`, `Chain#root`, `Chain#state`, `Chain#status`, `Chain#last`, `Chain#freeze`; Runtime `unshift`s the root result (so `chain.root` and `chain[0]` point to the outermost task) and freezes the chain on root teardown
|
|
42
|
+
- Add `Fault.for?(*tasks)`, `Fault.reason?(reason)`, and `Fault.matches?(&block)` anonymous matcher subclasses suitable for `rescue`
|
|
43
|
+
- Add `include Enumerable` to `Errors`, `Chain`, and `Context`, exposing `map`, `select`, `find`, `include?`, `to_a`, `any?`, `all?`, `group_by`, `partition`, etc.
|
|
44
|
+
- Add `Set`-backed deduping per key on `Errors`, plus `keys`, `each_key`, `each_value`, `count`, `delete`, `clear`, `full_messages`, `to_hash(full)`
|
|
45
|
+
- Add `Context#keys`, `values`, `empty?`, `size`, `delete`, `clear`, `eql?` / `==`, `hash`, `deep_dup`, `respond_to_missing?`, and `Context#merge` that accepts any context-like object
|
|
46
|
+
- Add `Coercions::Coerce` and `Validators::Validate` inline-callable handlers for `:coerce` / `:validate` hash entries; generic callables receive `(value, task)`, Symbol and Proc handlers still resolve against the task
|
|
47
|
+
- Add `Configuration#backtrace_cleaner` and `Configuration#telemetry`
|
|
48
|
+
- Add `Configuration#log_exclusions` (defaults to `[]`) and matching `Settings#log_exclusions` override — an array of `Result#to_h` keys to strip from the lifecycle log entry (e.g. `[:context, :metadata]`). When empty, `Runtime` logs the `Result` as before; otherwise it logs `result.to_h.except(*exclusions)`. Other consumers (telemetry, return values) see the full result
|
|
49
|
+
- Add `Configuration#strict_context` (defaults to `false`) and matching `Settings#strict_context` override, toggling `Context#strict`; when enabled, unknown dynamic reads (`ctx.missing`) raise `NoMethodError` instead of returning `nil` — `[]`, `fetch`, `dig`, `key?`, and `?` predicates stay lenient
|
|
50
|
+
- Add `CMDx.reset_configuration!` which clears global registry ivars on `Task` for clean test setup/teardown; subclasses that already cloned their registries are unaffected
|
|
51
|
+
- Add `:if` / `:unless` gates to `Callbacks#register` (Symbol, Proc, or any `#call`-able); per-event DSL helpers (`before_execution`, `on_success`, etc.) forward the options through
|
|
52
|
+
- Add `:if` / `:unless` gates to `Middlewares#register` (Symbol, Proc, or any `#call`-able); evaluated per task in `Middlewares#process` — skipped middlewares are bypassed and the chain continues
|
|
53
|
+
- Add `:if` / `:unless` gates to `Retry` / `Task.retry_on`; gate receives `(task, error, attempt)` and, when falsy, re-raises the exception instead of retrying (no further wait). Adds `Retry#condition_if` / `Retry#condition_unless` readers
|
|
54
|
+
- Add `Context#deconstruct` / `Context#deconstruct_keys` for pattern matching
|
|
55
|
+
- Add `Errors#deconstruct` / `Errors#deconstruct_keys` for pattern matching
|
|
56
|
+
- Add `:executor` option to parallel task groups (`Workflow.tasks ..., strategy: :parallel, executor: :threads | :fibers | #call`); `:threads` is the default and preserves current behavior, `:fibers` dispatches via `Fiber.schedule` bounded by `:pool_size` (requires a `Fiber.scheduler` such as the `async` gem's — raises `RuntimeError` when none is installed), and a user-supplied callable matching `call(jobs:, concurrency:, on_job:)` is accepted. Unknown symbols raise `ArgumentError`
|
|
57
|
+
- Add `:merger` option to parallel task groups controlling how successful sibling contexts fold back into the workflow context: `:last_write_wins` (default, matches previous behavior), `:deep_merge` (recursive over `Hash` values), `:no_merge` (workflow context left untouched), or a callable `call(workflow_context, result)`. Merging always walks successful results in declaration order. Unknown symbols raise `ArgumentError`
|
|
58
|
+
- Add `CMDx::Executors` registry (built-ins: `:threads` → `Executors::Thread`, `:fibers` → `Executors::Fiber`) and `CMDx::Mergers` registry (built-ins: `:last_write_wins`, `:deep_merge`, `:no_merge`) exposed on `Configuration#executors` / `#mergers` and per-task via `Task.executors` / `Task.mergers` (dup-on-inherit); `Task.register(:executor, ...)` and `Task.register(:merger, ...)` (plus matching `deregister`) let apps plug in custom dispatch/merge strategies resolvable by name from `:executor` / `:merger`
|
|
59
|
+
- Add `Context#deep_merge` — in-place recursive `Hash`-value merge; scalar-vs-hash collisions follow last-write-wins. Used by the `:deep_merge` parallel merge strategy but also available directly
|
|
60
|
+
- Add `:linear`, `:fibonacci`, and `:decorrelated_jitter` built-in retry jitter strategies on `Retry` / `Task.retry_on`. `:linear` sleeps `delay * (attempt + 1)`; `:fibonacci` sleeps `delay * fib(attempt + 1)`; `:decorrelated_jitter` is the AWS-recommended stateful strategy `next ∈ [delay, prev_sleep * 3]` (threaded across attempts inside a single `process` call, falls back to `delay` when no prior sleep). `Retry#wait` now accepts an optional `prev_delay` argument and returns the computed delay so callers can thread state
|
|
61
|
+
- Add `CMDx::Retriers` registry (built-ins: `:exponential`, `:half_random`, `:full_random`, `:bounded_random`, `:linear`, `:fibonacci`, `:decorrelated_jitter`) exposed on `Configuration#retriers` and per-task via `Task.retriers` (dup-on-inherit). `Task.register(:retrier, ...)` / `deregister(:retrier, ...)` let apps plug in custom jitter strategies resolvable by name from `:jitter`. Strategies are pure callables matching `call(attempt, delay, prev_delay)`. Symbols not present in the registry still fall through to task instance methods, preserving existing `jitter: :method_name` semantics
|
|
62
|
+
- Add `CMDx::Deprecators` registry (built-ins: `:log`, `:warn`, `:error`) exposed on `Configuration#deprecators` and per-task via `Task.deprecators` (dup-on-inherit). `Task.register(:deprecator, ...)` / `deregister(:deprecator, ...)` let apps plug in custom deprecation actions resolvable by name from `Task.deprecation`. Actions are callables matching `call(task)`. Symbols not present in the registry still fall through to task instance methods, preserving existing `deprecation :method_name` semantics. The built-in `:log`, `:warn`, `:error` arms are now strategy modules under `lib/cmdx/deprecators/` instead of inline branches in `Deprecation#execute`
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
- **BREAKING**: Rename `#call` → `#work` on task subclasses; `Task.execute` / `Task.execute!` are the new entry points (`call` / `call!` kept as aliases)
|
|
66
|
+
- **BREAKING**: `Result` is now frozen and read-only; all state lives in the embedded `Signal`, built once during `Runtime#finalize_result`
|
|
67
|
+
- **BREAKING**: Move `STATES` / `STATUSES` constants and the `initialized` / `executing` / `executed!` transitions from `Result` to `Signal::STATES` / `Signal::STATUSES` (only `complete` / `interrupted` and `success` / `skipped` / `failed` remain)
|
|
68
|
+
- **BREAKING**: Halt mechanism uses `catch(Signal::TAG)` + `throw` instead of mutating result state; `success!` / `skip!` / `fail!` / `throw!` are now private `Task` instance methods (no longer delegated through `Result`) and raise `FrozenError` when called after teardown
|
|
69
|
+
- **BREAKING**: `Chain` is now fiber-local (was thread-local), keyed on `Fiber[:cmdx_chain]`, with internal `Mutex` on `push` / `unshift`; root Runtime clears the chain on teardown
|
|
70
|
+
- **BREAKING**: `Result#chain` now returns the owning `Chain` object directly instead of its results array (use `result.chain.to_a` / `result.chain.results`, or iterate via `Chain`'s new Enumerable methods)
|
|
71
|
+
- **BREAKING**: Drive `Result#caused_failure` / `threw_failure` / `caused_failure?` / `thrown_failure?` off `Signal#origin` instead of `signal.cause`; `caused_failure` walks `origin` recursively to the originating leaf, `threw_failure` returns `origin || self`, `caused_failure?` is true when the result originated the failure chain, `thrown_failure?` is true when the result re-threw an upstream failure
|
|
72
|
+
- Generated input accessors are now plain instance methods backed by `@_input_<name>` ivars set during input resolution; outputs have no accessors and are read/written directly on `task.context`
|
|
73
|
+
- `Workflow` declares groups via `task` / `tasks` (still aliased) and supports `:strategy => :parallel`, `:pool_size`, `:continue_on_failure`, `:if` / `:unless` per group; defining `#work` on a workflow raises `ImplementationError`
|
|
74
|
+
- `Pipeline` gains a `:parallel` strategy with `:pool_size` (replacing the removed `Parallelizer`); parallel workers share the parent fiber's chain, each get a `deep_dup`-ed context, successful child contexts are merged back into the workflow's context, and the first failed result is echoed via `throw!` to halt the pipeline. By default pending tasks are cancelled on the first failure (in-flight tasks still finish and successful contexts still merge); opt into batch semantics with `:continue_on_failure => true` to run every task to completion and aggregate failures into the workflow's `errors` (keys namespaced as `"TaskClass.input"` for input/validation errors and `"TaskClass.<status>"` for bare `fail!` reasons). `:continue_on_failure` works for both `:sequential` and `:parallel` groups.
|
|
75
|
+
- `Task.callbacks`, `Task.middlewares`, `Task.coercions`, `Task.validators`, `Task.executors`, `Task.mergers`, `Task.telemetry`, `Task.inputs`, `Task.outputs` lazy-clone from the superclass (or global `Configuration`) on first access — subclasses extend rather than replace
|
|
76
|
+
- `Settings` is now a frozen value object holding only `logger`, `log_formatter`, `log_level`, `log_exclusions`, `backtrace_cleaner`, `tags`, `strict_context`; every getter falls back to `CMDx.configuration`
|
|
77
|
+
- `Context.build` accepts anything that responds to `#context` (e.g. another `Task`), unwraps repeatedly, and only re-wraps frozen contexts; symbolizes hash keys via `#to_hash` / `#to_h`
|
|
78
|
+
- `Retry` becomes a value object; `Task.retry_on` accumulates exceptions and options across the inheritance chain via `Retry#build`; supports built-in jitter strategies (`:exponential`, `:half_random`, `:full_random`, `:bounded_random`) plus Symbol / Proc / callable; retry wraps `work` only (input resolution and output verification run once, outside the retry loop)
|
|
79
|
+
- All registries (`Callbacks`, `Middlewares`, `Coercions`, `Validators`, `Telemetry`, `Inputs`, `Outputs`) implement `initialize_copy` for cheap copy-on-write inheritance; `register` / `deregister` validate types up-front and raise `ArgumentError` on misuse
|
|
80
|
+
- `Coercions#coerce` returns a `Coercions::Failure` sentinel with an i18n message recorded on `task.errors`; when multiple declared coercion rules match none (and none were inline), an aggregated `cmdx.coercions.into_any` message is reported instead of the per-rule messages
|
|
81
|
+
- `Validators#validate` records a message on `task.errors` for each failed rule (the individual built-in validators return `Validators::Failure`)
|
|
82
|
+
- Extend `Validators::Numeric` and `Validators::Length` with `:gt` / `:lt` (strict comparison, with `:gt_message` / `:lt_message` overrides and `cmdx.validators.{numeric,length}.{gt,lt}` i18n keys), plus `:gte` / `:lte` / `:eq` / `:not_eq` aliases that normalize to `:min` / `:max` / `:is` / `:is_not`
|
|
83
|
+
- `Fault#initialize` takes a single `Result`; `task`, `context`, and `chain` delegate to it; `Runtime` raises `Fault.new(@result.caused_failure)` so `fault.task` always points at the originating leaf (including in workflows and nested `execute!` chains)
|
|
84
|
+
- `Runtime` finalizes the `Result` before `raise_signal!` so the `Fault` it raises always carries a fully-built `Result`
|
|
85
|
+
- `Result#to_h` / `to_s` / `deconstruct_keys` now include `:origin` (compact `{ task:, tid: }` hash, or `nil` for locally originated failures)
|
|
86
|
+
- **BREAKING**: `Result#deconstruct` now returns `#to_h.to_a` (array of `[key, value]` pairs) instead of the fixed `[type, task, state, status, reason, metadata, cause, origin]` tuple — update any array-pattern matches to use find-patterns (`in [*, [:status, "failed"], *]`)
|
|
87
|
+
- `Result#deconstruct_keys` now honors its `keys` argument — `nil` returns the full `#to_h`, a key list slices it; previously it always returned the full hash
|
|
88
|
+
- `Middlewares` registry entries are now `[callable, options.freeze]` tuples — callers that read `Task.middlewares.registry` directly must map `.first` to recover the callable
|
|
89
|
+
- Slim the locale file: remove `attributes.undefined`, `coercions.unknown`, `faults.invalid`, `faults.unspecified`, `returns.*`; rename `returns.missing` → `outputs.missing`; add `nil_value` to `length` / `numeric` validator messages
|
|
90
|
+
- Generators emit the new `def work` template; the install template documents the new middleware / callback / telemetry / coercion / validator registration shapes
|
|
91
|
+
- Slim `Configuration` to: `middlewares`, `callbacks`, `coercions`, `validators`, `telemetry`, `default_locale`, `strict_context`, `backtrace_cleaner`, `logger`, `log_level`, `log_formatter`
|
|
92
|
+
- `Configuration#log_level` and `Configuration#log_formatter` now default to `nil` — treat them as optional overrides on top of `config.logger` (the default `Logger` still carries `Logger::INFO` + `LogFormatters::Line.new`). `LoggerProxy` only `dup`s the logger when a non-nil override differs from the logger's own level/formatter, so swapping `config.logger` no longer requires also clearing these fields
|
|
93
|
+
|
|
94
|
+
### Removed
|
|
95
|
+
- **BREAKING**: Remove `Result::STATES = [INITIALIZED, EXECUTING, COMPLETE, INTERRUPTED]`, the `executed!` / `executing!` transitions, and the `executed?` / `initialized?` / `executing?` predicates
|
|
96
|
+
- **BREAKING**: Remove `Task#id`, `Task#result`, `Task#chain` direct accessors — read these off the `Result` returned by `execute`
|
|
97
|
+
- **BREAKING**: Remove `Result#threw_failure?` predicate (`result.thrown_failure?` remains, with semantics flipped — true when the result re-threw an upstream failure)
|
|
98
|
+
- **BREAKING**: Remove `Result#chain_id` — read it off the chain: `result.cid`
|
|
99
|
+
- **BREAKING**: `Result#to_h` no longer produces nested `caused_failure` / `threw_failure` hashes; failure references render as `{ task:, tid: }` and `to_s` formats them as `<TaskClass uuid>`
|
|
100
|
+
- Remove `CMDx::Executor` (replaced by `CMDx::Runtime`)
|
|
101
|
+
- Remove `CMDx::Attribute`, `CMDx::AttributeRegistry`, `CMDx::AttributeValue` (replaced by `Input` / `Inputs` and `Output` / `Outputs`)
|
|
102
|
+
- Remove `CMDx::Resolver` (value resolution is owned by `Input#resolve`)
|
|
103
|
+
- Remove `CMDx::Identifier` (Runtime / Chain use `SecureRandom.uuid_v7` directly)
|
|
104
|
+
- Remove `CMDx::Locale` (superseded by `I18nProxy`)
|
|
105
|
+
- Remove `CMDx::Deprecator` (superseded by `Deprecation` declared per task class)
|
|
106
|
+
- Remove `CMDx::Parallelizer` (parallelism now lives in `Pipeline#run_parallel`)
|
|
107
|
+
- Remove `CMDx::CallbackRegistry`, `CMDx::MiddlewareRegistry`, `CMDx::CoercionRegistry`, `CMDx::ValidatorRegistry` (replaced by the simpler `Callbacks`, `Middlewares`, `Coercions`, `Validators` plain classes)
|
|
108
|
+
- Remove `CMDx::Utils::Call`, `CMDx::Utils::Condition`, `CMDx::Utils::Format`, `CMDx::Utils::Normalize`, `CMDx::Utils::Wrap` (collapsed into `CMDx::Util`)
|
|
109
|
+
- Remove built-in `CMDx::Middlewares::Correlate`, `CMDx::Middlewares::Runtime`, `CMDx::Middlewares::Timeout` — register equivalents on `config.middlewares` if needed
|
|
110
|
+
- Remove `CMDx::Exception` file — `CMDx::Error` / `Exception` and friends are now defined in `lib/cmdx.rb`
|
|
111
|
+
- Remove Zeitwerk autoloading (replaced by explicit `require_relative` ordering in `lib/cmdx.rb`); drop `forwardable`, `pathname`, `timeout`, and `zeitwerk` requires
|
|
112
|
+
- Remove `CMDx.gem_path` top-level helper
|
|
113
|
+
- Remove `Configuration#task_breakpoints`, `Configuration#workflow_breakpoints`, `Configuration#freeze_results`, `Configuration#exception_handler`, and the `SKIP_CMDX_FREEZING` env var — failure halting is now intrinsic to Runtime via `Signal` and `execute!` strict mode
|
|
114
|
+
- Remove `Chain#dry_run?` and the `dry_run:` context flag
|
|
115
|
+
|
|
116
|
+
### Migration notes
|
|
117
|
+
|
|
118
|
+
See [docs/v2-migration.md](docs/v2-migration.md) for the full upgrade guide. At minimum:
|
|
119
|
+
|
|
120
|
+
- Rename `def call` → `def work`; `MyTask.call(ctx)` still works (aliased) but prefer `MyTask.execute(ctx)`
|
|
121
|
+
- Replace `register :attribute, ...` with `required :name, ...` / `optional :name, ...` / `output :name, ...`
|
|
122
|
+
- Replace `result.chain_id` with `result.cid`
|
|
123
|
+
- Replace `task.id` / `task.result` / `task.chain` with reads off the `Result` returned by `execute`
|
|
124
|
+
- Subscribe to lifecycle observability via `config.telemetry.subscribe(:task_executed) { |event| ... }` instead of the removed `Runtime` / `Correlate` middlewares
|
|
8
125
|
|
|
9
126
|
## [1.21.0] - 2026-04-09
|
|
10
127
|
|
data/README.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
[Request Feature](https://github.com/drexed/cmdx/issues) ·
|
|
15
15
|
[AI Skills](https://github.com/drexed/cmdx/blob/main/skills) ·
|
|
16
16
|
[llms.txt](https://drexed.github.io/cmdx/llms.txt) ·
|
|
17
|
-
[llms-full.txt](https://drexed.github.io/cmdx/llms-full.txt)
|
|
17
|
+
[llms-full.txt](https://drexed.github.io/cmdx/llms-full.txt)
|
|
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">
|
|
@@ -26,14 +26,26 @@
|
|
|
26
26
|
Say goodbye to messy service objects. CMDx helps you design business logic with clarity and consistency—build faster, debug easier, and ship with confidence.
|
|
27
27
|
|
|
28
28
|
> [!NOTE]
|
|
29
|
-
> [Documentation](https://drexed.github.io/cmdx/getting_started/) reflects the latest code on `main`. For version-specific documentation,
|
|
29
|
+
> [Documentation](https://drexed.github.io/cmdx/getting_started/) reflects the latest code on `main`. For version-specific documentation, refer to the `docs/` directory within that version's tag.
|
|
30
|
+
|
|
31
|
+
## What you get
|
|
32
|
+
|
|
33
|
+
- **Standardized task contract** — typed inputs, declared outputs, explicit halts
|
|
34
|
+
- **Type system** — 13 coercers, 7 validators, all pluggable
|
|
35
|
+
- **Built-in flow control** — `skip!` / `fail!` / `throw!` with structured metadata
|
|
36
|
+
- **Retries and faults** — declarative `retry_on` with configurable jitter
|
|
37
|
+
- **Middleware and callbacks** — wrap the lifecycle without touching `work`
|
|
38
|
+
- **Observability** — structured logs and telemetry, no extra instrumentation
|
|
39
|
+
- **Composable workflows** — chain tasks into larger processes
|
|
40
|
+
|
|
41
|
+
See the [feature comparison](https://drexed.github.io/cmdx/comparison/) for how CMDx stacks up against other service-object gems.
|
|
30
42
|
|
|
31
43
|
## Requirements
|
|
32
44
|
|
|
33
|
-
- Ruby: MRI 3.
|
|
34
|
-
-
|
|
45
|
+
- Ruby: MRI 3.3+ or a compatible JRuby/TruffleRuby release
|
|
46
|
+
- Runtime dependencies: `bigdecimal` and `logger` (stdlib only — no ActiveSupport required)
|
|
35
47
|
|
|
36
|
-
Rails support is built-in, but
|
|
48
|
+
Rails support is built-in, but CMDx is framework-agnostic at its core.
|
|
37
49
|
|
|
38
50
|
## Installation
|
|
39
51
|
|
|
@@ -45,24 +57,23 @@ bundle add cmdx
|
|
|
45
57
|
|
|
46
58
|
## Quick Example
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
CMDx organizes business logic around the **CERO** pattern (pronounced "zero"): **Compose**, **Execute**, **React**, **Observe**.
|
|
49
61
|
|
|
50
62
|
### 1. Compose
|
|
51
63
|
|
|
52
|
-
|
|
53
|
-
# Full-featured task example
|
|
54
|
-
# See docs for minimum viable task examples
|
|
64
|
+
Declare inputs, outputs, retries, and callbacks, then implement `work`.
|
|
55
65
|
|
|
66
|
+
```ruby
|
|
56
67
|
class AnalyzeMetrics < CMDx::Task
|
|
57
|
-
|
|
68
|
+
retry_on Net::ReadTimeout, limit: 3, jitter: :exponential
|
|
58
69
|
|
|
59
70
|
on_success :track_analysis_completion!
|
|
60
71
|
|
|
61
|
-
required :dataset_id,
|
|
72
|
+
required :dataset_id, coerce: :integer, numeric: { min: 1 }
|
|
62
73
|
|
|
63
74
|
optional :analysis_type, default: "standard"
|
|
64
75
|
|
|
65
|
-
|
|
76
|
+
output :result, :analyzed_at
|
|
66
77
|
|
|
67
78
|
def work
|
|
68
79
|
if dataset.nil?
|
|
@@ -91,6 +102,8 @@ end
|
|
|
91
102
|
|
|
92
103
|
### 2. Execute
|
|
93
104
|
|
|
105
|
+
Every invocation returns a `Result`. Inputs are coerced and validated, exceptions are captured, outputs are verified, and the outcome is logged — automatically.
|
|
106
|
+
|
|
94
107
|
```ruby
|
|
95
108
|
result = AnalyzeMetrics.execute(
|
|
96
109
|
dataset_id: 123,
|
|
@@ -98,39 +111,39 @@ result = AnalyzeMetrics.execute(
|
|
|
98
111
|
)
|
|
99
112
|
```
|
|
100
113
|
|
|
114
|
+
Use `execute!` instead when you want failures to raise a `Fault`.
|
|
115
|
+
|
|
101
116
|
### 3. React
|
|
102
117
|
|
|
118
|
+
Branch on the result's status and read values, reasons, or metadata from it.
|
|
119
|
+
|
|
103
120
|
```ruby
|
|
104
121
|
if result.success?
|
|
105
122
|
puts "Metrics analyzed at #{result.context.analyzed_at}"
|
|
106
123
|
elsif result.skipped?
|
|
107
|
-
puts "
|
|
124
|
+
puts "Skipped: #{result.reason}"
|
|
108
125
|
elsif result.failed?
|
|
109
|
-
puts "
|
|
126
|
+
puts "Failed: #{result.reason} (code #{result.metadata[:code]})"
|
|
110
127
|
end
|
|
111
128
|
```
|
|
112
129
|
|
|
113
130
|
### 4. Observe
|
|
114
131
|
|
|
132
|
+
Every execution emits a structured log line with the chain id, task identity, state, status, reason, metadata, and duration — enough to correlate nested tasks and reconstruct what happened.
|
|
133
|
+
|
|
115
134
|
```log
|
|
116
|
-
I, [
|
|
117
|
-
index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
|
|
135
|
+
I, [2026-04-19T18:42:37.000000Z #3784] INFO -- cmdx: cid="018c2b95-b764-7fff-a1d2-..." index=1 root=false type="Task" task=SendAnalyzedEmail id="018c2b95-c091-..." state="complete" status="success" reason=nil metadata={} duration=34.7 tags=[]
|
|
118
136
|
|
|
119
|
-
I, [
|
|
120
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
|
|
137
|
+
I, [2026-04-19T18:43:15.000000Z #3784] INFO -- cmdx: cid="018c2b95-b764-7fff-a1d2-..." index=0 root=true type="Task" task=AnalyzeMetrics id="018c2b95-b764-..." state="complete" status="success" reason=nil metadata={} duration=187.4 tags=[]
|
|
121
138
|
```
|
|
122
139
|
|
|
123
|
-
Ready to dive in? Check out the [Getting Started](https://drexed.github.io/cmdx/getting_started/) guide
|
|
140
|
+
Ready to dive in? Check out the [Getting Started](https://drexed.github.io/cmdx/getting_started/) guide.
|
|
124
141
|
|
|
125
142
|
## Ecosystem
|
|
126
143
|
|
|
144
|
+
- [cmdx-i18n](https://github.com/drexed/cmdx-i18n) - 85+ translations
|
|
127
145
|
- [cmdx-rspec](https://github.com/drexed/cmdx-rspec) - RSpec test matchers
|
|
128
146
|
|
|
129
|
-
For backwards compatibility of certain functionality:
|
|
130
|
-
|
|
131
|
-
- [cmdx-i18n](https://github.com/drexed/cmdx-i18n) - 85+ translations, `v1.5.0` - `v1.6.2`
|
|
132
|
-
- [cmdx-parallel](https://github.com/drexed/cmdx-parallel) - Parallel workflow tasks, `v1.6.1` - `v1.6.2`
|
|
133
|
-
|
|
134
147
|
## Contributing
|
|
135
148
|
|
|
136
149
|
Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](CODE_OF_CONDUCT.md).
|
data/lib/cmdx/.DS_Store
CHANGED
|
Binary file
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CMDx
|
|
4
|
+
# Registry of lifecycle callbacks invoked by Runtime. Callbacks can be
|
|
5
|
+
# method names (Symbols dispatched via `task.send`), blocks/Procs
|
|
6
|
+
# (`instance_exec`'d on the task), or arbitrary `#call` objects.
|
|
7
|
+
#
|
|
8
|
+
# Each registration may carry `:if` / `:unless` gates (Symbol, Proc, or
|
|
9
|
+
# any `#call`-able). Gates are evaluated against the task before the
|
|
10
|
+
# callback is invoked; non-passing gates skip the callback silently.
|
|
11
|
+
class Callbacks
|
|
12
|
+
|
|
13
|
+
# Callback event names Runtime dispatches.
|
|
14
|
+
EVENTS = Set[
|
|
15
|
+
:before_validation,
|
|
16
|
+
:before_execution,
|
|
17
|
+
:around_execution,
|
|
18
|
+
:after_execution,
|
|
19
|
+
:on_complete,
|
|
20
|
+
:on_interrupted,
|
|
21
|
+
:on_success,
|
|
22
|
+
:on_skipped,
|
|
23
|
+
:on_failed,
|
|
24
|
+
:on_ok,
|
|
25
|
+
:on_ko
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :registry
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
@registry = {}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param source [Callbacks] registry to duplicate
|
|
35
|
+
# @return [void]
|
|
36
|
+
def initialize_copy(source)
|
|
37
|
+
@registry = source.registry.transform_values(&:dup)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Adds a callback for `event`.
|
|
41
|
+
#
|
|
42
|
+
# @param event [Symbol] one of {EVENTS}
|
|
43
|
+
# @param callable [Symbol, #call, nil] method name or callable; pass either this or a block
|
|
44
|
+
# @param block [#call, nil] callback body when `callable` is omitted
|
|
45
|
+
# @param options [Hash{Symbol => Object}]
|
|
46
|
+
# @option options [Symbol, Proc, #call] :if gate that must evaluate truthy
|
|
47
|
+
# @option options [Symbol, Proc, #call] :unless gate that must evaluate falsy
|
|
48
|
+
# @return [Callbacks] self for chaining
|
|
49
|
+
# @raise [ArgumentError] when both `callable` and a block are given, when the
|
|
50
|
+
# callback type is invalid, or when `event` is unknown
|
|
51
|
+
# @yield the callback body
|
|
52
|
+
def register(event, callable = nil, **options, &block)
|
|
53
|
+
callback = callable || block
|
|
54
|
+
|
|
55
|
+
if callable && block
|
|
56
|
+
raise ArgumentError, "provide either a callable or a block, not both"
|
|
57
|
+
elsif !callback.is_a?(Symbol) && !callback.respond_to?(:call)
|
|
58
|
+
raise ArgumentError, "callback must be a Symbol or respond to #call"
|
|
59
|
+
elsif !EVENTS.include?(event)
|
|
60
|
+
raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
(registry[event] ||= []) << [callback, options.freeze]
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Drops callbacks registered for `event`. With no `callable`, removes
|
|
68
|
+
# every callback for `event`. With a `callable`, removes only the
|
|
69
|
+
# entries whose callback matches `callable` by `==` (works for Symbol
|
|
70
|
+
# method names, classes/modules, and any callable held by reference).
|
|
71
|
+
# When the last entry for `event` is removed, the key itself is dropped.
|
|
72
|
+
#
|
|
73
|
+
# @param event [Symbol] one of {EVENTS}
|
|
74
|
+
# @param callable [Symbol, #call, nil] optional specific callback to remove
|
|
75
|
+
# @return [Callbacks] self for chaining
|
|
76
|
+
# @raise [ArgumentError] when `event` is unknown
|
|
77
|
+
def deregister(event, callable = nil)
|
|
78
|
+
raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}" unless EVENTS.include?(event)
|
|
79
|
+
|
|
80
|
+
if callable.nil?
|
|
81
|
+
registry.delete(event)
|
|
82
|
+
elsif (entries = registry[event])
|
|
83
|
+
entries.reject! { |cb, _opts| cb == callable }
|
|
84
|
+
registry.delete(event) if entries.empty?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [Boolean]
|
|
91
|
+
def empty?
|
|
92
|
+
registry.empty?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @return [Integer] number of distinct events with callbacks
|
|
96
|
+
def size
|
|
97
|
+
registry.size
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @return [Integer] total callbacks across all events
|
|
101
|
+
def count
|
|
102
|
+
registry.each_value.sum(&:size)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Fires each callback registered for `event` against `task`. Skips any
|
|
106
|
+
# callback whose `:if`/`:unless` gates fail.
|
|
107
|
+
#
|
|
108
|
+
# @param event [Symbol]
|
|
109
|
+
# @param task [Task]
|
|
110
|
+
# @return [void]
|
|
111
|
+
# @raise [ArgumentError] when a callback is neither a Symbol nor responds to `#call`
|
|
112
|
+
def process(event, task)
|
|
113
|
+
callbacks = registry[event]
|
|
114
|
+
return if callbacks.nil? || callbacks.empty?
|
|
115
|
+
|
|
116
|
+
callbacks.each do |callable, options|
|
|
117
|
+
next unless Util.satisfied?(options[:if], options[:unless], task)
|
|
118
|
+
|
|
119
|
+
invoke(callable, task)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Wraps `block` with every callback registered for `event` as a nested
|
|
124
|
+
# chain (outer-first by declaration order). Each callback receives a
|
|
125
|
+
# continuation it must invoke exactly once: Symbol callbacks get it as
|
|
126
|
+
# their block (use `yield`); Procs/blocks are `instance_exec`'d on the
|
|
127
|
+
# task with `(task, continuation)`; arbitrary callables receive
|
|
128
|
+
# `(task, continuation)`. Gates skip individual links silently while
|
|
129
|
+
# still running the body.
|
|
130
|
+
#
|
|
131
|
+
# @param event [Symbol]
|
|
132
|
+
# @param task [Task]
|
|
133
|
+
# @param body [#call] inner continuation (Runtime lifecycle body)
|
|
134
|
+
# @yield the innermost link — the lifecycle body to wrap
|
|
135
|
+
# @return [void]
|
|
136
|
+
# @raise [CallbackError] when a callback fails to invoke its continuation
|
|
137
|
+
def around(event, task, &body)
|
|
138
|
+
callbacks = registry[event]
|
|
139
|
+
return yield if callbacks.nil? || callbacks.empty?
|
|
140
|
+
|
|
141
|
+
callbacks.reverse_each.reduce(body) do |succ, (callable, options)|
|
|
142
|
+
lambda do
|
|
143
|
+
next succ.call unless Util.satisfied?(options[:if], options[:unless], task)
|
|
144
|
+
|
|
145
|
+
called = false
|
|
146
|
+
cont = lambda do
|
|
147
|
+
called = true
|
|
148
|
+
succ.call
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
invoke(callable, task, cont, &cont)
|
|
152
|
+
|
|
153
|
+
called || raise(CallbackError, "#{event} callback did not invoke its continuation")
|
|
154
|
+
end
|
|
155
|
+
end.call
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# @param callable [Symbol, Proc, #call]
|
|
161
|
+
# @param task [Task]
|
|
162
|
+
# @param extras [Array<Object>] extra args after `task` for continuation-style callbacks
|
|
163
|
+
# @return [Object] the callback's return value
|
|
164
|
+
# @raise [ArgumentError] when `callable` is invalid
|
|
165
|
+
def invoke(callable, task, *extras, &)
|
|
166
|
+
case callable
|
|
167
|
+
when Symbol
|
|
168
|
+
task.send(callable, &)
|
|
169
|
+
when Proc
|
|
170
|
+
task.instance_exec(task, *extras, &callable)
|
|
171
|
+
else
|
|
172
|
+
return callable.call(task, *extras) if callable.respond_to?(:call)
|
|
173
|
+
|
|
174
|
+
raise ArgumentError, "callback must be a Symbol, Proc, or respond to #call"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
end
|
|
179
|
+
end
|