cmdx 2.1.1 → 2.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03bacf5f39e3f6588988b533606f53a90cdba94beef7bc2c9303e1e00c53a989
4
- data.tar.gz: 02f9e8a212ace89b6d9e7b134a9b92b469ce049b553876506e5d6786bede830e
3
+ metadata.gz: f8adbc69a3e91974b801b1f11c404a12b12bdcfcfd3f3d947a8cb3c730b7a934
4
+ data.tar.gz: e0daecd4316350294630dc58f1a05bf1c0fde6203360981b156e468504108a29
5
5
  SHA512:
6
- metadata.gz: 4d76b548878d4b78ed131a18804c4810d928fa99dfa66cf31723a475e89034ccac16451ed62e1be7880be491911eaef31b215c9540d2a7e4b864b716ac5a8cf7
7
- data.tar.gz: 107083dfef5796ed6f628b662d1920bb0e7028e0ea9a56ecaf63d979f261c248d6ab113c3881079522352abe97fdde451d8e741403a77f1b38ea2b082d003179
6
+ metadata.gz: 93022dbeef63aab15d43cc0902d97dc0e0e41aa72edfc2ecc00d57b7d103e0a2518271b4fa5f0cd0f4e037d39d9bda5a2c07210c24a76996bfbc830785f521fb
7
+ data.tar.gz: 57a130066332c5bc60645c3dff28eb5909711c18eeb54d304961ba4787543d270773e52161f5a95dca988a971972d80de18547c1f2d59a5e6cd7f39342f28c69
data/CHANGELOG.md CHANGED
@@ -4,12 +4,21 @@ 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
- ## [2.x.x] - UNRELEASED
7
+ ## UNRELEASED
8
+
9
+ ## [2.2.0] - UNRELEASED
10
+
11
+ ### Added
12
+ - Post-work lifecycle callbacks (`after_execution`, every `on_*` hook) receive the finalized `Signal` as their second argument — `reason`, `cause`, `metadata`, `origin`, and `backtrace` are available before `Result` construction. Pre-work hooks and `around_execution` keep the single-`task` arity
13
+ - `Input#allow_nil` — permit `nil` after defaults without coercion, transform, or validation
14
+ - `Fault#detailed_message` — appends validation errors and propagation `origin` to the stdlib detailed message for IRB and debug formatters without changing `message`
15
+ - `Fault#inspect` — compact task/reason/errors summary for console debugging
8
16
 
9
17
  ## [2.1.1] - 2026-07-26
10
18
 
11
19
  ### Added
12
20
  - `Result#to_h` includes `:errors` (`Errors#to_h`) for structured validation messages in logs, telemetry, and JSON serialization
21
+ - Add `:decimal` coercion alias for `BigDecimal`
13
22
 
14
23
  ### Fixed
15
24
  - `Input` resolution continues coerce → transform → validate when the key is present (including explicit `nil`) instead of stopping after defaults; omitted keys still stop when the value is `nil` after defaults
@@ -25,6 +25,20 @@ module CMDx
25
25
  :on_ko
26
26
  ].freeze
27
27
 
28
+ # Events fired after `around_execution` (work, rollback) finishes. Runtime
29
+ # passes the finalized {Signal} as the second callback argument so hooks can
30
+ # read `reason`, `cause`, `metadata`, `origin`, and `backtrace`.
31
+ POST_WORK_EVENTS = Set[
32
+ :after_execution,
33
+ :on_complete,
34
+ :on_interrupted,
35
+ :on_success,
36
+ :on_skipped,
37
+ :on_failed,
38
+ :on_ok,
39
+ :on_ko
40
+ ].freeze
41
+
28
42
  attr_reader :registry
29
43
 
30
44
  def initialize
@@ -122,20 +136,26 @@ module CMDx
122
136
  # Fires each callback registered for `event` against `task`. Skips any
123
137
  # callback whose `:if`/`:unless` gates fail.
124
138
  #
139
+ # Post-work events ({POST_WORK_EVENTS}) receive the finalized `signal` as
140
+ # their second argument (Symbol method, Proc, or `#call`). Pre-work events
141
+ # and `around_execution` omit it.
142
+ #
125
143
  # @param event [Symbol]
126
144
  # @param task [Task]
