cmdx 1.7.0 → 1.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/LLM.md +45 -6
- data/README.md +57 -20
- data/docs/getting_started.md +26 -0
- data/docs/logging.md +4 -6
- data/docs/outcomes/result.md +17 -0
- data/lib/cmdx/middlewares/correlate.rb +3 -4
- data/lib/cmdx/task.rb +7 -6
- data/lib/cmdx/version.rb +1 -1
- data/lib/generators/cmdx/locale_generator.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 528f3b3b1d4f23045a295cc15c05106c3bd0f9efde6400bb88c01bb2a6455637
|
4
|
+
data.tar.gz: 473ca317fd214b48e4e4988ba4358bdcff4a3fc9f9805300dba292f4401e51c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b48be81fa184343f76c1891a3b87c25853e0dc859764dbf92946c7b31d69fd4af85b75eee7720470e29949edb395bce668343dbc6b17916079ba9befd1fbc80
|
7
|
+
data.tar.gz: b95fe96f12cf2aeb724782808d80b4ee36e083d0f6c60876edac45a6046c7925437f329dc2f98a42432b4794996c3269f71ba7f93452f161a38f365c6d40afa6
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
|
7
7
|
## [TODO]
|
8
8
|
|
9
|
+
## [1.7.2] - 2025-09-03
|
10
|
+
|
11
|
+
### Changes
|
12
|
+
- Correlation ID is set before continuing to further steps
|
13
|
+
|
14
|
+
## [1.7.1] - 2025-08-26
|
15
|
+
|
16
|
+
### Added
|
17
|
+
- Yield result if block given to `execute` and `execute!` methods
|
18
|
+
|
9
19
|
## [1.7.0] - 2025-08-25
|
10
20
|
|
11
21
|
### Added
|
data/LLM.md
CHANGED
@@ -11,6 +11,31 @@ url: https://github.com/drexed/cmdx/blob/main/docs/getting_started.md
|
|
11
11
|
|
12
12
|
CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. Design robust workflows with automatic attribute validation, structured error handling, comprehensive logging, and intelligent execution flow control.
|
13
13
|
|
14
|
+
**Common Challenges:**
|
15
|
+
|
16
|
+
- Inconsistent patterns across implementations
|
17
|
+
- Minimal or no logging, making debugging painful
|
18
|
+
- Fragile designs that erode developer confidence
|
19
|
+
|
20
|
+
**CMDx Solutions:**
|
21
|
+
|
22
|
+
- Establishes a consistent, standardized design
|
23
|
+
- Provides flow control and error handling
|
24
|
+
- Supports composable, reusable workflows
|
25
|
+
- Includes detailed logging for observability
|
26
|
+
- Defines input attributes with fallback defaults
|
27
|
+
- Adds validations and type coercions
|
28
|
+
- Plus many other developer-friendly tools
|
29
|
+
|
30
|
+
## Compose, Execute, React, Observe pattern
|
31
|
+
|
32
|
+
CMDx encourages breaking business logic into composable tasks. Each task can be combined into larger workflows, executed with standardized flow control, and fully observed through logging, validations, and context.
|
33
|
+
|
34
|
+
- **Compose** → Define small, contract-driven tasks with typed attributes, validations, and natural workflow composition.
|
35
|
+
- **Execute** → Run tasks with clear outcomes, intentional halts, and pluggable behaviors via middlewares and callbacks.
|
36
|
+
- **React** → Adapt to outcomes by chaining follow-up tasks, handling faults, or shaping future flows.
|
37
|
+
- **Observe** → Capture immutable results, structured logs, and full execution chains for reliable tracing and insight.
|
38
|
+
|
14
39
|
## Installation
|
15
40
|
|
16
41
|
Add CMDx to your Gemfile:
|
@@ -1251,6 +1276,22 @@ result.index #=> 0 (first task in chain)
|
|
1251
1276
|
result.chain.results[result.index] == result #=> true
|
1252
1277
|
```
|
1253
1278
|
|
1279
|
+
## Block Yield
|
1280
|
+
|
1281
|
+
Implement conditional logic using a block expression that yields a result for complete encapsulation.
|
1282
|
+
|
1283
|
+
```ruby
|
1284
|
+
BuildApplication.execute(version: "1.2.3") do |result|
|
1285
|
+
if result.success?
|
1286
|
+
notify_deployment_ready(result)
|
1287
|
+
elsif result.failed?
|
1288
|
+
handle_build_failure(result)
|
1289
|
+
else
|
1290
|
+
log_skip_reason(result)
|
1291
|
+
end
|
1292
|
+
end
|
1293
|
+
```
|
1294
|
+
|
1254
1295
|
## Handlers
|
1255
1296
|
|
1256
1297
|
Use result handlers for clean, functional-style conditional logic. Handlers return the result object, enabling method chaining and fluent interfaces.
|
@@ -2727,21 +2768,19 @@ Sample output:
|
|
2727
2768
|
```log
|
2728
2769
|
<!-- Success (INFO level) -->
|
2729
2770
|
I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
|
2730
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task"
|
2731
|
-
class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
2771
|
+
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
2732
2772
|
|
2733
2773
|
<!-- Skipped (WARN level) -->
|
2734
2774
|
W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
|
2735
|
-
index=1 state="interrupted" status="skipped" reason="Customer already validated"
|
2775
|
+
index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
|
2736
2776
|
|
2737
2777
|
<!-- Failed (ERROR level) -->
|
2738
2778
|
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
|
2739
|
-
index=2 state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
2779
|
+
index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
2740
2780
|
|
2741
2781
|
<!-- Failed Chain -->
|
2742
2782
|
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
|
2743
|
-
caused_failure={index: 2, class: "CalculateTax", status: "failed"}
|
2744
|
-
threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
2783
|
+
index=3 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="BillingWorkflow" state="interrupted" status="failed" caused_failure={index: 2, class: "CalculateTax", status: "failed"} threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
2745
2784
|
```
|
2746
2785
|
|
2747
2786
|
> [!TIP]
|
data/README.md
CHANGED
@@ -8,16 +8,24 @@
|
|
8
8
|
<img alt="License" src="https://img.shields.io/github/license/drexed/cmdx">
|
9
9
|
</p>
|
10
10
|
|
11
|
-
# CMDx
|
11
|
+
# 🚀 CMDx — Business logic without the chaos
|
12
12
|
|
13
|
-
|
13
|
+
Stop wrestling with messy service objects. CMDx gives you a clean, consistent way to design business processes:
|
14
14
|
|
15
|
-
-
|
16
|
-
-
|
17
|
-
-
|
18
|
-
|
19
|
-
|
20
|
-
|
15
|
+
- Start small with a single `work` method
|
16
|
+
- Scale to complex tasks and multi-step workflows
|
17
|
+
- Get built-in flow control, logging, validations, and more...
|
18
|
+
|
19
|
+
*Build faster. Debug easier. Stay sane.*
|
20
|
+
|
21
|
+
## Compose, Execute, React, Observe pattern
|
22
|
+
|
23
|
+
CMDx encourages breaking business logic into composable tasks. Each task can be combined into larger workflows, executed with standardized flow control, and fully observed through logging, validations, and context.
|
24
|
+
|
25
|
+
- **Compose** → Define small, contract-driven tasks with typed attributes, validations, and natural workflow composition.
|
26
|
+
- **Execute** → Run tasks with clear outcomes, intentional halts, and pluggable behaviors via middlewares and callbacks.
|
27
|
+
- **React** → Adapt to outcomes by chaining follow-up tasks, handling faults, or shaping future flows.
|
28
|
+
- **Observe** → Capture immutable results, structured logs, and full execution chains for reliable tracing and insight.
|
21
29
|
|
22
30
|
## Installation
|
23
31
|
|
@@ -37,11 +45,24 @@ Or install it yourself as:
|
|
37
45
|
|
38
46
|
## Quick Example
|
39
47
|
|
40
|
-
Here's how a quick
|
48
|
+
Here's how a quick 4 step process can open up a world of possibilities:
|
49
|
+
|
50
|
+
### 1. Compose
|
51
|
+
|
52
|
+
#### Minimum Viable Task
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class SendAnalyzedEmail < CMDx::Task
|
56
|
+
def work
|
57
|
+
user = User.find(context.user_id)
|
58
|
+
MetricsMailer.analyzed(user).deliver_now
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
#### Fully Featured Task
|
41
64
|
|
42
65
|
```ruby
|
43
|
-
# 1. Setup task
|
44
|
-
# ---------------------------------
|
45
66
|
class AnalyzeMetrics < CMDx::Task
|
46
67
|
register :middleware, CMDx::Middlewares::Correlate, id: -> { Current.request_id }
|
47
68
|
|
@@ -56,8 +77,10 @@ class AnalyzeMetrics < CMDx::Task
|
|
56
77
|
elsif dataset.unprocessed?
|
57
78
|
skip!("Dataset not ready for analysis")
|
58
79
|
else
|
59
|
-
context.result = PValueAnalyzer.
|
80
|
+
context.result = PValueAnalyzer.execute(dataset:, analysis_type:)
|
60
81
|
context.analyzed_at = Time.now
|
82
|
+
|
83
|
+
SendAnalyzedEmail.execute(user_id: Current.account.manager_id)
|
61
84
|
end
|
62
85
|
end
|
63
86
|
|
@@ -71,16 +94,20 @@ class AnalyzeMetrics < CMDx::Task
|
|
71
94
|
dataset.update!(analysis_result_id: context.result.id)
|
72
95
|
end
|
73
96
|
end
|
97
|
+
```
|
74
98
|
|
75
|
-
|
76
|
-
|
99
|
+
### 2. Execute
|
100
|
+
|
101
|
+
```ruby
|
77
102
|
result = AnalyzeMetrics.execute(
|
78
103
|
dataset_id: 123,
|
79
104
|
"analysis_type" => "advanced"
|
80
105
|
)
|
106
|
+
```
|
81
107
|
|
82
|
-
|
83
|
-
|
108
|
+
### 3. React
|
109
|
+
|
110
|
+
```ruby
|
84
111
|
if result.success?
|
85
112
|
puts "Metrics analyzed at #{result.context.analyzed_at}"
|
86
113
|
elsif result.skipped?
|
@@ -90,6 +117,16 @@ elsif result.failed?
|
|
90
117
|
end
|
91
118
|
```
|
92
119
|
|
120
|
+
### 4. Observe
|
121
|
+
|
122
|
+
```log
|
123
|
+
I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
|
124
|
+
index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
|
125
|
+
|
126
|
+
I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
|
127
|
+
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
|
128
|
+
```
|
129
|
+
|
93
130
|
## Table of contents
|
94
131
|
|
95
132
|
- [Getting Started](docs/getting_started.md)
|
@@ -122,12 +159,12 @@ end
|
|
122
159
|
|
123
160
|
## Ecosystem
|
124
161
|
|
125
|
-
- [cmdx-
|
126
|
-
- [cmdx-parallel](https://github.com/drexed/cmdx-parallel) - Parallel workflow tasks
|
162
|
+
- [cmdx-rspec](https://github.com/drexed/cmdx-rspec) - RSpec test matchers
|
127
163
|
|
128
|
-
|
164
|
+
For backwards compatibility of certain functionality:
|
129
165
|
|
130
|
-
-
|
166
|
+
- [cmdx-i18n](https://github.com/drexed/cmdx-i18n) - 85+ translations, `v1.5.0` - `v1.6.2`
|
167
|
+
- [cmdx-parallel](https://github.com/drexed/cmdx-parallel) - Parallel workflow tasks, `v1.6.1` - `v1.6.2`
|
131
168
|
|
132
169
|
## Development
|
133
170
|
|
data/docs/getting_started.md
CHANGED
@@ -2,8 +2,25 @@
|
|
2
2
|
|
3
3
|
CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. Design robust workflows with automatic attribute validation, structured error handling, comprehensive logging, and intelligent execution flow control.
|
4
4
|
|
5
|
+
**Common Challenges:**
|
6
|
+
|
7
|
+
- Inconsistent patterns across implementations
|
8
|
+
- Minimal or no logging, making debugging painful
|
9
|
+
- Fragile designs that erode developer confidence
|
10
|
+
|
11
|
+
**CMDx Solutions:**
|
12
|
+
|
13
|
+
- Establishes a consistent, standardized design
|
14
|
+
- Provides flow control and error handling
|
15
|
+
- Supports composable, reusable workflows
|
16
|
+
- Includes detailed logging for observability
|
17
|
+
- Defines input attributes with fallback defaults
|
18
|
+
- Adds validations and type coercions
|
19
|
+
- Plus many other developer-friendly tools
|
20
|
+
|
5
21
|
## Table of Contents
|
6
22
|
|
23
|
+
- [Compose, Execute, React, Observe pattern](#compose-execute-react-observe-pattern)
|
7
24
|
- [Installation](#installation)
|
8
25
|
- [Configuration Hierarchy](#configuration-hierarchy)
|
9
26
|
- [Global Configuration](#global-configuration)
|
@@ -21,6 +38,15 @@ CMDx is a Ruby framework for building maintainable, observable business logic th
|
|
21
38
|
- [Resetting](#resetting)
|
22
39
|
- [Task Generator](#task-generator)
|
23
40
|
|
41
|
+
## Compose, Execute, React, Observe pattern
|
42
|
+
|
43
|
+
CMDx encourages breaking business logic into composable tasks. Each task can be combined into larger workflows, executed with standardized flow control, and fully observed through logging, validations, and context.
|
44
|
+
|
45
|
+
- *Compose* → Define small, contract-driven tasks with typed attributes, validations, and natural workflow composition.
|
46
|
+
- *Execute* → Run tasks with clear outcomes, intentional halts, and pluggable behaviors via middlewares and callbacks.
|
47
|
+
- *React* → Adapt to outcomes by chaining follow-up tasks, handling faults, or shaping future flows.
|
48
|
+
- *Observe* → Capture immutable results, structured logs, and full execution chains for reliable tracing and insight.
|
49
|
+
|
24
50
|
## Installation
|
25
51
|
|
26
52
|
Add CMDx to your Gemfile:
|
data/docs/logging.md
CHANGED
@@ -25,21 +25,19 @@ Sample output:
|
|
25
25
|
```log
|
26
26
|
<!-- Success (INFO level) -->
|
27
27
|
I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
|
28
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task"
|
29
|
-
class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
28
|
+
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
30
29
|
|
31
30
|
<!-- Skipped (WARN level) -->
|
32
31
|
W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
|
33
|
-
index=1 state="interrupted" status="skipped" reason="Customer already validated"
|
32
|
+
index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
|
34
33
|
|
35
34
|
<!-- Failed (ERROR level) -->
|
36
35
|
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
|
37
|
-
index=2 state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
36
|
+
index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
38
37
|
|
39
38
|
<!-- Failed Chain -->
|
40
39
|
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
|
41
|
-
caused_failure={index: 2, class: "CalculateTax", status: "failed"}
|
42
|
-
threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
40
|
+
index=3 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="BillingWorkflow" state="interrupted" status="failed" caused_failure={index: 2, class: "CalculateTax", status: "failed"} threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
43
41
|
```
|
44
42
|
|
45
43
|
> [!TIP]
|
data/docs/outcomes/result.md
CHANGED
@@ -9,6 +9,7 @@ The result object is the comprehensive return value of task execution, providing
|
|
9
9
|
- [Outcome Analysis](#outcome-analysis)
|
10
10
|
- [Chain Analysis](#chain-analysis)
|
11
11
|
- [Index and Position](#index-and-position)
|
12
|
+
- [Block Yield](#block-yield)
|
12
13
|
- [Handlers](#handlers)
|
13
14
|
- [Pattern Matching](#pattern-matching)
|
14
15
|
- [Array Pattern](#array-pattern)
|
@@ -113,6 +114,22 @@ result.index #=> 0 (first task in chain)
|
|
113
114
|
result.chain.results[result.index] == result #=> true
|
114
115
|
```
|
115
116
|
|
117
|
+
## Block Yield
|
118
|
+
|
119
|
+
Implement conditional logic using a block expression that yields a result for complete encapsulation.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
BuildApplication.execute(version: "1.2.3") do |result|
|
123
|
+
if result.success?
|
124
|
+
notify_deployment_ready(result)
|
125
|
+
elsif result.failed?
|
126
|
+
handle_build_failure(result)
|
127
|
+
else
|
128
|
+
log_skip_reason(result)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
```
|
132
|
+
|
116
133
|
## Handlers
|
117
134
|
|
118
135
|
Use result handlers for clean, functional-style conditional logic. Handlers return the result object, enabling method chaining and fluent interfaces.
|
@@ -95,7 +95,8 @@ module CMDx
|
|
95
95
|
def call(task, **options, &)
|
96
96
|
return yield unless Utils::Condition.evaluate(task, options)
|
97
97
|
|
98
|
-
correlation_id =
|
98
|
+
correlation_id = task.result.metadata[:correlation_id] ||=
|
99
|
+
id ||
|
99
100
|
case callable = options[:id]
|
100
101
|
when Symbol then task.send(callable)
|
101
102
|
when Proc then task.instance_eval(&callable)
|
@@ -107,9 +108,7 @@ module CMDx
|
|
107
108
|
end
|
108
109
|
end
|
109
110
|
|
110
|
-
|
111
|
-
task.result.metadata[:correlation_id] = correlation_id
|
112
|
-
result
|
111
|
+
use(correlation_id, &)
|
113
112
|
end
|
114
113
|
|
115
114
|
end
|
data/lib/cmdx/task.rb
CHANGED
@@ -169,10 +169,10 @@ module CMDx
|
|
169
169
|
# if result.success?
|
170
170
|
# puts "Task completed successfully"
|
171
171
|
# end
|
172
|
-
def execute(
|
173
|
-
task = new(
|
172
|
+
def execute(*args, **kwargs)
|
173
|
+
task = new(*args, **kwargs)
|
174
174
|
task.execute(raise: false)
|
175
|
-
task.result
|
175
|
+
block_given? ? yield(task.result) : task.result
|
176
176
|
end
|
177
177
|
|
178
178
|
# @param args [Array] Arguments to pass to the task constructor
|
@@ -184,10 +184,10 @@ module CMDx
|
|
184
184
|
# @example
|
185
185
|
# result = MyTask.execute!(name: "example")
|
186
186
|
# # Will raise an exception if execution fails
|
187
|
-
def execute!(
|
188
|
-
task = new(
|
187
|
+
def execute!(*args, **kwargs)
|
188
|
+
task = new(*args, **kwargs)
|
189
189
|
task.execute(raise: true)
|
190
|
-
task.result
|
190
|
+
block_given? ? yield(task.result) : task.result
|
191
191
|
end
|
192
192
|
|
193
193
|
end
|
@@ -201,6 +201,7 @@ module CMDx
|
|
201
201
|
# result = task.execute(raise: true)
|
202
202
|
def execute(raise: false)
|
203
203
|
Executor.execute(self, raise:)
|
204
|
+
block_given? ? yield(result) : result
|
204
205
|
end
|
205
206
|
|
206
207
|
# @raise [UndefinedMethodError] Always raised as this method must be overridden
|
data/lib/cmdx/version.rb
CHANGED
@@ -10,7 +10,7 @@ module Cmdx
|
|
10
10
|
|
11
11
|
source_root File.expand_path("../../locales", __dir__)
|
12
12
|
|
13
|
-
desc "Copies the locale with the given
|
13
|
+
desc "Copies the locale with the given ISO 639 code"
|
14
14
|
|
15
15
|
argument :locale, type: :string, default: "en", banner: "locale: en, es, fr, etc"
|
16
16
|
|