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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/LLM.md +41 -14
  4. data/README.md +57 -20
  5. data/docs/attributes/coercions.md +2 -1
  6. data/docs/attributes/definitions.md +4 -2
  7. data/docs/attributes/validations.md +2 -1
  8. data/docs/getting_started.md +26 -0
  9. data/docs/interruptions/halt.md +4 -4
  10. data/docs/logging.md +4 -6
  11. data/lib/cmdx/executor.rb +5 -1
  12. data/lib/cmdx/middlewares/correlate.rb +3 -4
  13. data/lib/cmdx/version.rb +1 -1
  14. data/lib/generators/cmdx/locale_generator.rb +1 -1
  15. data/lib/locales/af.yml +2 -1
  16. data/lib/locales/ar.yml +1 -0
  17. data/lib/locales/az.yml +2 -1
  18. data/lib/locales/be.yml +2 -1
  19. data/lib/locales/bg.yml +2 -1
  20. data/lib/locales/bn.yml +1 -0
  21. data/lib/locales/bs.yml +2 -1
  22. data/lib/locales/ca.yml +2 -1
  23. data/lib/locales/cnr.yml +2 -1
  24. data/lib/locales/cs.yml +2 -1
  25. data/lib/locales/cy.yml +2 -1
  26. data/lib/locales/da.yml +2 -1
  27. data/lib/locales/de.yml +2 -1
  28. data/lib/locales/dz.yml +1 -0
  29. data/lib/locales/el.yml +2 -1
  30. data/lib/locales/en.yml +2 -1
  31. data/lib/locales/eo.yml +2 -1
  32. data/lib/locales/es.yml +2 -1
  33. data/lib/locales/et.yml +2 -1
  34. data/lib/locales/eu.yml +2 -1
  35. data/lib/locales/fa.yml +1 -0
  36. data/lib/locales/fi.yml +2 -1
  37. data/lib/locales/fr.yml +2 -1
  38. data/lib/locales/fy.yml +2 -1
  39. data/lib/locales/gd.yml +2 -1
  40. data/lib/locales/gl.yml +2 -1
  41. data/lib/locales/he.yml +1 -0
  42. data/lib/locales/hi.yml +1 -0
  43. data/lib/locales/hr.yml +2 -1
  44. data/lib/locales/hu.yml +2 -1
  45. data/lib/locales/hy.yml +2 -1
  46. data/lib/locales/id.yml +2 -1
  47. data/lib/locales/is.yml +2 -1
  48. data/lib/locales/it.yml +2 -1
  49. data/lib/locales/ja.yml +1 -0
  50. data/lib/locales/ka.yml +1 -0
  51. data/lib/locales/kk.yml +2 -1
  52. data/lib/locales/km.yml +1 -0
  53. data/lib/locales/kn.yml +1 -0
  54. data/lib/locales/ko.yml +1 -0
  55. data/lib/locales/lb.yml +2 -1
  56. data/lib/locales/lo.yml +1 -0
  57. data/lib/locales/lt.yml +2 -1
  58. data/lib/locales/lv.yml +2 -1
  59. data/lib/locales/mg.yml +2 -1
  60. data/lib/locales/mk.yml +2 -1
  61. data/lib/locales/ml.yml +1 -0
  62. data/lib/locales/mn.yml +2 -1
  63. data/lib/locales/mr-IN.yml +1 -0
  64. data/lib/locales/ms.yml +2 -1
  65. data/lib/locales/nb.yml +2 -1
  66. data/lib/locales/ne.yml +1 -0
  67. data/lib/locales/nl.yml +2 -1
  68. data/lib/locales/nn.yml +2 -1
  69. data/lib/locales/oc.yml +2 -1
  70. data/lib/locales/or.yml +1 -0
  71. data/lib/locales/pa.yml +1 -0
  72. data/lib/locales/pl.yml +2 -1
  73. data/lib/locales/pt.yml +2 -1
  74. data/lib/locales/rm.yml +2 -1
  75. data/lib/locales/ro.yml +2 -1
  76. data/lib/locales/ru.yml +2 -1
  77. data/lib/locales/sc.yml +2 -1
  78. data/lib/locales/sk.yml +2 -1
  79. data/lib/locales/sl.yml +2 -1
  80. data/lib/locales/sq.yml +2 -1
  81. data/lib/locales/sr.yml +2 -1
  82. data/lib/locales/st.yml +2 -1
  83. data/lib/locales/sv.yml +2 -1
  84. data/lib/locales/sw.yml +2 -1
  85. data/lib/locales/ta.yml +1 -0
  86. data/lib/locales/te.yml +1 -0
  87. data/lib/locales/th.yml +1 -0
  88. data/lib/locales/tl.yml +2 -1
  89. data/lib/locales/tr.yml +2 -1
  90. data/lib/locales/tt.yml +2 -1
  91. data/lib/locales/ug.yml +1 -0
  92. data/lib/locales/uk.yml +2 -1
  93. data/lib/locales/ur.yml +1 -0
  94. data/lib/locales/uz.yml +2 -1
  95. data/lib/locales/vi.yml +2 -1
  96. data/lib/locales/wo.yml +2 -1
  97. data/lib/locales/zh-CN.yml +1 -0
  98. data/lib/locales/zh-HK.yml +1 -0
  99. data/lib/locales/zh-TW.yml +1 -0
  100. data/lib/locales/zh-YUE.yml +1 -0
  101. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bbf1d8db7665b82da9af2dd5e017977945bbd550700177368b1363a8e63275e