145
+ # @param signal [Signal, nil] finalized halt token for post-work events
127
146
  # @return [void]
128
147
  # @raise [ArgumentError] when a callback is neither a Symbol nor responds to `#call`
129
- def process(event, task)
148
+ def process(event, task, signal = nil)
130
149
  return if empty?
131
150
 
132
151
  callbacks = registry[event]
133
152
  return if callbacks.nil? || callbacks.empty?
134
153
 
154
+ post_work = POST_WORK_EVENTS.include?(event)
135
155
  callbacks.each do |callable, options|
136
156
  next unless Util.satisfied?(options[:if], options[:unless], task)
137
157
 
138
- invoke(callable, task)
158
+ invoke(callable, task, signal: post_work ? signal : nil)
139
159
  end
140
160
  end
141
161
 
@@ -167,7 +187,7 @@ module CMDx
167
187
  succ.call
168
188
  end
169
189
 
170
- invoke(callable, task, cont, &cont)
190
+ invoke(callable, task, continuation: cont, &cont)
171
191
 
172
192
  called || raise(CallbackError, <<~MSG.chomp)
173
193
  #{event} callback did not invoke its continuation.
@@ -179,14 +199,25 @@ module CMDx
179
199
 
180
200
  private
181
201
 
182
- def invoke(callable, task, *extras, &)
202
+ def invoke(callable, task, continuation: nil, signal: nil, &block)
183
203
  case callable
184
204
  when Symbol
185
- task.send(callable, &)
205
+ if signal
206
+ task.send(callable, signal, &block)
207
+ else
208
+ task.send(callable, &block)
209
+ end
186
210
  when Proc
187
- task.instance_exec(task, *extras, &callable)
211
+ extras = continuation ? [continuation] : []
212
+ if signal
213
+ task.instance_exec(task, signal, *extras, &callable)
214
+ else
215
+ task.instance_exec(task, *extras, &callable)
216
+ end
188
217
  else
189
- return callable.call(task, *extras) if callable.respond_to?(:call)
218
+ args = signal ? [task, signal] : [task]
219
+ args << continuation if continuation
220
+ return callable.call(*args) if callable.respond_to?(:call)
190
221
 
191
222
  raise ArgumentError, <<~MSG.chomp
192
223
  callback must be a Symbol, Proc, or respond to #call (got #{callable.class}).
@@ -22,6 +22,7 @@ module CMDx
22
22
  complex: Coercions::Complex,
23
23
  date: Coercions::Date,
24
24
  date_time: Coercions::DateTime,
25
+ decimal: Coercions::BigDecimal,
25
26
  float: Coercions::Float,
26
27
  hash: Coercions::Hash,
27
28
  integer: Coercions::Integer,
data/lib/cmdx/fault.rb CHANGED
@@ -124,5 +124,43 @@ module CMDx
124
124
  @result.chain
125
125
  end
126
126
 
127
+ # Enriches the stdlib detailed message with validation errors and
128
+ # propagation context for IRB and other debug formatters. {#message} is
129
+ # unchanged.
130
+ #
131
+ # @param highlight [Boolean] forwarded to `Exception#detailed_message`
132
+ # @param order [Symbol] forwarded to `Exception#detailed_message`
133
+ # @return [String]
134
+ def detailed_message(highlight: true, order: :top)
135
+ lines = [super]
136
+ append_errors_lines(lines)
137
+ append_origin_line(lines)
138
+ lines.join("\n")
139
+ end
140
+
141
+ # @return [String] human-readable representation
142
+ def inspect
143
+ "#<#{self.class} task=#{task.name} tid=#{result.tid} cid=#{result.cid} xid=#{result.xid} reason=#{result.reason.inspect} errors=#{result.errors.to_h}>"
144
+ end
145
+
146
+ private
147
+
148
+ def append_errors_lines(lines)
149
+ return if result.errors.empty?
150
+
151
+ lines << " errors:"
152
+ result.errors.each do |key, messages|
153
+ messages.each { |message| lines << " #{key}: #{message}" }
154
+ end
155
+ end
156
+
157
+ def append_origin_line(lines)
158
+ origin = result.origin
159
+ return unless origin
160
+
161
+ reason = origin.reason || "no reason"
162
+ lines << " origin: #{origin.task.name} (#{reason})"
163
+ end
164
+
127
165
  end
