cmdx 0.5.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 +31 -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 +203 -31
- data/docs/outcomes/statuses.md +275 -30
- 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 +367 -25
- 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 +405 -37
- 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 -62
- data/lib/cmdx/batch.rb +0 -43
- data/lib/cmdx/parameters.rb +0 -35
- data/lib/cmdx/run.rb +0 -39
- data/lib/cmdx/run_inspector.rb +0 -26
- data/lib/cmdx/run_serializer.rb +0 -20
- 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/core_ext/object.rb
CHANGED
@@ -2,10 +2,52 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module CoreExt
|
5
|
+
# Extensions to Object that provide CMDx-specific utility methods.
|
6
|
+
#
|
7
|
+
# ObjectExtensions adds safe method calling, conditional evaluation,
|
8
|
+
# and value yielding capabilities to all Ruby objects. These methods
|
9
|
+
# are prefixed with `__cmdx_` to avoid conflicts with existing methods.
|
10
|
+
#
|
11
|
+
# @example Safe method calling
|
12
|
+
# object.__cmdx_try(:some_method) # Returns nil if method doesn't exist
|
13
|
+
# object.__cmdx_try(proc { expensive_calculation }) # Calls proc safely
|
14
|
+
#
|
15
|
+
# @example Conditional evaluation
|
16
|
+
# object.__cmdx_eval(if: :valid?) # True if object.valid? is true
|
17
|
+
# object.__cmdx_eval(unless: :empty?) # True unless object.empty? is true
|
18
|
+
# object.__cmdx_eval(if: :valid?, unless: :processed?) # Combined conditions
|
19
|
+
#
|
20
|
+
# @example Value yielding
|
21
|
+
# object.__cmdx_yield(:name) # Returns object.name if method exists, otherwise :name
|
22
|
+
# object.__cmdx_yield(-> { compute }) # Executes lambda and returns result
|
23
|
+
#
|
24
|
+
# @see Task Tasks that use these object extensions
|
25
|
+
# @see Parameter Parameters that leverage object extensions
|
5
26
|
module ObjectExtensions
|
6
27
|
|
28
|
+
# Store original respond_to? method before aliasing
|
7
29
|
alias __cmdx_respond_to? respond_to?
|
8
30
|
|
31
|
+
# Safely attempt to call a method or execute a proc on an object.
|
32
|
+
#
|
33
|
+
# This method provides safe method calling with fallback behavior.
|
34
|
+
# It handles method calls, proc execution, and hash key access gracefully.
|
35
|
+
#
|
36
|
+
# @param key [Symbol, String, Proc] method name or callable to attempt
|
37
|
+
# @param args [Array] arguments to pass to the method/proc (passed via splat)
|
38
|
+
# @return [Object, nil] result of method call, proc execution, or nil if not possible
|
39
|
+
#
|
40
|
+
# @example Method calling
|
41
|
+
# user.__cmdx_try(:name) # => "John" or nil
|
42
|
+
# user.__cmdx_try(:age, 25) # => calls user.age(25) or nil
|
43
|
+
#
|
44
|
+
# @example Proc execution
|
45
|
+
# user.__cmdx_try(-> { expensive_calc }) # => executes lambda
|
46
|
+
# user.__cmdx_try(proc { |x| x * 2 }, 5) # => 10
|
47
|
+
#
|
48
|
+
# @example Hash access
|
49
|
+
# hash = {name: "John"}
|
50
|
+
# hash.__cmdx_try(:name) # => "John"
|
9
51
|
def __cmdx_try(key, ...)
|
10
52
|
if key.is_a?(Proc)
|
11
53
|
return instance_eval(&key) unless is_a?(Module) || key.inspect.include?("(lambda)")
|
@@ -18,6 +60,27 @@ module CMDx
|
|
18
60
|
end
|
19
61
|
end
|
20
62
|
|
63
|
+
# Evaluate conditional options for execution control.
|
64
|
+
#
|
65
|
+
# This method evaluates :if and :unless conditions to determine
|
66
|
+
# whether something should proceed. Used extensively in callbacks
|
67
|
+
# and conditional parameter processing.
|
68
|
+
#
|
69
|
+
# @param options [Hash] conditional options
|
70
|
+
# @option options [Symbol, Proc] :if condition that must be true
|
71
|
+
# @option options [Symbol, Proc] :unless condition that must be false
|
72
|
+
# @option options [Boolean] :default default value if no conditions (default: true)
|
73
|
+
# @return [Boolean] true if conditions are met
|
74
|
+
#
|
75
|
+
# @example Simple conditions
|
76
|
+
# user.__cmdx_eval(if: :admin?) # => true if user.admin? is true
|
77
|
+
# user.__cmdx_eval(unless: :guest?) # => true unless user.guest? is true
|
78
|
+
#
|
79
|
+
# @example Combined conditions
|
80
|
+
# user.__cmdx_eval(if: :active?, unless: :banned?) # => active AND not banned
|
81
|
+
#
|
82
|
+
# @example With procs
|
83
|
+
# user.__cmdx_eval(if: -> { Time.now.monday? }) # => true if today is Monday
|
21
84
|
def __cmdx_eval(options = {})
|
22
85
|
if options[:if] && options[:unless]
|
23
86
|
__cmdx_try(options[:if]) && !__cmdx_try(options[:unless])
|
@@ -30,6 +93,27 @@ module CMDx
|
|
30
93
|
end
|
31
94
|
end
|
32
95
|
|
96
|
+
# Yield a value by attempting to call it as a method or executing it.
|
97
|
+
#
|
98
|
+
# This method provides intelligent value resolution - if the key is
|
99
|
+
# a method name and the object responds to it, call the method.
|
100
|
+
# Otherwise, try to execute it as a proc or return the value as-is.
|
101
|
+
#
|
102
|
+
# @param key [Object] value to yield, method name, or callable
|
103
|
+
# @param args [Array] arguments to pass if calling method/proc (passed via splat)
|
104
|
+
# @return [Object] yielded value
|
105
|
+
#
|
106
|
+
# @example Method yielding
|
107
|
+
# user.__cmdx_yield(:name) # => calls user.name if method exists, otherwise returns :name
|
108
|
+
# user.__cmdx_yield("email") # => calls user.email if method exists, otherwise returns "email"
|
109
|
+
#
|
110
|
+
# @example Proc yielding
|
111
|
+
# user.__cmdx_yield(-> { timestamp }) # => executes lambda
|
112
|
+
# hash.__cmdx_yield({key: "value"}) # => tries hash access
|
113
|
+
#
|
114
|
+
# @example Direct values
|
115
|
+
# user.__cmdx_yield(42) # => 42
|
116
|
+
# user.__cmdx_yield("literal") # => "literal"
|
33
117
|
def __cmdx_yield(key, ...)
|
34
118
|
if key.is_a?(Symbol) || key.is_a?(String)
|
35
119
|
return key unless respond_to?(key, true)
|
@@ -42,6 +126,26 @@ module CMDx
|
|
42
126
|
end
|
43
127
|
end
|
44
128
|
|
129
|
+
# Call an object if it responds to call, otherwise return itself.
|
130
|
+
#
|
131
|
+
# This method provides safe callable execution - if the object
|
132
|
+
# can be called (like a proc or lambda), call it with the given
|
133
|
+
# arguments. Otherwise, return the object unchanged.
|
134
|
+
#
|
135
|
+
# @param args [Array] arguments to pass to call method (passed via splat)
|
136
|
+
# @return [Object] result of calling or the object itself
|
137
|
+
#
|
138
|
+
# @example Callable objects
|
139
|
+
# proc = -> { "Hello" }
|
140
|
+
# proc.__cmdx_call # => "Hello"
|
141
|
+
#
|
142
|
+
# @example Non-callable objects
|
143
|
+
# string = "Hello"
|
144
|
+
# string.__cmdx_call # => "Hello"
|
145
|
+
#
|
146
|
+
# @example With arguments
|
147
|
+
# adder = ->(a, b) { a + b }
|
148
|
+
# adder.__cmdx_call(2, 3) # => 5
|
45
149
|
def __cmdx_call(...)
|
46
150
|
return self unless respond_to?(:call)
|
47
151
|
|
@@ -52,4 +156,5 @@ module CMDx
|
|
52
156
|
end
|
53
157
|
end
|
54
158
|
|
159
|
+
# Extend all objects with CMDx utility methods
|
55
160
|
Object.include(CMDx::CoreExt::ObjectExtensions)
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
##
|
5
|
+
# Thread-safe correlation ID management for tracking related operations across request boundaries.
|
6
|
+
#
|
7
|
+
# The Correlator provides a simple, thread-local storage mechanism for managing correlation
|
8
|
+
# identifiers throughout the execution lifecycle. It enables tracing related operations
|
9
|
+
# across different tasks, workflows, and service boundaries within the same execution context.
|
10
|
+
#
|
11
|
+
# Correlation IDs are automatically used by CMDx runs when available, providing seamless
|
12
|
+
# request tracking without requiring explicit parameter passing between tasks.
|
13
|
+
#
|
14
|
+
# ## Thread Safety
|
15
|
+
#
|
16
|
+
# All correlation operations are thread-local, ensuring that different threads maintain
|
17
|
+
# separate correlation contexts without interference. This is essential for concurrent
|
18
|
+
# request processing in multi-threaded applications.
|
19
|
+
#
|
20
|
+
# ## Integration with CMDx Runs
|
21
|
+
#
|
22
|
+
# When a new CMDx::Chain is created, it automatically uses the current thread's correlation
|
23
|
+
# ID as its chain identifier if available, falling back to UUID generation if none exists.
|
24
|
+
#
|
25
|
+
# @example Basic correlation usage
|
26
|
+
# CMDx::Correlator.id = "req-12345"
|
27
|
+
# CMDx::Correlator.id # => "req-12345"
|
28
|
+
#
|
29
|
+
# # Chain automatically inherits correlation ID
|
30
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
31
|
+
# result.chain.id # => "req-12345"
|
32
|
+
#
|
33
|
+
# @example Block-based correlation context
|
34
|
+
# CMDx::Correlator.use("workflow-operation-456") do
|
35
|
+
# # All tasks within this block share the same correlation
|
36
|
+
# ProcessOrderTask.call(order_id: 123)
|
37
|
+
# SendEmailTask.call(user_id: 456)
|
38
|
+
# end
|
39
|
+
# # Correlation context is automatically restored
|
40
|
+
#
|
41
|
+
# @example Nested correlation contexts
|
42
|
+
# CMDx::Correlator.use("parent-operation") do
|
43
|
+
# CMDx::Correlator.use("child-operation") do
|
44
|
+
# ProcessOrderTask.call(order_id: 123)
|
45
|
+
# # Uses "child-operation" as correlation ID
|
46
|
+
# end
|
47
|
+
# # Restored to "parent-operation"
|
48
|
+
# SendEmailTask.call(user_id: 456)
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @example Manual correlation management
|
52
|
+
# # Set correlation ID for current thread
|
53
|
+
# CMDx::Correlator.id = "manual-correlation"
|
54
|
+
#
|
55
|
+
# # Check current correlation
|
56
|
+
# current_id = CMDx::Correlator.id
|
57
|
+
#
|
58
|
+
# # Clear correlation when done
|
59
|
+
# CMDx::Correlator.clear
|
60
|
+
#
|
61
|
+
# @example Middleware integration pattern
|
62
|
+
# class CorrelationMiddleware
|
63
|
+
# def call(env)
|
64
|
+
# correlation_id = env['HTTP_X_CORRELATION_ID'] || CMDx::Correlator.generate
|
65
|
+
#
|
66
|
+
# CMDx::Correlator.use(correlation_id) do
|
67
|
+
# @app.call(env)
|
68
|
+
# end
|
69
|
+
# end
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# @see CMDx::Chain Chain execution context that inherits correlation IDs
|
73
|
+
# @since 1.0.0
|
74
|
+
module Correlator
|
75
|
+
|
76
|
+
##
|
77
|
+
# Thread-local storage key for correlation identifiers.
|
78
|
+
#
|
79
|
+
# Uses a Symbol key to avoid potential conflicts with other thread-local
|
80
|
+
# variables and to ensure consistent access across the correlator methods.
|
81
|
+
#
|
82
|
+
# @return [Symbol] the thread-local storage key
|
83
|
+
THREAD_KEY = :cmdx_correlation_id
|
84
|
+
|
85
|
+
module_function
|
86
|
+
|
87
|
+
##
|
88
|
+
# Generates a new UUID suitable for use as a correlation identifier.
|
89
|
+
#
|
90
|
+
# Creates a RFC 4122 compliant UUID using SecureRandom, providing a globally
|
91
|
+
# unique identifier suitable for distributed request tracing.
|
92
|
+
#
|
93
|
+
# @return [String] a new UUID string in standard format (e.g., "123e4567-e89b-12d3-a456-426614174000")
|
94
|
+
#
|
95
|
+
# @example Generating correlation IDs
|
96
|
+
# id1 = CMDx::Correlator.generate # => "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
97
|
+
# id2 = CMDx::Correlator.generate # => "018c2b95-b765-7123-b456-dd7c920fe3a8"
|
98
|
+
# id1 != id2 # => true
|
99
|
+
def generate
|
100
|
+
SecureRandom.uuid
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Retrieves the current thread's correlation identifier.
|
105
|
+
#
|
106
|
+
# Returns the correlation ID that was previously set for the current thread,
|
107
|
+
# or nil if no correlation ID has been established. This method is thread-safe
|
108
|
+
# and will not interfere with correlation IDs set in other threads.
|
109
|
+
#
|
110
|
+
# @return [String, nil] the current correlation ID or nil if none is set
|
111
|
+
#
|
112
|
+
# @example Checking current correlation
|
113
|
+
# CMDx::Correlator.id # => nil (when none is set)
|
114
|
+
#
|
115
|
+
# CMDx::Correlator.id = "test-correlation"
|
116
|
+
# CMDx::Correlator.id # => "test-correlation"
|
117
|
+
def id
|
118
|
+
Thread.current[THREAD_KEY]
|
119
|
+
end
|
120
|
+
|
121
|
+
##
|
122
|
+
# Sets the correlation identifier for the current thread.
|
123
|
+
#
|
124
|
+
# Assigns a correlation ID to the current thread's local storage, making it
|
125
|
+
# available for subsequent operations within the same thread context. The
|
126
|
+
# correlation ID will persist until explicitly changed or cleared.
|
127
|
+
#
|
128
|
+
# @param value [String, #to_s] the correlation identifier to set
|
129
|
+
# @return [String] the assigned correlation identifier
|
130
|
+
#
|
131
|
+
# @example Setting correlation ID
|
132
|
+
# CMDx::Correlator.id = "user-request-123"
|
133
|
+
# CMDx::Correlator.id # => "user-request-123"
|
134
|
+
#
|
135
|
+
# @example Type coercion
|
136
|
+
# CMDx::Correlator.id = 12345
|
137
|
+
# CMDx::Correlator.id # => 12345 (stored as provided)
|
138
|
+
def id=(value)
|
139
|
+
Thread.current[THREAD_KEY] = value
|
140
|
+
end
|
141
|
+
|
142
|
+
##
|
143
|
+
# Clears the correlation identifier for the current thread.
|
144
|
+
#
|
145
|
+
# Removes the correlation ID from the current thread's local storage,
|
146
|
+
# effectively resetting the correlation context. Subsequent calls to
|
147
|
+
# {#id} will return nil until a new correlation ID is set.
|
148
|
+
#
|
149
|
+
# @return [nil] always returns nil
|
150
|
+
#
|
151
|
+
# @example Clearing correlation
|
152
|
+
# CMDx::Correlator.id = "temporary-correlation"
|
153
|
+
# CMDx::Correlator.id # => "temporary-correlation"
|
154
|
+
#
|
155
|
+
# CMDx::Correlator.clear
|
156
|
+
# CMDx::Correlator.id # => nil
|
157
|
+
def clear
|
158
|
+
Thread.current[THREAD_KEY] = nil
|
159
|
+
end
|
160
|
+
|
161
|
+
##
|
162
|
+
# Temporarily sets a correlation identifier for the duration of a block.
|
163
|
+
#
|
164
|
+
# Establishes a correlation context for the given block, automatically
|
165
|
+
# restoring the previous correlation ID when the block completes. This
|
166
|
+
# method is exception-safe and will restore the original context even
|
167
|
+
# if the block raises an error.
|
168
|
+
#
|
169
|
+
# This is the preferred method for managing correlation contexts as it
|
170
|
+
# ensures proper cleanup and supports nested correlation scopes.
|
171
|
+
#
|
172
|
+
# @param value [String, #to_s] the correlation identifier to use during block execution
|
173
|
+
# @yieldreturn [Object] the return value of the block
|
174
|
+
# @return [Object] the return value of the executed block
|
175
|
+
#
|
176
|
+
# @example Basic usage
|
177
|
+
# result = CMDx::Correlator.use("operation-123") do
|
178
|
+
# ProcessOrderTask.call(order_id: 456)
|
179
|
+
# end
|
180
|
+
# # Correlation context is automatically restored
|
181
|
+
#
|
182
|
+
# @example Nested contexts
|
183
|
+
# CMDx::Correlator.use("parent") do
|
184
|
+
# CMDx::Correlator.id # => "parent"
|
185
|
+
#
|
186
|
+
# CMDx::Correlator.use("child") do
|
187
|
+
# CMDx::Correlator.id # => "child"
|
188
|
+
# end
|
189
|
+
#
|
190
|
+
# CMDx::Correlator.id # => "parent" (restored)
|
191
|
+
# end
|
192
|
+
#
|
193
|
+
# @example Exception safety
|
194
|
+
# CMDx::Correlator.id = "original"
|
195
|
+
#
|
196
|
+
# begin
|
197
|
+
# CMDx::Correlator.use("temporary") do
|
198
|
+
# raise StandardError, "something went wrong"
|
199
|
+
# end
|
200
|
+
# rescue StandardError
|
201
|
+
# CMDx::Correlator.id # => "original" (still restored)
|
202
|
+
# end
|
203
|
+
def use(value)
|
204
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
205
|
+
raise TypeError,
|
206
|
+
"must be a String or Symbol"
|
207
|
+
end
|
208
|
+
|
209
|
+
previous_id = id
|
210
|
+
self.id = value
|
211
|
+
yield
|
212
|
+
ensure
|
213
|
+
self.id = previous_id
|
214
|
+
end
|
215
|
+
|
216
|
+
end
|
217
|
+
end
|
data/lib/cmdx/error.rb
CHANGED
@@ -2,22 +2,224 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
|
5
|
-
|
5
|
+
##
|
6
|
+
# Base exception class for all CMDx-specific errors.
|
7
|
+
# All other CMDx exceptions inherit from this class, providing a common
|
8
|
+
# hierarchy for error handling and rescue operations.
|
9
|
+
#
|
10
|
+
# This allows for catching all CMDx-related exceptions with a single rescue clause
|
11
|
+
# while still maintaining specific error types for detailed error handling.
|
12
|
+
#
|
13
|
+
# @example Catching all CMDx errors
|
14
|
+
# begin
|
15
|
+
# ProcessOrderTask.call(invalid_params)
|
16
|
+
# rescue CMDx::Error => e
|
17
|
+
# logger.error "CMDx error occurred: #{e.message}"
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# @example Specific error handling
|
21
|
+
# begin
|
22
|
+
# ProcessOrderTask.call(order_id: "invalid")
|
23
|
+
# rescue CMDx::CoercionError => e
|
24
|
+
# # Handle type coercion failures
|
25
|
+
# rescue CMDx::ValidationError => e
|
26
|
+
# # Handle validation failures
|
27
|
+
# rescue CMDx::Error => e
|
28
|
+
# # Handle any other CMDx errors
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# @see StandardError Ruby's standard error base class
|
32
|
+
# @since 1.0.0
|
6
33
|
Error = Class.new(StandardError)
|
7
34
|
|
8
|
-
|
35
|
+
##
|
36
|
+
# Raised when a value cannot be coerced to the specified type.
|
37
|
+
# This exception occurs during parameter processing when type coercion fails,
|
38
|
+
# typically due to incompatible data formats or invalid input values.
|
39
|
+
#
|
40
|
+
# CoercionError is raised by the various coercion modules when they encounter
|
41
|
+
# values that cannot be converted to the target type. Each coercion module
|
42
|
+
# provides specific error messages indicating the expected type and the
|
43
|
+
# problematic value.
|
44
|
+
#
|
45
|
+
# @example Integer coercion failure
|
46
|
+
# class MyTask < CMDx::Task
|
47
|
+
# required :count, type: :integer
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# # This will raise CoercionError during parameter processing
|
51
|
+
# MyTask.call(count: "not_a_number")
|
52
|
+
# # => CMDx::CoercionError: could not coerce into an integer
|
53
|
+
#
|
54
|
+
# @example Date coercion failure
|
55
|
+
# class ScheduleTask < CMDx::Task
|
56
|
+
# required :due_date, type: :date
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# ScheduleTask.call(due_date: "invalid_date")
|
60
|
+
# # => CMDx::CoercionError: could not coerce into a date
|
61
|
+
#
|
62
|
+
# @example Handling coercion errors
|
63
|
+
# begin
|
64
|
+
# MyTask.call(count: "invalid")
|
65
|
+
# rescue CMDx::CoercionError => e
|
66
|
+
# # Log the coercion failure and provide user-friendly message
|
67
|
+
# logger.warn "Invalid input format: #{e.message}"
|
68
|
+
# render json: { error: "Please provide a valid number" }
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# @see Parameter Parameter type definitions and coercion
|
72
|
+
# @see ParameterValue Parameter value processing and coercion
|
73
|
+
# @since 1.0.0
|
9
74
|
CoercionError = Class.new(Error)
|
10
75
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
#
|
76
|
+
##
|
77
|
+
# Raised when a task class doesn't implement the required `call` method.
|
78
|
+
# This exception enforces the CMDx contract that all task classes must
|
79
|
+
# provide a `call` method containing their business logic.
|
80
|
+
#
|
81
|
+
# This error typically occurs during development when creating new task
|
82
|
+
# classes that inherit from CMDx::Task but forget to implement the
|
83
|
+
# abstract `call` method.
|
84
|
+
#
|
85
|
+
# @example Missing call method
|
86
|
+
# class IncompleteTask < CMDx::Task
|
87
|
+
# required :data, type: :string
|
88
|
+
# # Missing call method implementation
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# IncompleteTask.call(data: "test")
|
92
|
+
# # => CMDx::UndefinedCallError: call method not defined in IncompleteTask
|
93
|
+
#
|
94
|
+
# @example Proper task implementation
|
95
|
+
# class CompleteTask < CMDx::Task
|
96
|
+
# required :data, type: :string
|
97
|
+
#
|
98
|
+
# def call
|
99
|
+
# # Business logic implementation
|
100
|
+
# context.result = process(data)
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
#
|
104
|
+
# @example Handling undefined call errors
|
105
|
+
# begin
|
106
|
+
# SomeTask.call(params)
|
107
|
+
# rescue CMDx::UndefinedCallError => e
|
108
|
+
# # This should typically only happen during development
|
109
|
+
# logger.error "Task implementation incomplete: #{e.message}"
|
110
|
+
# raise # Re-raise as this is a programming error
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# @see Task Task base class and call method requirement
|
114
|
+
# @see Workflow Workflow base class and call method requirement
|
115
|
+
# @since 1.0.0
|
15
116
|
UndefinedCallError = Class.new(Error)
|
16
117
|
|
17
|
-
|
118
|
+
##
|
119
|
+
# Raised when an unknown or unsupported coercion type is specified.
|
120
|
+
# This exception occurs when parameter definitions reference type coercions
|
121
|
+
# that don't exist or aren't registered in the CMDx coercion system.
|
122
|
+
#
|
123
|
+
# This error helps catch typos in type specifications and ensures that
|
124
|
+
# only supported data types are used in parameter definitions.
|
125
|
+
#
|
126
|
+
# @example Unknown type specification
|
127
|
+
# class MyTask < CMDx::Task
|
128
|
+
# required :value, type: :unknown_type # Typo or unsupported type
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# MyTask.call(value: "test")
|
132
|
+
# # => CMDx::UnknownCoercionError: unknown coercion unknown_type
|
133
|
+
#
|
134
|
+
# @example Common typos
|
135
|
+
# class TaskWithTypo < CMDx::Task
|
136
|
+
# required :count, type: :integr # Should be :integer
|
137
|
+
# required :flag, type: :bool # Should be :boolean
|
138
|
+
# required :data, type: :json # Should be :hash
|
139
|
+
# end
|
140
|
+
#
|
141
|
+
# @example Supported types
|
142
|
+
# class ProperTask < CMDx::Task
|
143
|
+
# required :id, type: :integer
|
144
|
+
# required :active, type: :boolean
|
145
|
+
# required :metadata, type: :hash
|
146
|
+
# required :tags, type: :array
|
147
|
+
# required :name, type: :string
|
148
|
+
# required :score, type: :float
|
149
|
+
# required :created_at, type: :date_time
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# @example Handling unknown coercion errors
|
153
|
+
# begin
|
154
|
+
# MyTask.call(params)
|
155
|
+
# rescue CMDx::UnknownCoercionError => e
|
156
|
+
# # This indicates a programming error in parameter definition
|
157
|
+
# logger.error "Invalid type specification: #{e.message}"
|
158
|
+
# raise # Re-raise as this should be fixed in code
|
159
|
+
# end
|
160
|
+
#
|
161
|
+
# @see Parameter Parameter type definitions
|
162
|
+
# @see ParameterValue Type coercion processing
|
163
|
+
# @since 1.0.0
|
18
164
|
UnknownCoercionError = Class.new(Error)
|
19
165
|
|
20
|
-
|
166
|
+
##
|
167
|
+
# Raised when a parameter value fails validation rules.
|
168
|
+
# This exception occurs during parameter processing when values don't meet
|
169
|
+
# the specified validation criteria, such as format requirements, length
|
170
|
+
# constraints, or custom validation logic.
|
171
|
+
#
|
172
|
+
# ValidationError provides detailed feedback about why validation failed,
|
173
|
+
# helping developers and users understand what corrections are needed.
|
174
|
+
#
|
175
|
+
# @example Presence validation failure
|
176
|
+
# class CreateUserTask < CMDx::Task
|
177
|
+
# required :email, type: :string, presence: true
|
178
|
+
# end
|
179
|
+
#
|
180
|
+
# CreateUserTask.call(email: "")
|
181
|
+
# # => CMDx::ValidationError: cannot be empty
|
182
|
+
#
|
183
|
+
# @example Format validation failure
|
184
|
+
# class ValidateEmailTask < CMDx::Task
|
185
|
+
# required :email, type: :string, format: { with: /@/ }
|
186
|
+
# end
|
187
|
+
#
|
188
|
+
# ValidateEmailTask.call(email: "invalid-email")
|
189
|
+
# # => CMDx::ValidationError: is an invalid format
|
190
|
+
#
|
191
|
+
# @example Length validation failure
|
192
|
+
# class SetPasswordTask < CMDx::Task
|
193
|
+
# required :password, type: :string, length: { min: 8 }
|
194
|
+
# end
|
195
|
+
#
|
196
|
+
# SetPasswordTask.call(password: "short")
|
197
|
+
# # => CMDx::ValidationError: length must be at least 8
|
198
|
+
#
|
199
|
+
# @example Custom validation failure
|
200
|
+
# class ProcessOrderTask < CMDx::Task
|
201
|
+
# required :quantity, type: :integer, custom: -> (val) { val > 0 }
|
202
|
+
# end
|
203
|
+
#
|
204
|
+
# ProcessOrderTask.call(quantity: -1)
|
205
|
+
# # => CMDx::ValidationError: is not valid
|
206
|
+
#
|
207
|
+
# @example Handling validation errors
|
208
|
+
# begin
|
209
|
+
# CreateUserTask.call(email: "", password: "short")
|
210
|
+
# rescue CMDx::ValidationError => e
|
211
|
+
# # Provide user-friendly feedback
|
212
|
+
# render json: {
|
213
|
+
# error: "Validation failed",
|
214
|
+
# message: e.message,
|
215
|
+
# field: extract_field_from_context(e)
|
216
|
+
# }
|
217
|
+
# end
|
218
|
+
#
|
219
|
+
# @see Parameter Parameter validation options
|
220
|
+
# @see ParameterValue Validation processing
|
221
|
+
# @see Validators Validation modules (Presence, Format, Length, etc.)
|
222
|
+
# @since 1.0.0
|
21
223
|
ValidationError = Class.new(Error)
|
22
224
|
|
23
225
|
end
|