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/chain.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Thread-local chain that tracks task execution results within a correlation context.
|
5
|
+
#
|
6
|
+
# A Chain represents a sequence of task executions that are logically related,
|
7
|
+
# typically within the same request or operation flow. It provides thread-local
|
8
|
+
# storage to ensure that tasks executing in the same thread share the same chain
|
9
|
+
# while maintaining isolation across different threads.
|
10
|
+
#
|
11
|
+
# @example Basic usage with automatic chain creation
|
12
|
+
# # Chain is automatically created when first task runs
|
13
|
+
# result1 = MyTask.call(data: "first")
|
14
|
+
# result2 = MyTask.call(data: "second")
|
15
|
+
#
|
16
|
+
# result1.chain.id == result2.chain.id #=> true
|
17
|
+
# result1.index #=> 0
|
18
|
+
# result2.index #=> 1
|
19
|
+
#
|
20
|
+
# @example Using custom chain ID
|
21
|
+
# chain = CMDx::Chain.new(id: "custom-correlation-123")
|
22
|
+
# CMDx::Chain.current = chain
|
23
|
+
#
|
24
|
+
# result = MyTask.call(data: "test")
|
25
|
+
# result.chain.id #=> "custom-correlation-123"
|
26
|
+
#
|
27
|
+
# @example Thread isolation
|
28
|
+
# # Each thread gets its own chain
|
29
|
+
# Thread.new do
|
30
|
+
# result = MyTask.call(data: "thread1")
|
31
|
+
# result.chain.id #=> unique ID for this thread
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# Thread.new do
|
35
|
+
# result = MyTask.call(data: "thread2")
|
36
|
+
# result.chain.id #=> different unique ID
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @example Temporary chain context
|
40
|
+
# CMDx::Chain.use(id: "temp-correlation") do
|
41
|
+
# result = MyTask.call(data: "test")
|
42
|
+
# result.chain.id #=> "temp-correlation"
|
43
|
+
# end
|
44
|
+
# # Original chain is restored after block
|
45
|
+
#
|
46
|
+
# @see CMDx::Correlator
|
47
|
+
# @since 1.0.0
|
48
|
+
class Chain
|
49
|
+
|
50
|
+
# Thread-local storage key for the current chain
|
51
|
+
THREAD_KEY = :cmdx_correlation_chain
|
52
|
+
|
53
|
+
__cmdx_attr_delegator :index, :first, :last, :size,
|
54
|
+
to: :results
|
55
|
+
__cmdx_attr_delegator :state, :status, :outcome, :runtime,
|
56
|
+
to: :first
|
57
|
+
|
58
|
+
# @!attribute [r] id
|
59
|
+
# @return [String] the unique identifier for this chain
|
60
|
+
# @!attribute [r] results
|
61
|
+
# @return [Array<CMDx::Result>] the collection of task results in this chain
|
62
|
+
attr_reader :id, :results
|
63
|
+
|
64
|
+
# Creates a new chain instance.
|
65
|
+
#
|
66
|
+
# @param attributes [Hash] configuration options for the chain
|
67
|
+
# @option attributes [String] :id custom identifier for the chain.
|
68
|
+
# If not provided, uses the current correlator ID or generates a new UUID.
|
69
|
+
#
|
70
|
+
# @example Create chain with default ID
|
71
|
+
# chain = CMDx::Chain.new
|
72
|
+
# chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
73
|
+
#
|
74
|
+
# @example Create chain with custom ID
|
75
|
+
# chain = CMDx::Chain.new(id: "user-session-123")
|
76
|
+
# chain.id #=> "user-session-123"
|
77
|
+
def initialize(attributes = {})
|
78
|
+
@id = attributes[:id] || CMDx::Correlator.id || CMDx::Correlator.generate
|
79
|
+
@results = []
|
80
|
+
end
|
81
|
+
|
82
|
+
class << self
|
83
|
+
|
84
|
+
# Returns the current thread-local chain.
|
85
|
+
#
|
86
|
+
# @return [CMDx::Chain, nil] the chain for the current thread, or nil if none exists
|
87
|
+
#
|
88
|
+
# @example
|
89
|
+
# CMDx::Chain.current #=> nil (no chain set)
|
90
|
+
#
|
91
|
+
# MyTask.call(data: "test")
|
92
|
+
# CMDx::Chain.current #=> #<CMDx::Chain:0x... @id="018c2b95...">
|
93
|
+
def current
|
94
|
+
Thread.current[THREAD_KEY]
|
95
|
+
end
|
96
|
+
|
97
|
+
# Sets the current thread-local chain.
|
98
|
+
#
|
99
|
+
# @param chain [CMDx::Chain, nil] the chain to set for the current thread
|
100
|
+
# @return [CMDx::Chain, nil] the chain that was set
|
101
|
+
#
|
102
|
+
# @example
|
103
|
+
# chain = CMDx::Chain.new(id: "custom-id")
|
104
|
+
# CMDx::Chain.current = chain
|
105
|
+
# CMDx::Chain.current.id #=> "custom-id"
|
106
|
+
def current=(chain)
|
107
|
+
Thread.current[THREAD_KEY] = chain
|
108
|
+
end
|
109
|
+
|
110
|
+
# Clears the current thread-local chain.
|
111
|
+
#
|
112
|
+
# @return [nil]
|
113
|
+
#
|
114
|
+
# @example
|
115
|
+
# CMDx::Chain.current #=> #<CMDx::Chain:0x...>
|
116
|
+
# CMDx::Chain.clear
|
117
|
+
# CMDx::Chain.current #=> nil
|
118
|
+
def clear
|
119
|
+
Thread.current[THREAD_KEY] = nil
|
120
|
+
end
|
121
|
+
|
122
|
+
# Adds a result to the current chain, creating a new chain if none exists.
|
123
|
+
#
|
124
|
+
# This method is typically called internally by the task execution framework
|
125
|
+
# and should not be used directly in application code.
|
126
|
+
#
|
127
|
+
# @param result [CMDx::Result] the task result to add to the chain
|
128
|
+
# @return [CMDx::Chain] the chain containing the result
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
def build(result)
|
132
|
+
raise TypeError, "must be a Result" unless result.is_a?(Result)
|
133
|
+
|
134
|
+
self.current ||= new
|
135
|
+
current.results << result
|
136
|
+
current
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
# Converts the chain to a hash representation.
|
142
|
+
#
|
143
|
+
# Serializes the chain and all its results into a structured hash
|
144
|
+
# suitable for logging, debugging, and data interchange.
|
145
|
+
#
|
146
|
+
# @return [Hash] Structured hash representation of the chain
|
147
|
+
#
|
148
|
+
# @example
|
149
|
+
# chain.to_h
|
150
|
+
# # => {
|
151
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
152
|
+
# # state: "complete",
|
153
|
+
# # status: "success",
|
154
|
+
# # outcome: "success",
|
155
|
+
# # runtime: 0.5,
|
156
|
+
# # results: [
|
157
|
+
# # { class: "ProcessOrderTask", state: "complete", status: "success", ... },
|
158
|
+
# # { class: "SendEmailTask", state: "complete", status: "success", ... }
|
159
|
+
# # ]
|
160
|
+
# # }
|
161
|
+
def to_h
|
162
|
+
ChainSerializer.call(self)
|
163
|
+
end
|
164
|
+
alias to_a to_h
|
165
|
+
|
166
|
+
# Converts the chain to a string representation for inspection.
|
167
|
+
#
|
168
|
+
# Creates a comprehensive, human-readable summary of the chain including
|
169
|
+
# all task results with formatted headers and footers.
|
170
|
+
#
|
171
|
+
# @return [String] Formatted chain summary with task details
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# chain.to_s
|
175
|
+
# # => "
|
176
|
+
# # chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
177
|
+
# # ================================================
|
178
|
+
# #
|
179
|
+
# # ProcessOrderTask: index=0 state=complete status=success ...
|
180
|
+
# # SendEmailTask: index=1 state=complete status=success ...
|
181
|
+
# #
|
182
|
+
# # ================================================
|
183
|
+
# # state: complete | status: success | outcome: success | runtime: 0.5
|
184
|
+
# # "
|
185
|
+
def to_s
|
186
|
+
ChainInspector.call(self)
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Chain inspection utility for generating comprehensive chain summaries.
|
5
|
+
#
|
6
|
+
# The ChainInspector module provides functionality to convert Chain instances
|
7
|
+
# into detailed, human-readable string representations. It creates formatted
|
8
|
+
# summaries that include chain metadata, all task results, and summary statistics
|
9
|
+
# with visual separators for easy debugging and monitoring.
|
10
|
+
#
|
11
|
+
# @example Basic chain inspection
|
12
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
13
|
+
# chain = result.chain
|
14
|
+
#
|
15
|
+
# ChainInspector.call(chain)
|
16
|
+
# # => "
|
17
|
+
# # chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
18
|
+
# # ================================================
|
19
|
+
# #
|
20
|
+
# # {
|
21
|
+
# # class: "ProcessOrderTask",
|
22
|
+
# # type: "Task",
|
23
|
+
# # index: 0,
|
24
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
25
|
+
# # tags: [],
|
26
|
+
# # state: "complete",
|
27
|
+
# # status: "success",
|
28
|
+
# # outcome: "success",
|
29
|
+
# # metadata: {},
|
30
|
+
# # runtime: 0.5
|
31
|
+
# # }
|
32
|
+
# #
|
33
|
+
# # ================================================
|
34
|
+
# # state: complete | status: success | outcome: success | runtime: 0.5
|
35
|
+
# # "
|
36
|
+
#
|
37
|
+
# @example Chain with multiple tasks
|
38
|
+
# class ComplexTask < CMDx::Task
|
39
|
+
# def call
|
40
|
+
# SubTask1.call(context)
|
41
|
+
# SubTask2.call(context)
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# result = ComplexTask.call
|
46
|
+
# ChainInspector.call(result.chain)
|
47
|
+
# # => Shows formatted output with all three task results and summary
|
48
|
+
#
|
49
|
+
# @example Failed chain inspection
|
50
|
+
# # When a chain contains failed tasks, the summary reflects the failure state
|
51
|
+
# ChainInspector.call(failed_chain)
|
52
|
+
# # => Shows all task results with failure information and failed summary
|
53
|
+
#
|
54
|
+
# @see CMDx::Chain Chain execution context and result tracking
|
55
|
+
# @see CMDx::Result Individual result inspection via to_h
|
56
|
+
module ChainInspector
|
57
|
+
|
58
|
+
# Keys to display in the chain summary footer.
|
59
|
+
#
|
60
|
+
# These keys represent the most important chain-level information
|
61
|
+
# that should be displayed in the summary footer for quick reference.
|
62
|
+
FOOTER_KEYS = %i[
|
63
|
+
state status outcome runtime
|
64
|
+
].freeze
|
65
|
+
|
66
|
+
module_function
|
67
|
+
|
68
|
+
# Converts a Chain instance to a comprehensive string representation.
|
69
|
+
#
|
70
|
+
# Creates a formatted summary that includes:
|
71
|
+
# - Chain header with unique ID
|
72
|
+
# - Visual separator line
|
73
|
+
# - Pretty-printed hash representation of each result
|
74
|
+
# - Visual separator line
|
75
|
+
# - Summary footer with key chain statistics
|
76
|
+
#
|
77
|
+
# @param chain [CMDx::Chain] The chain instance to inspect
|
78
|
+
# @return [String] Formatted chain summary with task details and statistics
|
79
|
+
#
|
80
|
+
# @example Single task chain
|
81
|
+
# chain = SimpleTask.call.chain
|
82
|
+
# ChainInspector.call(chain)
|
83
|
+
# # => "
|
84
|
+
# # chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
85
|
+
# # ================================================
|
86
|
+
# #
|
87
|
+
# # {
|
88
|
+
# # class: "SimpleTask",
|
89
|
+
# # type: "Task",
|
90
|
+
# # index: 0,
|
91
|
+
# # state: "complete",
|
92
|
+
# # status: "success",
|
93
|
+
# # outcome: "success",
|
94
|
+
# # runtime: 0.1
|
95
|
+
# # }
|
96
|
+
# #
|
97
|
+
# # ================================================
|
98
|
+
# # state: complete | status: success | outcome: success | runtime: 0.1
|
99
|
+
# # "
|
100
|
+
#
|
101
|
+
# @example Multi-task chain
|
102
|
+
# class ParentTask < CMDx::Task
|
103
|
+
# def call
|
104
|
+
# ChildTask1.call(context)
|
105
|
+
# ChildTask2.call(context)
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# chain = ParentTask.call.chain
|
110
|
+
# ChainInspector.call(chain)
|
111
|
+
# # => "
|
112
|
+
# # chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
113
|
+
# # ================================================
|
114
|
+
# #
|
115
|
+
# # { class: "ParentTask", index: 0, state: "complete", status: "success", ... }
|
116
|
+
# # { class: "ChildTask1", index: 1, state: "complete", status: "success", ... }
|
117
|
+
# # { class: "ChildTask2", index: 2, state: "complete", status: "success", ... }
|
118
|
+
# #
|
119
|
+
# # ================================================
|
120
|
+
# # state: complete | status: success | outcome: success | runtime: 0.5
|
121
|
+
# # "
|
122
|
+
#
|
123
|
+
# @example Failed chain inspection
|
124
|
+
# failed_chain = FailingTask.call.chain
|
125
|
+
# ChainInspector.call(failed_chain)
|
126
|
+
# # => "
|
127
|
+
# # chain: 018c2b95-b764-7615-a924-cc5b910ed1e5
|
128
|
+
# # ================================================
|
129
|
+
# #
|
130
|
+
# # { class: "FailingTask", state: "interrupted", status: "failed", metadata: { reason: "Error" }, ... }
|
131
|
+
# #
|
132
|
+
# # ================================================
|
133
|
+
# # state: interrupted | status: failed | outcome: failed | runtime: 0.1
|
134
|
+
# # "
|
135
|
+
def call(chain)
|
136
|
+
header = "\nchain: #{chain.id}"
|
137
|
+
footer = FOOTER_KEYS.map { |key| "#{key}: #{chain.send(key)}" }.join(" | ")
|
138
|
+
spacer = "=" * [header.size, footer.size].max
|
139
|
+
|
140
|
+
chain
|
141
|
+
.results
|
142
|
+
.map { |r| r.to_h.except(:chain_id).pretty_inspect }
|
143
|
+
.unshift(header, "#{spacer}\n")
|
144
|
+
.push(spacer, "#{footer}\n\n")
|
145
|
+
.join("\n")
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CMDx
|
4
|
+
# Chain serialization utility for converting Chain objects to hash representations.
|
5
|
+
#
|
6
|
+
# The ChainSerializer module provides functionality to serialize Chain instances
|
7
|
+
# into structured hash representations suitable for inspection, logging,
|
8
|
+
# debugging, and data interchange. It creates comprehensive data structures
|
9
|
+
# that include chain metadata and all associated task results.
|
10
|
+
#
|
11
|
+
# @example Basic chain serialization
|
12
|
+
# result = ProcessOrderTask.call(order_id: 123)
|
13
|
+
# chain = result.chain
|
14
|
+
#
|
15
|
+
# ChainSerializer.call(chain)
|
16
|
+
# # => {
|
17
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
18
|
+
# # state: "complete",
|
19
|
+
# # status: "success",
|
20
|
+
# # outcome: "success",
|
21
|
+
# # runtime: 0.5,
|
22
|
+
# # results: [
|
23
|
+
# # {
|
24
|
+
# # class: "ProcessOrderTask",
|
25
|
+
# # type: "Task",
|
26
|
+
# # index: 0,
|
27
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
28
|
+
# # chain_id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
29
|
+
# # tags: [],
|
30
|
+
# # state: "complete",
|
31
|
+
# # status: "success",
|
32
|
+
# # outcome: "success",
|
33
|
+
# # metadata: {},
|
34
|
+
# # runtime: 0.5
|
35
|
+
# # }
|
36
|
+
# # ]
|
37
|
+
# # }
|
38
|
+
#
|
39
|
+
# @example Chain with multiple tasks
|
40
|
+
# class ComplexTask < CMDx::Task
|
41
|
+
# def call
|
42
|
+
# SubTask1.call(context)
|
43
|
+
# SubTask2.call(context)
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# result = ComplexTask.call
|
48
|
+
# chain = result.chain
|
49
|
+
#
|
50
|
+
# ChainSerializer.call(chain)
|
51
|
+
# # => {
|
52
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
53
|
+
# # state: "complete",
|
54
|
+
# # status: "success",
|
55
|
+
# # outcome: "success",
|
56
|
+
# # runtime: 1.2,
|
57
|
+
# # results: [
|
58
|
+
# # { class: "ComplexTask", index: 0, state: "complete", status: "success", ... },
|
59
|
+
# # { class: "SubTask1", index: 1, state: "complete", status: "success", ... },
|
60
|
+
# # { class: "SubTask2", index: 2, state: "complete", status: "success", ... }
|
61
|
+
# # ]
|
62
|
+
# # }
|
63
|
+
#
|
64
|
+
# @example Failed chain serialization
|
65
|
+
# failed_result = FailingTask.call
|
66
|
+
# failed_chain = failed_result.chain
|
67
|
+
#
|
68
|
+
# ChainSerializer.call(failed_chain)
|
69
|
+
# # => {
|
70
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
71
|
+
# # state: "interrupted",
|
72
|
+
# # status: "failed",
|
73
|
+
# # outcome: "failed",
|
74
|
+
# # runtime: 0.1,
|
75
|
+
# # results: [
|
76
|
+
# # {
|
77
|
+
# # class: "FailingTask",
|
78
|
+
# # state: "interrupted",
|
79
|
+
# # status: "failed",
|
80
|
+
# # outcome: "failed",
|
81
|
+
# # metadata: { reason: "Something went wrong" },
|
82
|
+
# # runtime: 0.1,
|
83
|
+
# # ...
|
84
|
+
# # }
|
85
|
+
# # ]
|
86
|
+
# # }
|
87
|
+
#
|
88
|
+
# @see CMDx::Chain Chain execution context and result tracking
|
89
|
+
# @see CMDx::ResultSerializer Individual result serialization
|
90
|
+
# @see CMDx::ChainInspector Human-readable chain formatting
|
91
|
+
module ChainSerializer
|
92
|
+
|
93
|
+
module_function
|
94
|
+
|
95
|
+
# Converts a Chain object to a hash representation.
|
96
|
+
#
|
97
|
+
# Serializes a Chain instance into a structured hash containing chain metadata
|
98
|
+
# and all associated task results. The chain-level data is derived from the
|
99
|
+
# first result in the collection, while all individual results are included
|
100
|
+
# in their full serialized form.
|
101
|
+
#
|
102
|
+
# @param chain [CMDx::Chain] The chain object to serialize
|
103
|
+
# @return [Hash] Structured hash representation of the chain and all results
|
104
|
+
#
|
105
|
+
# @example Simple chain serialization
|
106
|
+
# chain = SimpleTask.call.chain
|
107
|
+
# ChainSerializer.call(chain)
|
108
|
+
# # => {
|
109
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
110
|
+
# # state: "complete",
|
111
|
+
# # status: "success",
|
112
|
+
# # outcome: "success",
|
113
|
+
# # runtime: 0.1,
|
114
|
+
# # results: [
|
115
|
+
# # {
|
116
|
+
# # class: "SimpleTask",
|
117
|
+
# # type: "Task",
|
118
|
+
# # index: 0,
|
119
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
120
|
+
# # chain_id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
121
|
+
# # tags: [],
|
122
|
+
# # state: "complete",
|
123
|
+
# # status: "success",
|
124
|
+
# # outcome: "success",
|
125
|
+
# # metadata: {},
|
126
|
+
# # runtime: 0.1
|
127
|
+
# # }
|
128
|
+
# # ]
|
129
|
+
# # }
|
130
|
+
#
|
131
|
+
# @example Multi-task chain serialization
|
132
|
+
# class ParentTask < CMDx::Task
|
133
|
+
# def call
|
134
|
+
# ChildTask.call(context)
|
135
|
+
# end
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# chain = ParentTask.call.chain
|
139
|
+
# ChainSerializer.call(chain)
|
140
|
+
# # => {
|
141
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
142
|
+
# # state: "complete", # From first result (ParentTask)
|
143
|
+
# # status: "success", # From first result (ParentTask)
|
144
|
+
# # outcome: "success", # From first result (ParentTask)
|
145
|
+
# # runtime: 0.5, # From first result (ParentTask)
|
146
|
+
# # results: [
|
147
|
+
# # { class: "ParentTask", index: 0, ... },
|
148
|
+
# # { class: "ChildTask", index: 1, ... }
|
149
|
+
# # ]
|
150
|
+
# # }
|
151
|
+
#
|
152
|
+
# @example Empty chain serialization
|
153
|
+
# empty_chain = Chain.new
|
154
|
+
# ChainSerializer.call(empty_chain)
|
155
|
+
# # => {
|
156
|
+
# # id: "018c2b95-b764-7615-a924-cc5b910ed1e5",
|
157
|
+
# # state: nil,
|
158
|
+
# # status: nil,
|
159
|
+
# # outcome: nil,
|
160
|
+
# # runtime: nil,
|
161
|
+
# # results: []
|
162
|
+
# # }
|
163
|
+
def call(chain)
|
164
|
+
{
|
165
|
+
id: chain.id,
|
166
|
+
state: chain.state,
|
167
|
+
status: chain.status,
|
168
|
+
outcome: chain.outcome,
|
169
|
+
runtime: chain.runtime,
|
170
|
+
results: chain.results.map(&:to_h)
|
171
|
+
}
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|
data/lib/cmdx/coercions/array.rb
CHANGED
@@ -2,10 +2,47 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module Coercions
|
5
|
+
# Coerces values to Array type.
|
6
|
+
#
|
7
|
+
# The Array coercion converts parameter values to Array objects,
|
8
|
+
# with special handling for JSON-formatted strings and general
|
9
|
+
# array conversion using Ruby's Array() method.
|
10
|
+
#
|
11
|
+
# @example Basic array coercion
|
12
|
+
# class ProcessOrderTask < CMDx::Task
|
13
|
+
# optional :tags, type: :array, default: []
|
14
|
+
# optional :item_ids, type: :array
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Coercion behavior
|
18
|
+
# Coercions::Array.call([1, 2, 3]) # => [1, 2, 3]
|
19
|
+
# Coercions::Array.call("hello") # => ["hello"]
|
20
|
+
# Coercions::Array.call('["a","b"]') # => ["a", "b"] (JSON)
|
21
|
+
# Coercions::Array.call('[1,2,3]') # => [1, 2, 3] (JSON)
|
22
|
+
# Coercions::Array.call(nil) # => []
|
23
|
+
# Coercions::Array.call(42) # => [42]
|
24
|
+
#
|
25
|
+
# @see ParameterValue Parameter value coercion
|
26
|
+
# @see Parameter Parameter type definitions
|
5
27
|
module Array
|
6
28
|
|
7
29
|
module_function
|
8
30
|
|
31
|
+
# Coerce a value to Array.
|
32
|
+
#
|
33
|
+
# If the value is a JSON-formatted string (starts with '['), it will
|
34
|
+
# be parsed as JSON. Otherwise, it uses Ruby's Array() method for
|
35
|
+
# general array conversion.
|
36
|
+
#
|
37
|
+
# @param value [Object] value to coerce to array
|
38
|
+
# @param _options [Hash] coercion options (unused)
|
39
|
+
# @return [Array] coerced array value
|
40
|
+
# @raise [JSON::ParserError] if JSON parsing fails
|
41
|
+
#
|
42
|
+
# @example
|
43
|
+
# Coercions::Array.call("test") # => ["test"]
|
44
|
+
# Coercions::Array.call('["a","b"]') # => ["a", "b"]
|
45
|
+
# Coercions::Array.call([1, 2]) # => [1, 2]
|
9
46
|
def call(value, _options = {})
|
10
47
|
if value.is_a?(::String) && value.start_with?("[")
|
11
48
|
JSON.parse(value)
|
@@ -2,12 +2,45 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module Coercions
|
5
|
+
# Coerces values to BigDecimal type.
|
6
|
+
#
|
7
|
+
# The BigDecimal coercion converts parameter values to BigDecimal objects
|
8
|
+
# for high-precision decimal arithmetic. Supports configurable precision
|
9
|
+
# and handles various numeric input formats.
|
10
|
+
#
|
11
|
+
# @example Basic BigDecimal coercion
|
12
|
+
# class ProcessOrderTask < CMDx::Task
|
13
|
+
# required :total_amount, type: :big_decimal
|
14
|
+
# optional :tax_rate, type: :big_decimal, precision: 4
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Coercion behavior
|
18
|
+
# Coercions::BigDecimal.call("123.45") # => #<BigDecimal:...,'0.12345E3',18(27)>
|
19
|
+
# Coercions::BigDecimal.call(42) # => #<BigDecimal:...,'0.42E2',9(18)>
|
20
|
+
# Coercions::BigDecimal.call("0.333333", precision: 6) # Custom precision
|
21
|
+
#
|
22
|
+
# @see ParameterValue Parameter value coercion
|
23
|
+
# @see Parameter Parameter type definitions
|
5
24
|
module BigDecimal
|
6
25
|
|
26
|
+
# Default precision for BigDecimal calculations
|
27
|
+
# @return [Integer] default precision value
|
7
28
|
DEFAULT_PRECISION = 14
|
8
29
|
|
9
30
|
module_function
|
10
31
|
|
32
|
+
# Coerce a value to BigDecimal.
|
33
|
+
#
|
34
|
+
# @param value [Object] value to coerce to BigDecimal
|
35
|
+
# @param options [Hash] coercion options
|
36
|
+
# @option options [Integer] :precision decimal precision (default: 14)
|
37
|
+
# @return [BigDecimal] coerced BigDecimal value
|
38
|
+
# @raise [CoercionError] if coercion fails
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Coercions::BigDecimal.call("123.45") # => BigDecimal with default precision
|
42
|
+
# Coercions::BigDecimal.call("0.333", precision: 10) # => BigDecimal with custom precision
|
43
|
+
# Coercions::BigDecimal.call(42.5) # => BigDecimal from float
|
11
44
|
def call(value, options = {})
|
12
45
|
BigDecimal(value, options[:precision] || DEFAULT_PRECISION)
|
13
46
|
rescue ArgumentError, TypeError
|
@@ -2,15 +2,55 @@
|
|
2
2
|
|
3
3
|
module CMDx
|
4
4
|
module Coercions
|
5
|
+
# Coerces values to Boolean type (true/false).
|
6
|
+
#
|
7
|
+
# The Boolean coercion converts parameter values to true or false
|
8
|
+
# based on string pattern matching for common boolean representations.
|
9
|
+
# It handles various textual representations of true and false values.
|
10
|
+
#
|
11
|
+
# @example Basic boolean coercion
|
12
|
+
# class ProcessOrderTask < CMDx::Task
|
13
|
+
# optional :send_email, type: :boolean, default: true
|
14
|
+
# optional :is_urgent, type: :boolean, default: false
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Coercion behavior
|
18
|
+
# Coercions::Boolean.call("true") # => true
|
19
|
+
# Coercions::Boolean.call("yes") # => true
|
20
|
+
# Coercions::Boolean.call("1") # => true
|
21
|
+
# Coercions::Boolean.call("false") # => false
|
22
|
+
# Coercions::Boolean.call("no") # => false
|
23
|
+
# Coercions::Boolean.call("0") # => false
|
24
|
+
# Coercions::Boolean.call("invalid") # => raises CoercionError
|
25
|
+
#
|
26
|
+
# @see ParameterValue Parameter value coercion
|
27
|
+
# @see Parameter Parameter type definitions
|
5
28
|
module Boolean
|
6
29
|
|
30
|
+
# Pattern matching false-like values (case insensitive)
|
31
|
+
# @return [Regexp] regex for falsey string values
|
7
32
|
FALSEY = /^(false|f|no|n|0)$/i
|
33
|
+
|
34
|
+
# Pattern matching true-like values (case insensitive)
|
35
|
+
# @return [Regexp] regex for truthy string values
|
8
36
|
TRUTHY = /^(true|t|yes|y|1)$/i
|
9
37
|
|
10
38
|
module_function
|
11
39
|
|
40
|
+
# Coerce a value to Boolean.
|
41
|
+
#
|
42
|
+
# @param value [Object] value to coerce to boolean
|
43
|
+
# @param _options [Hash] coercion options (unused)
|
44
|
+
# @return [Boolean] coerced boolean value (true or false)
|
45
|
+
# @raise [CoercionError] if value cannot be coerced to boolean
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# Coercions::Boolean.call("yes") # => true
|
49
|
+
# Coercions::Boolean.call("False") # => false
|
50
|
+
# Coercions::Boolean.call("1") # => true
|
51
|
+
# Coercions::Boolean.call("0") # => false
|
12
52
|
def call(value, _options = {})
|
13
|
-
case value.to_s
|
53
|
+
case value.to_s.downcase
|
14
54
|
when FALSEY then false
|
15
55
|
when TRUTHY then true
|
16
56
|
else
|