128
166
  end
data/lib/cmdx/input.rb CHANGED
@@ -22,6 +22,7 @@ module CMDx
22
22
  # @option options [Symbol, Proc, #call] :if
23
23
  # @option options [Symbol, Proc, #call] :unless
24
24
  # @option options [Boolean] :required
25
+ # @option options [Boolean] :allow_nil (see {#allow_nil})
25
26
  def initialize(name, children: EMPTY_ARRAY, **options)
26
27
  @name = name.to_sym
27
28
  @children = children.freeze
@@ -78,6 +79,14 @@ module CMDx
78
79
  @options.fetch(:required, false)
79
80
  end
80
81
 
82
+ # When `true`, a `nil` value after defaults skips coercion, transform, and
83
+ # validation. Per-validator `:allow_nil` only skips individual rules.
84
+ #
85
+ # @return [Boolean]
86
+ def allow_nil
87
+ @options.fetch(:allow_nil, false)
88
+ end
89
+
81
90
  # Computed accessor/reader method name. Uses `:as` when provided,
82
91
  # otherwise combines `:prefix`, `name`, and `:suffix` around the source.
83
92
  #
@@ -131,7 +140,8 @@ module CMDx
131
140
  # @note "Required" here means "the key is present in the source"; an
132
141
  # explicit `nil` under an existing key satisfies the required check
133
142
  # and is then routed through `:default`. Combine with `:presence` /
134
- # `:validate` to reject explicit `nil` values.
143
+ # `:validate` to reject explicit `nil` values, or `:allow_nil` (see
144
+ # {#allow_nil}) to leave `nil` unresolved.
135
145
  #
136
146
  # @param task [Task]
137
147
  # @return [Object, nil] the resolved value (`nil` on failure)
@@ -189,7 +199,7 @@ module CMDx
189
199
  end
190
200
 
191
201
  value = apply_default(task) if value.nil?
192
- return if value.nil? && !key_provided
202
+ return if value.nil? && (!key_provided || allow_nil)
193
203
 
194
204
  @coercions ||= task.class.coercions.extract(@options)
195
205
  value = task.class.coercions.coerce(task, accessor_name, value, @coercions)
data/lib/cmdx/inputs.rb CHANGED
@@ -35,6 +35,7 @@ module CMDx
35
35
  # @option options [Symbol, Proc, #call] :if
36
36
  # @option options [Symbol, Proc, #call] :unless
37
37
  # @option options [Boolean] :required
38
+ # @option options [Boolean] :allow_nil (see {Input#allow_nil})
38
39
  # @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
39
40
  # @option options [Object] :validate forwarded with declaration (see {Validators#extract})
40
41
  # @return [Inputs] self for chaining
@@ -138,6 +139,7 @@ module CMDx
138
139
  # @option options [Symbol, Proc, #call] :if
139
140
  # @option options [Symbol, Proc, #call] :unless
140
141
  # @option options [Boolean] :required
142
+ # @option options [Boolean] :allow_nil (see {Input#allow_nil})
141
143
  # @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
142
144
  # @option options [Object] :validate forwarded with declaration (see {Validators#extract})
143
145
  # @yield nested child input DSL
@@ -162,6 +164,7 @@ module CMDx
162
164
  # @option options [Symbol, Proc, #call] :transform mutator applied after coercion
163
165
  # @option options [Symbol, Proc, #call] :if
164
166
  # @option options [Symbol, Proc, #call] :unless
167
+ # @option options [Boolean] :allow_nil (see {Input#allow_nil})
165
168
  # @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
166
169
  # @option options [Object] :validate forwarded with declaration (see {Validators#extract})
167
170
  # @yield nested child input DSL
@@ -185,6 +188,7 @@ module CMDx
185
188
  # @option options [Symbol, Proc, #call] :transform mutator applied after coercion
186
189
  # @option options [Symbol, Proc, #call] :if
187
190
  # @option options [Symbol, Proc, #call] :unless
191
+ # @option options [Boolean] :allow_nil (see {Input#allow_nil})
188
192
  # @option options [Object] :coerce forwarded with declaration (see {Coercions#extract})