4
- data.tar.gz: 0f1720432a999ff31399d67c602e5277ee8cb1a8062117165c425a39233eb81c
3
+ metadata.gz: 01fd49f8d31e90b2818d78cbf452c68888f76e8f4983647b1c163b235963b6a8
4
+ data.tar.gz: ab15559fbfedd9f359f23fbce24b7259ed428ef37caeb5522777de0724f395a8
5
5
  SHA512:
6
- metadata.gz: 144c01abf663258e34b40eaa9e1deba09b59347b97773237f9bd84f5d82474825aca38f21b6ad0c052f1b058f6a5550e24ef8f3598da4cdecad94da1689bea5c
7
- data.tar.gz: 25220966f85607b73740107b5af5da7889e75592055ad17156bc48f157550e2d030f542b7f279ec731cbb78ef732be1c6507b8e448a97a8324749d4746549d95
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 #=> "no reason given"
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 #=> "no reason given"
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! #=> "no reason given"
917
- fail! #=> "no reason given"
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 #=> "environment is required. network_config is required."
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 #=> "port is required."
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 #=> "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal."
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 #=> "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."
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
- CMDx is a framework for building maintainable business processes. It simplifies building task objects by offering integrated:
13
+ Stop wrestling with messy service objects. CMDx gives you a clean, consistent way to design business processes:
14
14
 
15
- - Flow controls
16
- - Composable workflows
17
- - Comprehensive logging
18
- - Attribute definition
19
- - Validations and coercions
20
- - And much more...
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 3 step process can open up a world of possibilities:
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.analyze(dataset, analysis_type)
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
- # 2. Execute task
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
- # 3. Handle result
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-i18n](https://github.com/drexed/cmdx-i18n) - 85+ translations
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
- The following gems are currently under development:
164
+ For backwards compatibility of certain functionality:
129
165
 
130
- - `cmdx-testing` - RSpec and Minitest matchers
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 #=> "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal."
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 #=> "environment is required. network_config is required."
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 #=> "port is required."
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 #=> "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."
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"],
@@ -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:
@@ -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 #=> "no reason given"
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 #=> "no reason given"
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! #=> "no reason given"
204
- fail! #=> "no reason given"
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!(task.errors.to_s, messages: task.errors.to_h)
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
- result = use(correlation_id, &)
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CMDx
4
4
 
5
- VERSION = "1.7.1"
5
+ VERSION = "1.7.3"
6
6
 
7
7
  end
@@ -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 alpha-2 code"
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
- unspecified: "geen rede gegee nie"
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
@@ -9,6 +9,7 @@ ar:
9
9
  into_any: "لا يمكن تحويله إلى واحد من: %{types}"
10
10
  unknown: "نوع تحويل %{type} غير معروف"
11
11
  faults:
12
+ invalid: "مدخلات غير صالحة"
12
13
  unspecified: "لم يتم تقديم سبب"
13
14
  types:
14
15
  array: "مصفوفة"
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
- unspecified: "səbəb göstərilməyib"
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
- unspecified: "прычына не паказана"
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
- unspecified: "не е посочена причина"
12
+ invalid: "Невалидни входни данни"
13
+ unspecified: "Не е посочена причина"
13
14
  types:
14
15
  array: "масив"
15
16
  big_decimal: "голямо десетично число"
data/lib/locales/bn.yml CHANGED
@@ -9,6 +9,7 @@ bn:
9
9
  into_any: "নিম্নলিখিতগুলির মধ্যে একটিতে রূপান্তর করা যায়নি: %{types}"
10
10
  unknown: "অজানা %{type} রূপান্তর প্রকার"
11
11
  faults:
12
+ invalid: "অবৈধ ইনপুট"
12
13
  unspecified: "কোন কারণ দেওয়া হয়নি"
13
14
  types:
14
15
  array: "অ্যারে"
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
- unspecified: "nije naveden razlog"
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
- unspecified: "no s'ha donat cap raó"
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
- unspecified: "nije dat razlog"
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
- unspecified: "není uveden důvod"
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
- unspecified: "dim rheswm wedi'i roi"
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
- unspecified: "ingen grund givet"
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
- unspecified: "kein Grund angegeben"
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
@@ -9,6 +9,7 @@ dz:
9
9
  into_any: "འདི་ཚོའི་ནང་ནས་གཅིག་ལ་བསྒྱུར་མ་ཐུབ། %{types}"
10
10
  unknown: "མ་ཤེས་པའི་ %{type} བསྒྱུར་པའི་རིགས།"
11
11
  faults:
12
+ invalid: "ཆ་རྐྱེན་མི་ཆོག་པའི་འཇུག་ཆོག"
12
13
  unspecified: "རྒྱུ་མཚན་མ་བཟུང་།"
13
14
  types:
14
15
  array: "ཐིག་ཕྲེང་།"