cmdx 1.7.1 → 1.7.3
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 +11 -0
- data/LLM.md +41 -14
- data/README.md +57 -20
- data/docs/attributes/coercions.md +2 -1
- data/docs/attributes/definitions.md +4 -2
- data/docs/attributes/validations.md +2 -1
- data/docs/getting_started.md +26 -0
- data/docs/interruptions/halt.md +4 -4
- data/docs/logging.md +4 -6
- data/lib/cmdx/executor.rb +5 -1
- data/lib/cmdx/middlewares/correlate.rb +3 -4
- data/lib/cmdx/version.rb +1 -1
- data/lib/generators/cmdx/locale_generator.rb +1 -1
- data/lib/locales/af.yml +2 -1
- data/lib/locales/ar.yml +1 -0
- data/lib/locales/az.yml +2 -1
- data/lib/locales/be.yml +2 -1
- data/lib/locales/bg.yml +2 -1
- data/lib/locales/bn.yml +1 -0
- data/lib/locales/bs.yml +2 -1
- data/lib/locales/ca.yml +2 -1
- data/lib/locales/cnr.yml +2 -1
- data/lib/locales/cs.yml +2 -1
- data/lib/locales/cy.yml +2 -1
- data/lib/locales/da.yml +2 -1
- data/lib/locales/de.yml +2 -1
- data/lib/locales/dz.yml +1 -0
- data/lib/locales/el.yml +2 -1
- data/lib/locales/en.yml +2 -1
- data/lib/locales/eo.yml +2 -1
- data/lib/locales/es.yml +2 -1
- data/lib/locales/et.yml +2 -1
- data/lib/locales/eu.yml +2 -1
- data/lib/locales/fa.yml +1 -0
- data/lib/locales/fi.yml +2 -1
- data/lib/locales/fr.yml +2 -1
- data/lib/locales/fy.yml +2 -1
- data/lib/locales/gd.yml +2 -1
- data/lib/locales/gl.yml +2 -1
- data/lib/locales/he.yml +1 -0
- data/lib/locales/hi.yml +1 -0
- data/lib/locales/hr.yml +2 -1
- data/lib/locales/hu.yml +2 -1
- data/lib/locales/hy.yml +2 -1
- data/lib/locales/id.yml +2 -1
- data/lib/locales/is.yml +2 -1
- data/lib/locales/it.yml +2 -1
- data/lib/locales/ja.yml +1 -0
- data/lib/locales/ka.yml +1 -0
- data/lib/locales/kk.yml +2 -1
- data/lib/locales/km.yml +1 -0
- data/lib/locales/kn.yml +1 -0
- data/lib/locales/ko.yml +1 -0
- data/lib/locales/lb.yml +2 -1
- data/lib/locales/lo.yml +1 -0
- data/lib/locales/lt.yml +2 -1
- data/lib/locales/lv.yml +2 -1
- data/lib/locales/mg.yml +2 -1
- data/lib/locales/mk.yml +2 -1
- data/lib/locales/ml.yml +1 -0
- data/lib/locales/mn.yml +2 -1
- data/lib/locales/mr-IN.yml +1 -0
- data/lib/locales/ms.yml +2 -1
- data/lib/locales/nb.yml +2 -1
- data/lib/locales/ne.yml +1 -0
- data/lib/locales/nl.yml +2 -1
- data/lib/locales/nn.yml +2 -1
- data/lib/locales/oc.yml +2 -1
- data/lib/locales/or.yml +1 -0
- data/lib/locales/pa.yml +1 -0
- data/lib/locales/pl.yml +2 -1
- data/lib/locales/pt.yml +2 -1
- data/lib/locales/rm.yml +2 -1
- data/lib/locales/ro.yml +2 -1
- data/lib/locales/ru.yml +2 -1
- data/lib/locales/sc.yml +2 -1
- data/lib/locales/sk.yml +2 -1
- data/lib/locales/sl.yml +2 -1
- data/lib/locales/sq.yml +2 -1
- data/lib/locales/sr.yml +2 -1
- data/lib/locales/st.yml +2 -1
- data/lib/locales/sv.yml +2 -1
- data/lib/locales/sw.yml +2 -1
- data/lib/locales/ta.yml +1 -0
- data/lib/locales/te.yml +1 -0
- data/lib/locales/th.yml +1 -0
- data/lib/locales/tl.yml +2 -1
- data/lib/locales/tr.yml +2 -1
- data/lib/locales/tt.yml +2 -1
- data/lib/locales/ug.yml +1 -0
- data/lib/locales/uk.yml +2 -1
- data/lib/locales/ur.yml +1 -0
- data/lib/locales/uz.yml +2 -1
- data/lib/locales/vi.yml +2 -1
- data/lib/locales/wo.yml +2 -1
- data/lib/locales/zh-CN.yml +1 -0
- data/lib/locales/zh-HK.yml +1 -0
- data/lib/locales/zh-TW.yml +1 -0
- data/lib/locales/zh-YUE.yml +1 -0
- 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: 01fd49f8d31e90b2818d78cbf452c68888f76e8f4983647b1c163b235963b6a8
|
4
|
+
data.tar.gz: ab15559fbfedd9f359f23fbce24b7259ed428ef37caeb5522777de0724f395a8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f5a8da6a5f773f69d2e25fb856f8440ec630c7d049493b0bf1478719ef4ac17f2d1bfc82f2718030de3883cf91db43986d0826b4bcfbfed7ceaaf1f7b5ba1c6f
|
7
|
+
data.tar.gz: c36629bdbe94b2ce563e67f0d1bcaf9c292d180f30a22871a10480d11676714819f2636f463f538916f72c56200170f4ab39e8d9a84c28479a41a5310f7fdd12
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,17 @@ 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.3] - 2025-09-03
|
10
|
+
|
11
|
+
### Changes
|
12
|
+
- Return generic validation reason
|
13
|
+
- Move validation full message string to `:full_message` within metadata
|
14
|
+
|
15
|
+
## [1.7.2] - 2025-09-03
|
16
|
+
|
17
|
+
### Changes
|
18
|
+
- Correlation ID is set before continuing to further steps
|
19
|
+
|
9
20
|
## [1.7.1] - 2025-08-26
|
10
21
|
|
11
22
|
### 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:
|
@@ -758,7 +783,7 @@ result = ProcessInventory.execute(inventory_id: 456)
|
|
758
783
|
result.status #=> "skipped"
|
759
784
|
|
760
785
|
# Without a reason
|
761
|
-
result.reason #=> "
|
786
|
+
result.reason #=> "No reason given"
|
762
787
|
|
763
788
|
# With a reason
|
764
789
|
result.reason #=> "Warehouse closed"
|
@@ -793,7 +818,7 @@ result = ProcessRefund.execute(refund_id: 789)
|
|
793
818
|
result.status #=> "failed"
|
794
819
|
|
795
820
|
# Without a reason
|
796
|
-
result.reason #=> "
|
821
|
+
result.reason #=> "No reason given"
|
797
822
|
|
798
823
|
# With a reason
|
799
824
|
result.reason #=> "Refund period has expired"
|
@@ -913,8 +938,8 @@ skip!("Paused")
|
|
913
938
|
fail!("Unsupported")
|
914
939
|
|
915
940
|
# Bad: Default, cannot determine reason
|
916
|
-
skip! #=> "
|
917
|
-
fail! #=> "
|
941
|
+
skip! #=> "No reason given"
|
942
|
+
fail! #=> "No reason given"
|
918
943
|
```
|
919
944
|
|
920
945
|
---
|
@@ -1729,8 +1754,9 @@ result = ConfigureServer.execute(server_id: "srv-001")
|
|
1729
1754
|
|
1730
1755
|
result.state #=> "interrupted"
|
1731
1756
|
result.status #=> "failed"
|
1732
|
-
result.reason #=> "
|
1757
|
+
result.reason #=> "Invalid inputs"
|
1733
1758
|
result.metadata #=> {
|
1759
|
+
# full_message: "environment is required. network_config is required.",
|
1734
1760
|
# messages: {
|
1735
1761
|
# environment: ["is required"],
|
1736
1762
|
# network_config: ["is required"]
|
@@ -1746,8 +1772,9 @@ result = ConfigureServer.execute(
|
|
1746
1772
|
|
1747
1773
|
result.state #=> "interrupted"
|
1748
1774
|
result.status #=> "failed"
|
1749
|
-
result.reason #=> "
|
1775
|
+
result.reason #=> "Invalid inputs"
|
1750
1776
|
result.metadata #=> {
|
1777
|
+
# full_message: "port is required.",
|
1751
1778
|
# messages: {
|
1752
1779
|
# port: ["is required"]
|
1753
1780
|
# }
|
@@ -1973,8 +2000,9 @@ result = AnalyzePerformance.execute(
|
|
1973
2000
|
|
1974
2001
|
result.state #=> "interrupted"
|
1975
2002
|
result.status #=> "failed"
|
1976
|
-
result.reason #=> "
|
2003
|
+
result.reason #=> "Invalid inputs"
|
1977
2004
|
result.metadata #=> {
|
2005
|
+
# full_message: "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal.",
|
1978
2006
|
# messages: {
|
1979
2007
|
# iterations: ["could not coerce into an integer"],
|
1980
2008
|
# score: ["could not coerce into one of: float, big_decimal"]
|
@@ -2266,8 +2294,9 @@ result = CreateProject.execute(
|
|
2266
2294
|
|
2267
2295
|
result.state #=> "interrupted"
|
2268
2296
|
result.status #=> "failed"
|
2269
|
-
result.reason #=> "
|
2297
|
+
result.reason #=> "Invalid inputs"
|
2270
2298
|
result.metadata #=> {
|
2299
|
+
# full_message: "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid.",
|
2271
2300
|
# messages: {
|
2272
2301
|
# project_name: ["is too short (minimum is 3 characters)"],
|
2273
2302
|
# budget: ["must be greater than 1000"],
|
@@ -2743,21 +2772,19 @@ Sample output:
|
|
2743
2772
|
```log
|
2744
2773
|
<!-- Success (INFO level) -->
|
2745
2774
|
I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
|
2746
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task"
|
2747
|
-
class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
2775
|
+
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
2748
2776
|
|
2749
2777
|
<!-- Skipped (WARN level) -->
|
2750
2778
|
W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
|
2751
|
-
index=1 state="interrupted" status="skipped" reason="Customer already validated"
|
2779
|
+
index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
|
2752
2780
|
|
2753
2781
|
<!-- Failed (ERROR level) -->
|
2754
2782
|
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
|
2755
|
-
index=2 state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
2783
|
+
index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
2756
2784
|
|
2757
2785
|
<!-- Failed Chain -->
|
2758
2786
|
E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
|
2759
|
-
caused_failure={index: 2, class: "CalculateTax", status: "failed"}
|
2760
|
-
threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
2787
|
+
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"}
|
2761
2788
|
```
|
2762
2789
|
|
2763
2790
|
> [!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
|
|
@@ -149,8 +149,9 @@ result = AnalyzePerformance.execute(
|
|
149
149
|
|
150
150
|
result.state #=> "interrupted"
|
151
151
|
result.status #=> "failed"
|
152
|
-
result.reason #=> "
|
152
|
+
result.reason #=> "Invalid inputs"
|
153
153
|
result.metadata #=> {
|
154
|
+
# full_message: "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal.",
|
154
155
|
# messages: {
|
155
156
|
# iterations: ["could not coerce into an integer"],
|
156
157
|
# score: ["could not coerce into one of: float, big_decimal"]
|
@@ -250,8 +250,9 @@ result = ConfigureServer.execute(server_id: "srv-001")
|
|
250
250
|
|
251
251
|
result.state #=> "interrupted"
|
252
252
|
result.status #=> "failed"
|
253
|
-
result.reason #=> "
|
253
|
+
result.reason #=> "Invalid inputs"
|
254
254
|
result.metadata #=> {
|
255
|
+
# full_message: "environment is required. network_config is required.",
|
255
256
|
# messages: {
|
256
257
|
# environment: ["is required"],
|
257
258
|
# network_config: ["is required"]
|
@@ -267,8 +268,9 @@ result = ConfigureServer.execute(
|
|
267
268
|
|
268
269
|
result.state #=> "interrupted"
|
269
270
|
result.status #=> "failed"
|
270
|
-
result.reason #=> "
|
271
|
+
result.reason #=> "Invalid inputs"
|
271
272
|
result.metadata #=> {
|
273
|
+
# full_message: "port is required.",
|
272
274
|
# messages: {
|
273
275
|
# port: ["is required"]
|
274
276
|
# }
|
@@ -294,8 +294,9 @@ result = CreateProject.execute(
|
|
294
294
|
|
295
295
|
result.state #=> "interrupted"
|
296
296
|
result.status #=> "failed"
|
297
|
-
result.reason #=> "
|
297
|
+
result.reason #=> "Invalid inputs"
|
298
298
|
result.metadata #=> {
|
299
|
+
# full_message: "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid.",
|
299
300
|
# messages: {
|
300
301
|
# project_name: ["is too short (minimum is 3 characters)"],
|
301
302
|
# budget: ["must be greater than 1000"],
|
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/interruptions/halt.md
CHANGED
@@ -45,7 +45,7 @@ result = ProcessInventory.execute(inventory_id: 456)
|
|
45
45
|
result.status #=> "skipped"
|
46
46
|
|
47
47
|
# Without a reason
|
48
|
-
result.reason #=> "
|
48
|
+
result.reason #=> "No reason given"
|
49
49
|
|
50
50
|
# With a reason
|
51
51
|
result.reason #=> "Warehouse closed"
|
@@ -80,7 +80,7 @@ result = ProcessRefund.execute(refund_id: 789)
|
|
80
80
|
result.status #=> "failed"
|
81
81
|
|
82
82
|
# Without a reason
|
83
|
-
result.reason #=> "
|
83
|
+
result.reason #=> "No reason given"
|
84
84
|
|
85
85
|
# With a reason
|
86
86
|
result.reason #=> "Refund period has expired"
|
@@ -200,8 +200,8 @@ skip!("Paused")
|
|
200
200
|
fail!("Unsupported")
|
201
201
|
|
202
202
|
# Bad: Default, cannot determine reason
|
203
|
-
skip! #=> "
|
204
|
-
fail! #=> "
|
203
|
+
skip! #=> "No reason given"
|
204
|
+
fail! #=> "No reason given"
|
205
205
|
```
|
206
206
|
|
207
207
|
---
|
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/lib/cmdx/executor.rb
CHANGED
@@ -142,7 +142,11 @@ module CMDx
|
|
142
142
|
task.class.settings[:attributes].define_and_verify(task)
|
143
143
|
return if task.errors.empty?
|
144
144
|
|
145
|
-
task.result.fail!(
|
145
|
+
task.result.fail!(
|
146
|
+
Locale.t("cmdx.faults.invalid"),
|
147
|
+
full_message: task.errors.to_s,
|
148
|
+
messages: task.errors.to_h
|
149
|
+
)
|
146
150
|
end
|
147
151
|
|
148
152
|
# Executes the main task logic.
|
@@ -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/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
|
|
data/lib/locales/af.yml
CHANGED
@@ -9,7 +9,8 @@ af:
|
|
9
9
|
into_any: "kon nie na een van: %{types} omskep word nie"
|
10
10
|
unknown: "onbekende %{type} omskep tipe"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Ongeldige insette"
|
13
|
+
unspecified: "Geen rede gegee nie"
|
13
14
|
types:
|
14
15
|
array: "skikking"
|
15
16
|
big_decimal: "groot desimale"
|
data/lib/locales/ar.yml
CHANGED
data/lib/locales/az.yml
CHANGED
@@ -9,7 +9,8 @@ az:
|
|
9
9
|
into_any: "aşağıdakılardan birinə çevrilə bilmədi: %{types}"
|
10
10
|
unknown: "naməlum %{type} çevrilmə tipi"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Etibarsız girişlər"
|
13
|
+
unspecified: "Səbəb göstərilməyib"
|
13
14
|
types:
|
14
15
|
array: "massiv"
|
15
16
|
big_decimal: "böyük onluq"
|
data/lib/locales/be.yml
CHANGED
@@ -9,7 +9,8 @@ be:
|
|
9
9
|
into_any: "не ўдалося пераўтварыць у адзін з: %{types}"
|
10
10
|
unknown: "невядомы тып пераўтварэння %{type}"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Няправільныя ўваходныя даныя"
|
13
|
+
unspecified: "Прычына не паказана"
|
13
14
|
types:
|
14
15
|
array: "масіў"
|
15
16
|
big_decimal: "вялікае дзесятковае лік"
|
data/lib/locales/bg.yml
CHANGED
@@ -9,7 +9,8 @@ bg:
|
|
9
9
|
into_any: "не може да бъде преобразуван в един от: %{types}"
|
10
10
|
unknown: "неизвестен тип преобразуване %{type}"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Невалидни входни данни"
|
13
|
+
unspecified: "Не е посочена причина"
|
13
14
|
types:
|
14
15
|
array: "масив"
|
15
16
|
big_decimal: "голямо десетично число"
|
data/lib/locales/bn.yml
CHANGED
data/lib/locales/bs.yml
CHANGED
@@ -9,7 +9,8 @@ bs:
|
|
9
9
|
into_any: "nije mogao biti pretvoren u jedan od: %{types}"
|
10
10
|
unknown: "nepoznati tip pretvorbe %{type}"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Neispravni ulazi"
|
13
|
+
unspecified: "Nije naveden razlog"
|
13
14
|
types:
|
14
15
|
array: "niz"
|
15
16
|
big_decimal: "veliki decimalni broj"
|
data/lib/locales/ca.yml
CHANGED
@@ -9,7 +9,8 @@ ca:
|
|
9
9
|
into_any: "no es va poder convertir a un de: %{types}"
|
10
10
|
unknown: "tipus de conversió %{type} desconegut"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Entrades no vàlides"
|
13
|
+
unspecified: "No s'ha donat cap raó"
|
13
14
|
types:
|
14
15
|
array: "array"
|
15
16
|
big_decimal: "decimal gran"
|
data/lib/locales/cnr.yml
CHANGED
@@ -9,7 +9,8 @@ cnr:
|
|
9
9
|
into_any: "nije mogao konvertovati u jedan od: %{types}"
|
10
10
|
unknown: "nepoznati %{type} tip konverzije"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Neispravni ulazi"
|
13
|
+
unspecified: "Nije dat razlog"
|
13
14
|
types:
|
14
15
|
array: "niz"
|
15
16
|
big_decimal: "veliki decimalni"
|
data/lib/locales/cs.yml
CHANGED
@@ -9,7 +9,8 @@ cs:
|
|
9
9
|
into_any: "nelze převést na jeden z: %{types}"
|
10
10
|
unknown: "neznámý typ převodu %{type}"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Neplatné vstupy"
|
13
|
+
unspecified: "Není uveden důvod"
|
13
14
|
types:
|
14
15
|
array: "pole"
|
15
16
|
big_decimal: "velké desetinné číslo"
|
data/lib/locales/cy.yml
CHANGED
@@ -9,7 +9,8 @@ cy:
|
|
9
9
|
into_any: "ni allwyd ei drawsnewid i un o: %{types}"
|
10
10
|
unknown: "math anhysbys o drawsnewid %{type}"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Mewnbynnau annilys"
|
13
|
+
unspecified: "Dim rheswm wedi'i roi"
|
13
14
|
types:
|
14
15
|
array: "arae"
|
15
16
|
big_decimal: "degol mawr"
|
data/lib/locales/da.yml
CHANGED
@@ -9,7 +9,8 @@ da:
|
|
9
9
|
into_any: "kunne ikke konverteres til en af: %{types}"
|
10
10
|
unknown: "ukendt %{type} konverteringstype"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Ugyldige input"
|
13
|
+
unspecified: "Ingen grund givet"
|
13
14
|
types:
|
14
15
|
array: "array"
|
15
16
|
big_decimal: "stort decimaltal"
|
data/lib/locales/de.yml
CHANGED
@@ -9,7 +9,8 @@ de:
|
|
9
9
|
into_any: "konnte nicht in einen von: %{types} umgewandelt werden"
|
10
10
|
unknown: "unbekannter %{type} Umwandlungstyp"
|
11
11
|
faults:
|
12
|
-
|
12
|
+
invalid: "Ungültige Eingaben"
|
13
|
+
unspecified: "Kein Grund angegeben"
|
13
14
|
types:
|
14
15
|
array: "Array"
|
15
16
|
big_decimal: "große Dezimalzahl"
|
data/lib/locales/dz.yml
CHANGED