189
193
  # @option options [Object] :validate forwarded with declaration (see {Validators#extract})
190
194
  # @yield nested child input DSL
data/lib/cmdx/runtime.rb CHANGED
@@ -156,7 +156,8 @@ module CMDx
156
156
  callbacks = @task.class.callbacks
157
157
  return if callbacks.empty?
158
158
 
159
- callbacks.process(event, @task)
159
+ signal = Callbacks::POST_WORK_EVENTS.include?(event) ? @signal : nil
160
+ callbacks.process(event, @task, signal)
160
161
  end
161
162
 
162
163
  def run_around(event, &)
data/lib/cmdx/task.rb CHANGED
@@ -274,6 +274,7 @@ module CMDx
274
274
  # @option options [Symbol, Proc, #call] :if
275
275
  # @option options [Symbol, Proc, #call] :unless
276
276
  # @option options [Boolean] :required
277
+ # @option options [Boolean] :allow_nil (see {Input#allow_nil})
277
278
  # @option options [Object] :coerce (see {Coercions#extract})
278
279
  # @option options [Object] :validate (see {Validators#extract})
279
280
  # @yield nested-input DSL block (see {Inputs::ChildBuilder})
@@ -307,6 +308,7 @@ module CMDx
307
308
  # @option options [Symbol, Proc, #call] :transform mutator applied after coercion
308
309
  # @option options [Symbol, Proc, #call] :if
309
310
  # @option options [Symbol, Proc, #call] :unless
311
+ # @option options [Boolean] :allow_nil (see {Input#allow_nil})
310
312
  # @option options [Object] :coerce (see {Coercions#extract})
311
313
  # @option options [Object] :validate (see {Validators#extract})
312
314
  # @yield nested-input DSL block (see {Inputs::ChildBuilder})
@@ -329,6 +331,7 @@ module CMDx
329
331
  # @option options [Symbol, Proc, #call] :transform mutator applied after coercion
330
332
  # @option options [Symbol, Proc, #call] :if
331
333
  # @option options [Symbol, Proc, #call] :unless
334
+ # @option options [Boolean] :allow_nil (see {Input#allow_nil})
332
335
  # @option options [Object] :coerce (see {Coercions#extract})
333
336
  # @option options [Object] :validate (see {Validators#extract})
334
337
  # @yield nested-input DSL block (see {Inputs::ChildBuilder})
data/lib/cmdx/version.rb CHANGED
@@ -3,6 +3,6 @@
3
3
  module CMDx
4
4
 
5
5
  # Gem version. Bumped on release; mirrored in the gemspec.
6
- VERSION = "2.1.1"
6
+ VERSION = "2.2.0"
7
7
 
8
8
  end
@@ -79,7 +79,8 @@ CMDx.configure do |config|
79
79
  # Callbacks
80
80
  # https://drexed.github.io/cmdx/configuration/#callbacks
81
81
  # ===========================================================================
82
- # Hook into a task's lifecycle. Each callback receives the task instance.
82
+ # Hook into a task's lifecycle. Pre-work hooks receive the task; post-work
83
+ # hooks (`after_execution`, every `on_*`) also receive the finalized Signal.
83
84
  #
84
85
  # Available events:
85
86
  # :before_execution, :before_validation,
@@ -88,8 +89,8 @@ CMDx.configure do |config|
88
89
  # :on_success, :on_skipped, :on_failed,
89
90
  # :on_ok, :on_ko
90
91
  #
91
- # config.callbacks.register(:on_failed, proc do |task|
92
- # Rails.logger.error("[cmdx] #{task.class.name} failed: #{task.metadata[:reason]}")
92
+ # config.callbacks.register(:on_failed, proc do |task, signal|
93
+ # Rails.logger.error("[cmdx] #{task.class.name} failed: #{signal.reason}")
93
94
  # end)
94
95
 
95
96
  # ===========================================================================
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cmdx
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juan Gomez
@@ -312,7 +312,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
312
312
  - !ruby/object:Gem::Version
313
313
  version: '0'
314
314
  requirements: []
315
- rubygems_version: 4.0.13
315
+ rubygems_version: 4.0.17
316
316
  specification_version: 4
317
317
  summary: CMDx is a framework for building maintainable business processes.
318
318
  test_files: []