cmdx 0.4.0 → 1.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/.DS_Store +0 -0
- data/.cursor/rules/cursor-instructions.mdc +6 -0
- data/.rubocop.yml +16 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +42 -1
- data/README.md +72 -25
- data/docs/ai_prompts.md +309 -0
- data/docs/basics/call.md +225 -14
- data/docs/basics/chain.md +271 -0
- data/docs/basics/context.md +232 -33
- data/docs/basics/setup.md +76 -12
- data/docs/callbacks.md +273 -0
- data/docs/configuration.md +158 -28
- data/docs/getting_started.md +134 -22
- data/docs/interruptions/exceptions.md +189 -11
- data/docs/interruptions/faults.md +187 -44
- data/docs/interruptions/halt.md +179 -35
- data/docs/logging.md +194 -53
- data/docs/middlewares.md +735 -0
- data/docs/outcomes/result.md +296 -10
- data/docs/outcomes/states.md +212 -19
- data/docs/outcomes/statuses.md +284 -18
- data/docs/parameters/coercions.md +402 -29
- data/docs/parameters/defaults.md +249 -25
- data/docs/parameters/definitions.md +238 -72
- data/docs/parameters/namespacing.md +250 -27
- data/docs/parameters/validations.md +193 -168
- data/docs/testing.md +550 -0
- data/docs/tips_and_tricks.md +95 -43
- data/docs/workflows.md +319 -0
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +69 -0
- data/lib/cmdx/callback_registry.rb +106 -0
- data/lib/cmdx/chain.rb +190 -0
- data/lib/cmdx/chain_inspector.rb +149 -0
- data/lib/cmdx/chain_serializer.rb +175 -0
- data/lib/cmdx/coercions/array.rb +37 -0
- data/lib/cmdx/coercions/big_decimal.rb +33 -0
- data/lib/cmdx/coercions/boolean.rb +41 -1
- data/lib/cmdx/coercions/complex.rb +31 -0
- data/lib/cmdx/coercions/date.rb +39 -0
- data/lib/cmdx/coercions/date_time.rb +39 -0
- data/lib/cmdx/coercions/float.rb +31 -0
- data/lib/cmdx/coercions/hash.rb +42 -0
- data/lib/cmdx/coercions/integer.rb +32 -0
- data/lib/cmdx/coercions/rational.rb +31 -0
- data/lib/cmdx/coercions/string.rb +31 -0
- data/lib/cmdx/coercions/time.rb +39 -0
- data/lib/cmdx/coercions/virtual.rb +31 -0
- data/lib/cmdx/configuration.rb +217 -9
- data/lib/cmdx/context.rb +173 -2
- data/lib/cmdx/core_ext/hash.rb +72 -0
- data/lib/cmdx/core_ext/module.rb +94 -0
- data/lib/cmdx/core_ext/object.rb +105 -0
- data/lib/cmdx/correlator.rb +217 -0
- data/lib/cmdx/error.rb +210 -8
- data/lib/cmdx/errors.rb +256 -1
- data/lib/cmdx/fault.rb +177 -2
- data/lib/cmdx/faults.rb +158 -2
- data/lib/cmdx/immutator.rb +121 -2
- data/lib/cmdx/lazy_struct.rb +261 -18
- data/lib/cmdx/log_formatters/json.rb +46 -0
- data/lib/cmdx/log_formatters/key_value.rb +46 -0
- data/lib/cmdx/log_formatters/line.rb +54 -0
- data/lib/cmdx/log_formatters/logstash.rb +64 -0
- data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
- data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
- data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
- data/lib/cmdx/log_formatters/raw.rb +54 -0
- data/lib/cmdx/logger.rb +85 -0
- data/lib/cmdx/logger_ansi.rb +93 -7
- data/lib/cmdx/logger_serializer.rb +116 -0
- data/lib/cmdx/middleware.rb +74 -0
- data/lib/cmdx/middleware_registry.rb +106 -0
- data/lib/cmdx/middlewares/correlate.rb +266 -0
- data/lib/cmdx/middlewares/timeout.rb +232 -0
- data/lib/cmdx/parameter.rb +228 -1
- data/lib/cmdx/parameter_inspector.rb +61 -0
- data/lib/cmdx/parameter_registry.rb +125 -0
- data/lib/cmdx/parameter_serializer.rb +83 -0
- data/lib/cmdx/parameter_validator.rb +62 -0
- data/lib/cmdx/parameter_value.rb +109 -1
- data/lib/cmdx/parameters_inspector.rb +59 -0
- data/lib/cmdx/parameters_serializer.rb +102 -0
- data/lib/cmdx/railtie.rb +123 -3
- data/lib/cmdx/result.rb +399 -20
- data/lib/cmdx/result_ansi.rb +105 -9
- data/lib/cmdx/result_inspector.rb +76 -0
- data/lib/cmdx/result_logger.rb +90 -3
- data/lib/cmdx/result_serializer.rb +137 -0
- data/lib/cmdx/rspec/result_matchers.rb +917 -0
- data/lib/cmdx/rspec/task_matchers.rb +570 -0
- data/lib/cmdx/task.rb +409 -34
- data/lib/cmdx/task_serializer.rb +74 -2
- data/lib/cmdx/utils/ansi_color.rb +95 -0
- data/lib/cmdx/utils/log_timestamp.rb +48 -0
- data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
- data/lib/cmdx/utils/name_affix.rb +78 -0
- data/lib/cmdx/validators/custom.rb +82 -0
- data/lib/cmdx/validators/exclusion.rb +94 -0
- data/lib/cmdx/validators/format.rb +102 -8
- data/lib/cmdx/validators/inclusion.rb +104 -0
- data/lib/cmdx/validators/length.rb +128 -0
- data/lib/cmdx/validators/numeric.rb +128 -0
- data/lib/cmdx/validators/presence.rb +93 -7
- data/lib/cmdx/version.rb +7 -1
- data/lib/cmdx/workflow.rb +394 -0
- data/lib/cmdx.rb +25 -64
- data/lib/generators/cmdx/install_generator.rb +37 -1
- data/lib/generators/cmdx/task_generator.rb +69 -1
- data/lib/generators/cmdx/templates/install.rb +8 -12
- data/lib/generators/cmdx/workflow_generator.rb +109 -0
- metadata +54 -15
- data/docs/basics/run.md +0 -34
- data/docs/batch.md +0 -53
- data/docs/example.md +0 -82
- data/docs/hooks.md +0 -59
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -34
- data/lib/cmdx/run.rb +0 -38
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -16
- data/lib/cmdx/task_hook.rb +0 -18
- data/lib/generators/cmdx/batch_generator.rb +0 -30
- /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/lib/cmdx/railtie.rb
CHANGED
@@ -1,12 +1,102 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
+
##
|
5
|
+
# Railtie provides seamless integration between CMDx and Ruby on Rails applications.
|
6
|
+
# It automatically configures Rails-specific features including internationalization,
|
7
|
+
# autoloading paths, and directory structure conventions for CMDx tasks and workflows.
|
8
|
+
#
|
9
|
+
# The Railtie handles two main integration aspects:
|
10
|
+
# 1. **I18n Configuration**: Automatically loads CMDx locale files for available locales
|
11
|
+
# 2. **Autoloading Setup**: Configures Rails autoloaders for CMDx command objects
|
12
|
+
#
|
13
|
+
# ## Directory Structure
|
14
|
+
#
|
15
|
+
# The Railtie expects CMDx command objects to be organized in the following structure:
|
16
|
+
# ```
|
17
|
+
# app/
|
18
|
+
# cmds/
|
19
|
+
# workflows/ # Workflow command objects
|
20
|
+
# order_processing_workflow.rb
|
21
|
+
# tasks/ # Task command objects
|
22
|
+
# process_order_task.rb
|
23
|
+
# send_email_task.rb
|
24
|
+
# ```
|
25
|
+
#
|
26
|
+
# ## Automatic Features
|
27
|
+
#
|
28
|
+
# When CMDx is included in a Rails application, the Railtie automatically:
|
29
|
+
# - Adds `app/cmds` to Rails autoload paths
|
30
|
+
# - Configures autoloader to collapse `app/cmds/workflows` and `app/cmds/tasks` directories
|
31
|
+
# - Loads appropriate locale files from CMDx gem for error messages and validations
|
32
|
+
# - Reloads I18n configuration to include CMDx translations
|
33
|
+
#
|
34
|
+
# @example Rails application structure
|
35
|
+
# # app/cmds/tasks/process_order_task.rb
|
36
|
+
# class ProcessOrderTask < CMDx::Task
|
37
|
+
# required :order_id, type: :integer
|
38
|
+
#
|
39
|
+
# def call
|
40
|
+
# context.order = Order.find(order_id)
|
41
|
+
# context.order.process!
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# @example Using in Rails controllers
|
46
|
+
# class OrdersController < ApplicationController
|
47
|
+
# def process
|
48
|
+
# result = ProcessOrderTask.call(order_id: params[:id])
|
49
|
+
#
|
50
|
+
# if result.success?
|
51
|
+
# redirect_to order_path(result.context.order), notice: 'Order processed!'
|
52
|
+
# else
|
53
|
+
# redirect_to order_path(params[:id]), alert: result.metadata[:reason]
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# @example I18n integration
|
59
|
+
# # CMDx automatically loads locale files for validation messages
|
60
|
+
# # en.yml, es.yml, etc. are automatically available
|
61
|
+
# result = MyTask.call(invalid_param: nil)
|
62
|
+
# result.errors.full_messages # Uses localized error messages
|
63
|
+
#
|
64
|
+
# @see Configuration Configuration options for Rails integration
|
65
|
+
# @see Task Task base class for command objects
|
66
|
+
# @see Workflow Workflow base class for multi-task operations
|
67
|
+
# @since 1.0.0
|
4
68
|
class Railtie < Rails::Railtie
|
5
69
|
|
6
|
-
CONCEPTS = %w[batches tasks].freeze
|
7
|
-
|
8
70
|
railtie_name :cmdx
|
9
71
|
|
72
|
+
##
|
73
|
+
# Configures internationalization (I18n) for CMDx in Rails applications.
|
74
|
+
# Automatically loads locale files from the CMDx gem for all configured
|
75
|
+
# application locales, ensuring error messages and validations are properly localized.
|
76
|
+
#
|
77
|
+
# This initializer:
|
78
|
+
# 1. Iterates through all configured application locales
|
79
|
+
# 2. Checks for corresponding CMDx locale files
|
80
|
+
# 3. Adds found locale files to I18n load path
|
81
|
+
# 4. Reloads I18n configuration
|
82
|
+
#
|
83
|
+
# @param app [Rails::Application] the Rails application instance
|
84
|
+
# @return [void]
|
85
|
+
#
|
86
|
+
# @example Available locales
|
87
|
+
# # If Rails app has config.i18n.available_locales = [:en, :es]
|
88
|
+
# # This will load:
|
89
|
+
# # - lib/locales/en.yml (CMDx English translations)
|
90
|
+
# # - lib/locales/es.yml (CMDx Spanish translations)
|
91
|
+
#
|
92
|
+
# @example Localized error messages
|
93
|
+
# # With Spanish locale active
|
94
|
+
# class MyTask < CMDx::Task
|
95
|
+
# required :name, presence: true
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# result = MyTask.call(name: "")
|
99
|
+
# result.errors.full_messages # Returns Spanish error messages
|
10
100
|
initializer("cmdx.configure_locales") do |app|
|
11
101
|
Array(app.config.i18n.available_locales).each do |locale|
|
12
102
|
path = File.expand_path("../../../lib/locales/#{locale}.yml", __FILE__)
|
@@ -18,10 +108,40 @@ module CMDx
|
|
18
108
|
I18n.reload!
|
19
109
|
end
|
20
110
|
|
111
|
+
##
|
112
|
+
# Configures Rails autoloading for CMDx command objects.
|
113
|
+
# Sets up proper autoloading paths and directory collapsing to ensure
|
114
|
+
# CMDx tasks and workflows are loaded correctly in Rails applications.
|
115
|
+
#
|
116
|
+
# This initializer:
|
117
|
+
# 1. Adds `app/cmds` to Rails autoload paths
|
118
|
+
# 2. Configures all autoloaders to collapse concept directories
|
119
|
+
# 3. Ensures proper class name resolution for nested directories
|
120
|
+
#
|
121
|
+
# Directory collapsing means that files in `app/cmds/tasks/` will be loaded
|
122
|
+
# as if they were directly in `app/cmds/`, allowing for better organization
|
123
|
+
# without affecting class naming conventions.
|
124
|
+
#
|
125
|
+
# @param app [Rails::Application] the Rails application instance
|
126
|
+
# @return [void]
|
127
|
+
#
|
128
|
+
# @example Directory structure and class loading
|
129
|
+
# # File: app/cmds/tasks/process_order_task.rb
|
130
|
+
# # Class: ProcessOrderTask (not Tasks::ProcessOrderTask)
|
131
|
+
#
|
132
|
+
# # File: app/cmds/workflows/order_processing_workflow.rb
|
133
|
+
# # Class: OrderProcessingWorkflow (not Workflows::OrderProcessingWorkflow)
|
134
|
+
#
|
135
|
+
# @example Autoloading in action
|
136
|
+
# # Rails will automatically load these classes when referenced:
|
137
|
+
# ProcessOrderTask.call(order_id: 123) # Loads from app/cmds/tasks/
|
138
|
+
# OrderProcessingWorkflow.call(orders: [...]) # Loads from app/cmds/workflows/
|
21
139
|
initializer("cmdx.configure_rails_auto_load_paths") do |app|
|
22
140
|
app.config.autoload_paths += %w[app/cmds]
|
141
|
+
|
142
|
+
types = %w[workflows tasks]
|
23
143
|
app.autoloaders.each do |autoloader|
|
24
|
-
|
144
|
+
types.each do |concept|
|
25
145
|
dir = app.root.join("app/cmds/#{concept}")
|
26
146
|
autoloader.collapse(dir)
|
27
147
|
end
|
data/lib/cmdx/result.rb
CHANGED
@@ -1,14 +1,64 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module CMDx
|
4
|
+
# Result object representing the outcome of task execution.
|
5
|
+
#
|
6
|
+
# The Result class encapsulates all information about a task's execution,
|
7
|
+
# including its state, status, metadata, and runtime information. It provides
|
8
|
+
# a comprehensive interface for tracking task lifecycle, handling failures,
|
9
|
+
# and chaining execution outcomes.
|
10
|
+
#
|
11
|
+
# @example Basic result usage
|
12
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
13
|
+
# result.success? # => true
|
14
|
+
# result.complete? # => true
|
15
|
+
# result.runtime # => 0.5
|
16
|
+
#
|
17
|
+
# @example Result with failure handling
|
18
|
+
# result = ProcessOrderTask.call(invalid_params)
|
19
|
+
# result.failed? # => true
|
20
|
+
# result.bad? # => true
|
21
|
+
# result.metadata # => { reason: "Invalid parameters" }
|
22
|
+
#
|
23
|
+
# @example Result state callbacks
|
24
|
+
# ProcessOrderTask.call(order_id: 123)
|
25
|
+
# .on_success { |result| logger.info "Order processed successfully" }
|
26
|
+
# .on_failed { |result| logger.error "Order processing failed: #{result.metadata[:reason]}" }
|
27
|
+
#
|
28
|
+
# @example Result chaining and failure propagation
|
29
|
+
# result1 = FirstTask.call
|
30
|
+
# result2 = SecondTask.call
|
31
|
+
# result2.throw!(result1) if result1.failed? # Propagate failure
|
32
|
+
#
|
33
|
+
# @see CMDx::Task Task execution and result creation
|
34
|
+
# @see CMDx::Chain Chain execution context and result tracking
|
35
|
+
# @see CMDx::Fault Fault handling for result failures
|
4
36
|
class Result
|
5
37
|
|
6
|
-
__cmdx_attr_delegator :context, :
|
38
|
+
__cmdx_attr_delegator :context, :chain,
|
39
|
+
to: :task
|
7
40
|
|
41
|
+
# @return [CMDx::Task] The task instance that generated this result
|
42
|
+
# @return [String] The current execution state (initialized, executing, complete, interrupted)
|
43
|
+
# @return [String] The current execution status (success, skipped, failed)
|
44
|
+
# @return [Hash] Additional metadata associated with the result
|
8
45
|
attr_reader :task, :state, :status, :metadata
|
9
46
|
|
47
|
+
# Initializes a new Result instance.
|
48
|
+
#
|
49
|
+
# Creates a result object for tracking task execution outcomes.
|
50
|
+
# Results start in initialized state with success status.
|
51
|
+
#
|
52
|
+
# @param task [CMDx::Task] The task instance this result belongs to
|
53
|
+
# @raise [TypeError] If task is not a Task or Workflow instance
|
54
|
+
#
|
55
|
+
# @example Creating a result
|
56
|
+
# task = ProcessOrderTask.new
|
57
|
+
# result = Result.new(task)
|
58
|
+
# result.initialized? # => true
|
59
|
+
# result.success? # => true
|
10
60
|
def initialize(task)
|
11
|
-
raise
|
61
|
+
raise TypeError, "must be a Task or Workflow" unless task.is_a?(Task)
|
12
62
|
|
13
63
|
@task = task
|
14
64
|
@state = INITIALIZED
|
@@ -16,26 +66,87 @@ module CMDx
|
|
16
66
|
@metadata = {}
|
17
67
|
end
|
18
68
|
|
69
|
+
# Available execution states for task results.
|
70
|
+
#
|
71
|
+
# States represent the execution lifecycle of a task from initialization
|
72
|
+
# through completion or interruption.
|
19
73
|
STATES = [
|
20
|
-
INITIALIZED = "initialized",
|
21
|
-
EXECUTING = "executing",
|
22
|
-
COMPLETE = "complete",
|
23
|
-
INTERRUPTED = "interrupted"
|
74
|
+
INITIALIZED = "initialized", # Initial state before execution
|
75
|
+
EXECUTING = "executing", # Currently executing task logic
|
76
|
+
COMPLETE = "complete", # Successfully completed execution
|
77
|
+
INTERRUPTED = "interrupted" # Execution was halted due to failure
|
24
78
|
].freeze
|
25
79
|
|
80
|
+
# Dynamically defines state predicate and callback methods.
|
81
|
+
#
|
82
|
+
# For each state, creates:
|
83
|
+
# - Predicate method (e.g., `executing?`)
|
84
|
+
# - Callback method (e.g., `on_executing`)
|
26
85
|
STATES.each do |s|
|
27
86
|
# eg: executing?
|
28
87
|
define_method(:"#{s}?") { state == s }
|
88
|
+
|
89
|
+
# eg: on_interrupted { ... }
|
90
|
+
define_method(:"on_#{s}") do |&block|
|
91
|
+
raise ArgumentError, "block required" unless block
|
92
|
+
|
93
|
+
block.call(self) if send(:"#{s}?")
|
94
|
+
self
|
95
|
+
end
|
29
96
|
end
|
30
97
|
|
98
|
+
# Marks the result as executed based on current status.
|
99
|
+
#
|
100
|
+
# Transitions to complete state if successful, or interrupted state
|
101
|
+
# if the task has failed or been skipped.
|
102
|
+
#
|
103
|
+
# @return [void]
|
104
|
+
#
|
105
|
+
# @example Successful execution
|
106
|
+
# result.executed!
|
107
|
+
# result.complete? # => true (if status was success)
|
108
|
+
#
|
109
|
+
# @example Failed execution
|
110
|
+
# result.fail!(reason: "Something went wrong")
|
111
|
+
# result.executed!
|
112
|
+
# result.interrupted? # => true
|
31
113
|
def executed!
|
32
114
|
success? ? complete! : interrupt!
|
33
115
|
end
|
34
116
|
|
117
|
+
# Checks if the result has been executed (completed or interrupted).
|
118
|
+
#
|
119
|
+
# @return [Boolean] true if result is complete or interrupted
|
120
|
+
#
|
121
|
+
# @example
|
122
|
+
# result.executed? # => true if complete? || interrupted?
|
35
123
|
def executed?
|
36
124
|
complete? || interrupted?
|
37
125
|
end
|
38
126
|
|
127
|
+
# Executes a callback if the result has been executed.
|
128
|
+
#
|
129
|
+
# @yield [Result] The result instance
|
130
|
+
# @return [Result] Self for method chaining
|
131
|
+
# @raise [ArgumentError] If no block is provided
|
132
|
+
#
|
133
|
+
# @example
|
134
|
+
# result.on_executed { |r| logger.info "Task finished: #{r.status}" }
|
135
|
+
def on_executed(&)
|
136
|
+
raise ArgumentError, "block required" unless block_given?
|
137
|
+
|
138
|
+
yield(self) if executed?
|
139
|
+
self
|
140
|
+
end
|
141
|
+
|
142
|
+
# Transitions the result to executing state.
|
143
|
+
#
|
144
|
+
# @return [void]
|
145
|
+
# @raise [RuntimeError] If not transitioning from initialized state
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# result.executing!
|
149
|
+
# result.executing? # => true
|
39
150
|
def executing!
|
40
151
|
return if executing?
|
41
152
|
|
@@ -44,6 +155,14 @@ module CMDx
|
|
44
155
|
@state = EXECUTING
|
45
156
|
end
|
46
157
|
|
158
|
+
# Transitions the result to complete state.
|
159
|
+
#
|
160
|
+
# @return [void]
|
161
|
+
# @raise [RuntimeError] If not transitioning from executing state
|
162
|
+
#
|
163
|
+
# @example
|
164
|
+
# result.complete!
|
165
|
+
# result.complete? # => true
|
47
166
|
def complete!
|
48
167
|
return if complete?
|
49
168
|
|
@@ -52,6 +171,14 @@ module CMDx
|
|
52
171
|
@state = COMPLETE
|
53
172
|
end
|
54
173
|
|
174
|
+
# Transitions the result to interrupted state.
|
175
|
+
#
|
176
|
+
# @return [void]
|
177
|
+
# @raise [RuntimeError] If trying to interrupt from complete state
|
178
|
+
#
|
179
|
+
# @example
|
180
|
+
# result.interrupt!
|
181
|
+
# result.interrupted? # => true
|
55
182
|
def interrupt!
|
56
183
|
return if interrupted?
|
57
184
|
|
@@ -60,25 +187,99 @@ module CMDx
|
|
60
187
|
@state = INTERRUPTED
|
61
188
|
end
|
62
189
|
|
190
|
+
# Available execution statuses for task results.
|
191
|
+
#
|
192
|
+
# Statuses represent the outcome of task logic execution.
|
63
193
|
STATUSES = [
|
64
|
-
SUCCESS = "success",
|
65
|
-
SKIPPED = "skipped",
|
66
|
-
FAILED = "failed"
|
194
|
+
SUCCESS = "success", # Task completed successfully
|
195
|
+
SKIPPED = "skipped", # Task was skipped intentionally
|
196
|
+
FAILED = "failed" # Task failed due to error or validation
|
67
197
|
].freeze
|
68
198
|
|
199
|
+
# Dynamically defines status predicate and callback methods.
|
200
|
+
#
|
201
|
+
# For each status, creates:
|
202
|
+
# - Predicate method (e.g., `success?`)
|
203
|
+
# - Callback method (e.g., `on_success`)
|
69
204
|
STATUSES.each do |s|
|
70
205
|
# eg: skipped?
|
71
206
|
define_method(:"#{s}?") { status == s }
|
207
|
+
|
208
|
+
# eg: on_failed { ... }
|
209
|
+
define_method(:"on_#{s}") do |&block|
|
210
|
+
raise ArgumentError, "block required" unless block
|
211
|
+
|
212
|
+
block.call(self) if send(:"#{s}?")
|
213
|
+
self
|
214
|
+
end
|
72
215
|
end
|
73
216
|
|
217
|
+
# Checks if the result represents a good outcome (success or skipped).
|
218
|
+
#
|
219
|
+
# @return [Boolean] true if not failed
|
220
|
+
#
|
221
|
+
# @example
|
222
|
+
# result.good? # => true if success? || skipped?
|
74
223
|
def good?
|
75
224
|
!failed?
|
76
225
|
end
|
77
226
|
|
227
|
+
# Executes a callback if the result has a good outcome.
|
228
|
+
#
|
229
|
+
# @yield [Result] The result instance
|
230
|
+
# @return [Result] Self for method chaining
|
231
|
+
# @raise [ArgumentError] If no block is provided
|
232
|
+
#
|
233
|
+
# @example
|
234
|
+
# result.on_good { |r| logger.info "Task completed successfully" }
|
235
|
+
def on_good(&)
|
236
|
+
raise ArgumentError, "block required" unless block_given?
|
237
|
+
|
238
|
+
yield(self) if good?
|
239
|
+
self
|
240
|
+
end
|
241
|
+
|
242
|
+
# Checks if the result represents a bad outcome (skipped or failed).
|
243
|
+
#
|
244
|
+
# @return [Boolean] true if not successful
|
245
|
+
#
|
246
|
+
# @example
|
247
|
+
# result.bad? # => true if skipped? || failed?
|
78
248
|
def bad?
|
79
249
|
!success?
|
80
250
|
end
|
81
251
|
|
252
|
+
# Executes a callback if the result has a bad outcome.
|
253
|
+
#
|
254
|
+
# @yield [Result] The result instance
|
255
|
+
# @return [Result] Self for method chaining
|
256
|
+
# @raise [ArgumentError] If no block is provided
|
257
|
+
#
|
258
|
+
# @example
|
259
|
+
# result.on_bad { |r| logger.error "Task had issues: #{r.status}" }
|
260
|
+
def on_bad(&)
|
261
|
+
raise ArgumentError, "block required" unless block_given?
|
262
|
+
|
263
|
+
yield(self) if bad?
|
264
|
+
self
|
265
|
+
end
|
266
|
+
|
267
|
+
# Marks the result as skipped with optional metadata.
|
268
|
+
#
|
269
|
+
# Transitions from success to skipped status and halts execution
|
270
|
+
# unless the skip was caused by an original exception.
|
271
|
+
#
|
272
|
+
# @param metadata [Hash] Additional metadata about the skip
|
273
|
+
# @return [void]
|
274
|
+
# @raise [RuntimeError] If not transitioning from success status
|
275
|
+
# @raise [CMDx::Fault] If halting due to skip (unless original_exception present)
|
276
|
+
#
|
277
|
+
# @example Basic skip
|
278
|
+
# result.skip!(reason: "Order already processed")
|
279
|
+
# result.skipped? # => true
|
280
|
+
#
|
281
|
+
# @example Skip with exception context
|
282
|
+
# result.skip!(original_exception: StandardError.new("DB unavailable"))
|
82
283
|
def skip!(**metadata)
|
83
284
|
return if skipped?
|
84
285
|
|
@@ -90,6 +291,22 @@ module CMDx
|
|
90
291
|
halt! unless metadata[:original_exception]
|
91
292
|
end
|
92
293
|
|
294
|
+
# Marks the result as failed with optional metadata.
|
295
|
+
#
|
296
|
+
# Transitions from success to failed status and halts execution
|
297
|
+
# unless the failure was caused by an original exception.
|
298
|
+
#
|
299
|
+
# @param metadata [Hash] Additional metadata about the failure
|
300
|
+
# @return [void]
|
301
|
+
# @raise [RuntimeError] If not transitioning from success status
|
302
|
+
# @raise [CMDx::Fault] If halting due to failure (unless original_exception present)
|
303
|
+
#
|
304
|
+
# @example Basic failure
|
305
|
+
# result.fail!(reason: "Invalid order data", code: 422)
|
306
|
+
# result.failed? # => true
|
307
|
+
#
|
308
|
+
# @example Failure with exception context
|
309
|
+
# result.fail!(original_exception: StandardError.new("Validation failed"))
|
93
310
|
def fail!(**metadata)
|
94
311
|
return if failed?
|
95
312
|
|
@@ -101,14 +318,39 @@ module CMDx
|
|
101
318
|
halt! unless metadata[:original_exception]
|
102
319
|
end
|
103
320
|
|
321
|
+
# Halts execution by raising a fault if the result is not successful.
|
322
|
+
#
|
323
|
+
# @return [void]
|
324
|
+
# @raise [CMDx::Fault] If result status is not success
|
325
|
+
#
|
326
|
+
# @example
|
327
|
+
# result.fail!(reason: "Something went wrong")
|
328
|
+
# result.halt! # Raises CMDx::Fault
|
104
329
|
def halt!
|
105
330
|
return if success?
|
106
331
|
|
107
332
|
raise Fault.build(self)
|
108
333
|
end
|
109
334
|
|
335
|
+
# Propagates another result's failure status to this result.
|
336
|
+
#
|
337
|
+
# Copies the failure or skip status from another result, merging
|
338
|
+
# metadata and preserving failure chain information.
|
339
|
+
#
|
340
|
+
# @param result [CMDx::Result] The result to propagate from
|
341
|
+
# @param local_metadata [Hash] Additional metadata to merge
|
342
|
+
# @return [void]
|
343
|
+
# @raise [TypeError] If result parameter is not a Result instance
|
344
|
+
#
|
345
|
+
# @example Propagating failure
|
346
|
+
# first_result = FirstTask.call
|
347
|
+
# second_result = SecondTask.call
|
348
|
+
# second_result.throw!(first_result) if first_result.failed?
|
349
|
+
#
|
350
|
+
# @example Propagating with additional context
|
351
|
+
# result.throw!(other_result, context: "During order processing")
|
110
352
|
def throw!(result, local_metadata = {})
|
111
|
-
raise
|
353
|
+
raise TypeError, "must be a Result" unless result.is_a?(Result)
|
112
354
|
|
113
355
|
md = result.metadata.merge(local_metadata)
|
114
356
|
|
@@ -116,61 +358,198 @@ module CMDx
|
|
116
358
|
fail!(**md) if result.failed?
|
117
359
|
end
|
118
360
|
|
361
|
+
# Finds the result that originally caused a failure in the execution chain.
|
362
|
+
#
|
363
|
+
# @return [CMDx::Result, nil] The result that first failed, or nil if not failed
|
364
|
+
#
|
365
|
+
# @example
|
366
|
+
# failed_result = result.caused_failure
|
367
|
+
# puts "Original failure: #{failed_result.metadata[:reason]}" if failed_result
|
119
368
|
def caused_failure
|
120
369
|
return unless failed?
|
121
370
|
|
122
|
-
|
371
|
+
chain.results.reverse.find(&:failed?)
|
123
372
|
end
|
124
373
|
|
374
|
+
# Checks if this result was the original cause of failure.
|
375
|
+
#
|
376
|
+
# @return [Boolean] true if this result caused the failure chain
|
377
|
+
#
|
378
|
+
# @example
|
379
|
+
# result.caused_failure? # => true if this result started the failure chain
|
125
380
|
def caused_failure?
|
126
381
|
return false unless failed?
|
127
382
|
|
128
383
|
caused_failure == self
|
129
384
|
end
|
130
385
|
|
386
|
+
# Finds the result that threw/propagated the failure to this result.
|
387
|
+
#
|
388
|
+
# @return [CMDx::Result, nil] The result that threw the failure, or nil if not failed
|
389
|
+
#
|
390
|
+
# @example
|
391
|
+
# throwing_result = result.threw_failure
|
392
|
+
# puts "Failure thrown by: #{throwing_result.task.class}" if throwing_result
|
131
393
|
def threw_failure
|
132
394
|
return unless failed?
|
133
395
|
|
134
|
-
results =
|
396
|
+
results = chain.results.select(&:failed?)
|
135
397
|
results.find { |r| r.index > index } || results.last
|
136
398
|
end
|
137
399
|
|
400
|
+
# Checks if this result threw/propagated a failure.
|
401
|
+
#
|
402
|
+
# @return [Boolean] true if this result threw a failure to another result
|
403
|
+
#
|
404
|
+
# @example
|
405
|
+
# result.threw_failure? # => true if this result propagated failure
|
138
406
|
def threw_failure?
|
139
407
|
return false unless failed?
|
140
408
|
|
141
409
|
threw_failure == self
|
142
410
|
end
|
143
411
|
|
412
|
+
# Checks if this result received a thrown failure (not the original cause).
|
413
|
+
#
|
414
|
+
# @return [Boolean] true if failed but not the original cause
|
415
|
+
#
|
416
|
+
# @example
|
417
|
+
# result.thrown_failure? # => true if failed due to propagated failure
|
144
418
|
def thrown_failure?
|
145
419
|
failed? && !caused_failure?
|
146
420
|
end
|
147
421
|
|
422
|
+
# Gets the index of this result within the execution chain.
|
423
|
+
#
|
424
|
+
# @return [Integer] The zero-based index of this result in the chain
|
425
|
+
#
|
426
|
+
# @example
|
427
|
+
# result.index # => 0 for first result, 1 for second, etc.
|
148
428
|
def index
|
149
|
-
|
429
|
+
chain.index(self)
|
150
430
|
end
|
151
431
|
|
432
|
+
# Gets the outcome of the result based on state and status.
|
433
|
+
#
|
434
|
+
# Returns state for initialized results or thrown failures,
|
435
|
+
# otherwise returns the status.
|
436
|
+
#
|
437
|
+
# @return [String] The result outcome (state or status)
|
438
|
+
#
|
439
|
+
# @example
|
440
|
+
# result.outcome # => "success", "failed", "interrupted", etc.
|
152
441
|
def outcome
|
153
442
|
initialized? || thrown_failure? ? state : status
|
154
443
|
end
|
155
444
|
|
156
|
-
|
445
|
+
# Measures and returns the runtime of a block execution.
|
446
|
+
#
|
447
|
+
# If called without a block, returns the stored runtime value.
|
448
|
+
# If called with a block, executes and measures the execution
|
449
|
+
# time using monotonic clock.
|
450
|
+
#
|
451
|
+
# @yield Block to execute and measure
|
452
|
+
# @return [Float] Runtime in seconds
|
453
|
+
#
|
454
|
+
# @example Getting stored runtime
|
455
|
+
# result.runtime # => 0.5
|
456
|
+
#
|
457
|
+
# @example Measuring block execution
|
458
|
+
# result.runtime do
|
459
|
+
# # Task execution logic
|
460
|
+
# perform_work
|
461
|
+
# end # => 0.5 (and stores the runtime)
|
462
|
+
def runtime(&)
|
157
463
|
return @runtime unless block_given?
|
158
464
|
|
159
|
-
|
160
|
-
timeout_secs = task.task_setting(timeout_type)
|
161
|
-
|
162
|
-
Timeout.timeout(timeout_secs, TimeoutError, "execution exceeded #{timeout_secs} seconds") do
|
163
|
-
@runtime = Utils::MonotonicRuntime.call(&block)
|
164
|
-
end
|
465
|
+
@runtime = Utils::MonotonicRuntime.call(&)
|
165
466
|
end
|
166
467
|
|
468
|
+
# Converts the result to a hash representation.
|
469
|
+
#
|
470
|
+
# @return [Hash] Serialized result data including task info, state, status, and metadata
|
471
|
+
#
|
472
|
+
# @example
|
473
|
+
# result.to_h
|
474
|
+
# # => {
|
475
|
+
# # class: "ProcessOrderTask",
|
476
|
+
# # type: "Task",
|
477
|
+
# # index: 0,
|
478
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
479
|
+
# # state: "complete",
|
480
|
+
# # status: "success",
|
481
|
+
# # outcome: "success",
|
482
|
+
# # metadata: {},
|
483
|
+
# # runtime: 0.5
|
484
|
+
# # }
|
167
485
|
def to_h
|
168
486
|
ResultSerializer.call(self)
|
169
487
|
end
|
170
488
|
|
489
|
+
# Converts the result to a string representation for inspection.
|
490
|
+
#
|
491
|
+
# @return [String] Human-readable result description
|
492
|
+
#
|
493
|
+
# @example
|
494
|
+
# result.to_s
|
495
|
+
# # => "ProcessOrderTask: type=Task index=0 id=018c2b95... state=complete status=success outcome=success runtime=0.5"
|
171
496
|
def to_s
|
172
497
|
ResultInspector.call(to_h)
|
173
498
|
end
|
174
499
|
|
500
|
+
# Deconstructs the result for array pattern matching.
|
501
|
+
#
|
502
|
+
# Enables pattern matching with array syntax to match against
|
503
|
+
# state and status in order.
|
504
|
+
#
|
505
|
+
# @return [Array<String>] Array containing [state, status]
|
506
|
+
#
|
507
|
+
# @example Array pattern matching
|
508
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
509
|
+
# case result
|
510
|
+
# in ["complete", "success"]
|
511
|
+
# puts "Task completed successfully"
|
512
|
+
# in ["interrupted", "failed"]
|
513
|
+
# puts "Task failed"
|
514
|
+
# end
|
515
|
+
def deconstruct
|
516
|
+
[state, status]
|
517
|
+
end
|
518
|
+
|
519
|
+
# Deconstructs the result for hash pattern matching.
|
520
|
+
#
|
521
|
+
# Enables pattern matching with hash syntax to match against
|
522
|
+
# specific result attributes.
|
523
|
+
#
|
524
|
+
# @param keys [Array<Symbol>] Specific keys to extract (optional)
|
525
|
+
# @return [Hash] Hash containing result attributes
|
526
|
+
#
|
527
|
+
# @example Hash pattern matching
|
528
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
529
|
+
# case result
|
530
|
+
# in { state: "complete", status: "success" }
|
531
|
+
# puts "Success!"
|
532
|
+
# in { state: "interrupted", status: "failed", metadata: { reason: String => reason } }
|
533
|
+
# puts "Failed: #{reason}"
|
534
|
+
# end
|
535
|
+
#
|
536
|
+
# @example Specific key extraction
|
537
|
+
# result.deconstruct_keys([:state, :status])
|
538
|
+
# # => { state: "complete", status: "success" }
|
539
|
+
def deconstruct_keys(keys)
|
540
|
+
attributes = {
|
541
|
+
state: state,
|
542
|
+
status: status,
|
543
|
+
metadata: metadata,
|
544
|
+
executed: executed?,
|
545
|
+
good: good?,
|
546
|
+
bad: bad?
|
547
|
+
}
|
548
|
+
|
549
|
+
return attributes if keys.nil?
|
550
|
+
|
551
|
+
attributes.slice(*keys)
|
552
|
+
end
|
553
|
+
|
175
554
|
end
|
176
555
|
end
|