cmdx 1.10.0 β†’ 1.10.1

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.
@@ -2,10 +2,10 @@
2
2
 
3
3
  CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. It brings structure, consistency, and powerful developer tools to your business processes.
4
4
 
5
- **Common challenges it solves:**
5
+ **Common challenges:**
6
6
 
7
7
  - Inconsistent service object patterns across your codebase
8
- - Limited logging makes debugging a nightmare
8
+ - Black boxes make debugging a nightmare
9
9
  - Fragile error handling erodes confidence
10
10
 
11
11
  **What you get:**
@@ -17,343 +17,80 @@ CMDx is a Ruby framework for building maintainable, observable business logic th
17
17
  - Attribute validation with type coercions
18
18
  - Sensible defaults and developer-friendly APIs
19
19
 
20
- ## The CERO Pattern
21
-
22
- CMDx embraces the Compose, Execute, React, Observe (CERO) patternβ€”a simple yet powerful approach to building reliable business logic.
23
-
24
- 🧩 **Compose** β€” Define small, focused tasks with typed attributes and validations
25
-
26
- ⚑ **Execute** β€” Run tasks with clear outcomes and pluggable behaviors
27
-
28
- πŸ”„ **React** β€” Adapt to outcomes by chaining follow-up tasks or handling faults
29
-
30
- πŸ” **Observe** β€” Capture structured logs and execution chains for debugging
31
-
32
20
  ## Installation
33
21
 
34
22
  Add CMDx to your Gemfile:
35
23
 
36
- ```ruby
37
- gem 'cmdx'
38
- ```
24
+ ```sh
25
+ gem install cmdx
39
26
 
40
- For Rails applications, generate the configuration:
27
+ # - or -
41
28
 
42
- ```bash
43
- rails generate cmdx:install
29
+ bundle add cmdx
44
30
  ```
45
31
 
46
- This creates `config/initializers/cmdx.rb` file.
47
-
48
- ## Configuration Hierarchy
49
-
50
- CMDx uses a straightforward two-tier configuration system:
51
-
52
- 1. **Global Configuration** β€” Framework-wide defaults
53
- 2. **Task Settings** β€” Class-level overrides using `settings`
54
-
55
- !!! warning "Important"
56
-
57
- Task settings take precedence over global config. Settings are inherited from parent classes and can be overridden in subclasses.
58
-
59
- ## Global Configuration
60
-
61
- Configure framework-wide defaults that apply to all tasks. These settings come with sensible defaults out of the box.
62
-
63
- ### Breakpoints
64
-
65
- Control when `execute!` raises a `CMDx::Fault` based on task status.
66
-
67
- ```ruby
68
- CMDx.configure do |config|
69
- config.task_breakpoints = "failed" # String or Array[String]
70
- end
71
- ```
72
-
73
- For workflows, configure which statuses halt the execution pipeline:
74
-
75
- ```ruby
76
- CMDx.configure do |config|
77
- config.workflow_breakpoints = ["skipped", "failed"]
78
- end
79
- ```
32
+ ## Configuration
80
33
 
81
- ### Rollback
34
+ For Rails applications, run the following command to generate a global configuration file in `config/initializers/cmdx.rb`.
82
35
 
83
- Control when a `rollback` of task execution is called.
84
-
85
- ```ruby
86
- CMDx.configure do |config|
87
- config.rollback_on = ["failed"] # String or Array[String]
88
- end
89
- ```
90
-
91
- ### Backtraces
92
-
93
- Enable detailed backtraces for non-fault exceptions to improve debugging. Optionally clean up stack traces to remove framework noise.
94
-
95
- !!! note
96
-
97
- In Rails environments, `backtrace_cleaner` defaults to `Rails.backtrace_cleaner.clean`.
98
-
99
- ```ruby
100
- CMDx.configure do |config|
101
- # Truthy
102
- config.backtrace = true
103
-
104
- # Via callable (must respond to `call(backtrace)`)
105
- config.backtrace_cleaner = AdvanceCleaner.new
106
-
107
- # Via proc or lambda
108
- config.backtrace_cleaner = ->(backtrace) { backtrace[0..5] }
109
- end
110
- ```
111
-
112
- ### Exception Handlers
113
-
114
- Register handlers that run when non-fault exceptions occur.
115
-
116
- !!! tip
117
-
118
- Use exception handlers to send errors to your APM of choice.
119
-
120
- ```ruby
121
- CMDx.configure do |config|
122
- # Via callable (must respond to `call(task, exception)`)
123
- config.exception_handler = NewRelicReporter
124
-
125
- # Via proc or lambda
126
- config.exception_handler = proc do |task, exception|
127
- APMService.report(exception, extra_data: { task: task.name, id: task.id })
128
- end
129
- end
130
- ```
131
-
132
- ### Logging
133
-
134
- ```ruby
135
- CMDx.configure do |config|
136
- config.logger = CustomLogger.new($stdout)
137
- end
138
- ```
139
-
140
- ### Middlewares
141
-
142
- See the [Middlewares](middlewares.md#declarations) docs for task level configurations.
143
-
144
- ```ruby
145
- CMDx.configure do |config|
146
- # Via callable (must respond to `call(task, options)`)
147
- config.middlewares.register CMDx::Middlewares::Timeout
148
-
149
- # Via proc or lambda
150
- config.middlewares.register proc { |task, options|
151
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
152
- result = yield
153
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
154
- Rails.logger.debug { "task completed in #{((end_time - start_time) * 1000).round(2)}ms" }
155
- result
156
- }
157
-
158
- # With options
159
- config.middlewares.register AuditTrailMiddleware, service_name: "document_processor"
160
-
161
- # Remove middleware
162
- config.middlewares.deregister CMDx::Middlewares::Timeout
163
- end
164
- ```
165
-
166
- !!! note
167
-
168
- Middlewares are executed in registration order. Each middleware wraps the next, creating an execution chain around task logic.
169
-
170
- ### Callbacks
171
-
172
- See the [Callbacks](callbacks.md#declarations) docs for task level configurations.
173
-
174
- ```ruby
175
- CMDx.configure do |config|
176
- # Via method
177
- config.callbacks.register :before_execution, :initialize_user_session
178
-
179
- # Via callable (must respond to `call(task)`)
180
- config.callbacks.register :on_success, LogUserActivity
181
-
182
- # Via proc or lambda
183
- config.callbacks.register :on_complete, proc { |task|
184
- execution_time = task.metadata[:runtime]
185
- Metrics.timer("task.execution_time", execution_time, tags: ["task:#{task.class.name.underscore}"])
186
- }
187
-
188
- # With options
189
- config.callbacks.register :on_failure, :send_alert_notification, if: :critical_task?
190
-
191
- # Remove callback
192
- config.callbacks.deregister :on_success, LogUserActivity
193
- end
194
- ```
195
-
196
- ### Coercions
197
-
198
- See the [Attributes - Coercions](attributes/coercions.md#declarations) docs for task level configurations.
199
-
200
- ```ruby
201
- CMDx.configure do |config|
202
- # Via callable (must respond to `call(value, options)`)
203
- config.coercions.register :currency, CurrencyCoercion
204
-
205
- # Via method (must match signature `def coordinates_coercion(value, options)`)
206
- config.coercions.register :coordinates, :coordinates_coercion
207
-
208
- # Via proc or lambda
209
- config.coercions.register :tag_list, proc { |value, options|
210
- delimiter = options[:delimiter] || ','
211
- max_tags = options[:max_tags] || 50
212
-
213
- tags = value.to_s.split(delimiter).map(&:strip).reject(&:empty?)
214
- tags.first(max_tags)
215
- }
216
-
217
- # Remove coercion
218
- config.coercions.deregister :currency
219
- end
36
+ ```bash
37
+ rails generate cmdx:install
220
38
  ```
221
39
 
222
- ### Validators
223
-
224
- See the [Attributes - Validations](attributes/validations.md#declarations) docs for task level configurations.
225
-
226
- ```ruby
227
- CMDx.configure do |config|
228
- # Via callable (must respond to `call(value, options)`)
229
- config.validators.register :username, UsernameValidator
230
-
231
- # Via method (must match signature `def url_validator(value, options)`)
232
- config.validators.register :url, :url_validator
40
+ If not using Rails, manually copy the [configuration file](https://github.com/drexed/cmdx/blob/main/lib/generators/cmdx/templates/install.rb).
233
41
 
234
- # Via proc or lambda
235
- config.validators.register :access_token, proc { |value, options|
236
- expected_prefix = options[:prefix] || "tok_"
237
- minimum_length = options[:min_length] || 40
238
-
239
- value.start_with?(expected_prefix) && value.length >= minimum_length
240
- }
241
-
242
- # Remove validator
243
- config.validators.deregister :username
244
- end
245
- ```
42
+ ## The CERO Pattern
246
43
 
247
- ## Task Configuration
44
+ CMDx embraces the Compose, Execute, React, Observe (CERO, pronounced "zero") patternβ€”a simple yet powerful approach to building reliable business logic.
248
45
 
249
- ### Settings
46
+ ### Compose
250
47
 
251
- Override global configuration for specific tasks using `settings`:
48
+ Build reusable, single-responsibility tasks with typed attributes, validation, and callbacks. Tasks can be chained together in workflows to create complex business processes from simple building blocks.
252
49
 
253
50
  ```ruby
254
- class GenerateInvoice < CMDx::Task
255
- settings(
256
- # Global configuration overrides
257
- task_breakpoints: ["failed"], # Breakpoint override
258
- workflow_breakpoints: [], # Breakpoint override
259
- backtrace: true, # Toggle backtrace
260
- backtrace_cleaner: ->(bt) { bt[0..5] }, # Backtrace cleaner
261
- logger: CustomLogger.new($stdout), # Custom logger
262
-
263
- # Task configuration settings
264
- breakpoints: ["failed"], # Contextual pointer for :task_breakpoints and :workflow_breakpoints
265
- log_level: :info, # Log level override
266
- log_formatter: CMDx::LogFormatters::Json.new # Log formatter override
267
- tags: ["billing", "financial"], # Logging tags
268
- deprecated: true, # Task deprecations
269
- retries: 3, # Non-fault exception retries
270
- retry_on: [External::ApiError], # List of exceptions to retry on
271
- retry_jitter: 1, # Space between retry iteration, eg: current retry num + 1
272
- rollback_on: ["failed", "skipped"], # Rollback on override
273
- )
274
-
51
+ class AnalyzeMetrics < CMDx::Task
275
52
  def work
276
53
  # Your logic here...
277
54
  end
278
55
  end
279
56
  ```
280
57
 
281
- !!! warning "Important"
282
-
283
- Retries reuse the same context. By default, all `StandardError` exceptions (including faults) are retried unless you specify `retry_on` option for specific matches.
58
+ ### Execute
284
59
 
285
- ### Registrations
286
-
287
- Register or deregister middlewares, callbacks, coercions, and validators for specific tasks:
60
+ Invoke tasks with a consistent API that always returns a result object. Execution automatically handles validation, type coercion, error handling, and logging. Arguments are validated and coerced before your task logic runs.
288
61
 
289
62
  ```ruby
290
- class SendCampaignEmail < CMDx::Task
291
- # Middlewares
292
- register :middleware, CMDx::Middlewares::Timeout
293
- deregister :middleware, AuditTrailMiddleware
294
-
295
- # Callbacks
296
- register :callback, :on_complete, proc { |task|
297
- runtime = task.metadata[:runtime]
298
- Analytics.track("email_campaign.sent", runtime, tags: ["task:#{task.class.name}"])
299
- }
300
- deregister :callback, :before_execution, :initialize_user_session
301
-
302
- # Coercions
303
- register :coercion, :currency, CurrencyCoercion
304
- deregister :coercion, :coordinates
305
-
306
- # Validators
307
- register :validator, :username, :username_validator
308
- deregister :validator, :url
63
+ # Without args
64
+ result = AnalyzeMetrics.execute
309
65
 
310
- def work
311
- # Your logic here...
312
- end
313
- end
66
+ # With args
67
+ result = AnalyzeMetrics.execute(model: "blackbox", "sensitivity" => 3)
314
68
  ```
315
69
 
316
- ## Configuration Management
70
+ ### React
317
71
 
318
- ### Access
72
+ Every execution returns a result object with a clear outcome. Check the result's state (`success?`, `failed?`, `skipped?`) and access returned values, error messages, and metadata to make informed decisions.
319
73
 
320
74
  ```ruby
321
- # Global configuration access
322
- CMDx.configuration.logger #=> <Logger instance>
323
- CMDx.configuration.task_breakpoints #=> ["failed"]
324
- CMDx.configuration.middlewares.registry #=> [<Middleware>, ...]
325
-
326
- # Task configuration access
327
- class ProcessUpload < CMDx::Task
328
- settings(tags: ["files", "storage"])
329
-
330
- def work
331
- self.class.settings[:logger] #=> Global configuration value
332
- self.class.settings[:tags] #=> Task configuration value => ["files", "storage"]
333
- end
75
+ if result.success?
76
+ # Handle success
77
+ elsif result.skipped?
78
+ # Handle skipped
79
+ elsif result.failed?
80
+ # Handle failed
334
81
  end
335
82
  ```
336
83
 
337
- ### Resetting
84
+ ### Observe
338
85
 
339
- !!! warning
86
+ Every task execution generates structured logs with execution chains, runtime metrics, and contextual metadata. Logs can be automatically correlated using chain IDs, making it easy to trace complex workflows and debug issues.
340
87
 
341
- Resetting affects your entire application. Use this primarily in test environments.
342
-
343
- ```ruby
344
- # Reset to framework defaults
345
- CMDx.reset_configuration!
88
+ ```log
89
+ I, [2022-07-17T18:42:37.000000 #3784] INFO -- CMDx:
90
+ index=1 chain_id="018c2b95-23j4-2kj3-32kj-3n4jk3n4jknf" type="Task" class="SendAnalyzedEmail" state="complete" status="success" metadata={runtime: 347}
346
91
 
347
- # Verify reset
348
- CMDx.configuration.task_breakpoints #=> ["failed"] (default)
349
- CMDx.configuration.middlewares.registry #=> Empty registry
350
-
351
- # Commonly used in test setup (RSpec example)
352
- RSpec.configure do |config|
353
- config.before(:each) do
354
- CMDx.reset_configuration!
355
- end
356
- end
92
+ I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
93
+ index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
357
94
  ```
358
95
 
359
96
  ## Task Generator
data/docs/index.md CHANGED
@@ -8,7 +8,7 @@ Build business logic that's powerful, predictable, and maintainable.
8
8
 
9
9
  ---
10
10
 
11
- Say goodbye to messy service objects. CMDx helps you design business logic with clarity and consistencyβ€”build faster, debug easier, and ship with confidence.
11
+ Say goodbye to messy service objects. CMDx (pronounced "Command X") helps you design business logic with clarity and consistencyβ€”build faster, debug easier, and ship with confidence.
12
12
 
13
13
  !!! note
14
14
 
@@ -24,7 +24,9 @@ CMDx works with any Ruby framework. Rails support is built-in, but it's framewor
24
24
 
25
25
  ```sh
26
26
  gem install cmdx
27
+
27
28
  # - or -
29
+
28
30
  bundle add cmdx
29
31
  ```
30
32
 
@@ -125,7 +127,7 @@ For backwards compatibility of certain functionality:
125
127
 
126
128
  ## Contributing
127
129
 
128
- Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](CODE_OF_CONDUCT.md).
130
+ Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](https://github.com/drexed/cmdx/blob/main/CODE_OF_CONDUCT.md).
129
131
 
130
132
  ## License
131
133
 
data/docs/retries.md CHANGED
@@ -17,7 +17,7 @@ class FetchExternalData < CMDx::Task
17
17
  end
18
18
  ```
19
19
 
20
- When an exception occurs during execution, CMDx automatically retries up to the configured limit.
20
+ When an exception occurs during execution, CMDx automatically retries up to the configured limit. Each retry attempt is logged at the `warn` level with retry metadata. If all retries are exhausted, the task fails with the original exception.
21
21
 
22
22
  ## Selective Retries
23
23
 
@@ -9,34 +9,34 @@
9
9
  --md-accent-fg-color--transparent: hsla(#{hex2hsl(#fe1817)}, 0.1);
10
10
  }
11
11
 
12
- /* Atom One Light Pro syntax highlighting */
12
+ /* GitHub High Contrast Light syntax highlighting */
13
13
  [data-md-color-scheme="default"] {
14
- --md-code-hl-color: #2c3036;
15
- --md-code-hl-keyword-color: #a626a4;
16
- --md-code-hl-string-color: #50a14f;
17
- --md-code-hl-name-color: #e4564a;
18
- --md-code-hl-function-color: #4078f2;
19
- --md-code-hl-number-color: #ca7601;
20
- --md-code-hl-constant-color: #c18401;
21
- --md-code-hl-comment-color: #9ca0a4;
22
- --md-code-hl-operator-color: #0184bc;
23
- --md-code-hl-punctuation-color:#383a42;
24
- --md-code-hl-variable-color: #e4564a;
25
- --md-code-hl-generic-color: #e4564a;
14
+ --md-code-hl-color: #0e1116;
15
+ --md-code-hl-keyword-color: #a0095d;
16
+ --md-code-hl-string-color: #024c1a;
17
+ --md-code-hl-name-color: #622cbc;
18
+ --md-code-hl-function-color: #622cbc;
19
+ --md-code-hl-number-color: #0349b4;
20
+ --md-code-hl-constant-color: #702c00;
21
+ --md-code-hl-comment-color: #66707b;
22
+ --md-code-hl-operator-color: #a0095d;
23
+ --md-code-hl-punctuation-color:#0e1116;
24
+ --md-code-hl-variable-color: #702c00;
25
+ --md-code-hl-generic-color: #622cbc;
26
26
  }
27
27
 
28
- /* Atom One Dark Pro syntax highlighting */
28
+ /* GitHub High Contrast Dark syntax highlighting */
29
29
  [data-md-color-scheme="slate"] {
30
- --md-code-hl-color: #e5e5e6;
31
- --md-code-hl-keyword-color: #c678dd;
32
- --md-code-hl-string-color: #98c379;
33
- --md-code-hl-name-color: #e06c75;
34
- --md-code-hl-function-color: #61afef;
35
- --md-code-hl-number-color: #d19a66;
36
- --md-code-hl-constant-color: #d19a66;
37
- --md-code-hl-comment-color: #7f848e;
38
- --md-code-hl-operator-color: #56b6c2;
39
- --md-code-hl-punctuation-color:#abb2bf;
40
- --md-code-hl-variable-color: #e06c75;
41
- --md-code-hl-generic-color: #e06c75;
30
+ --md-code-hl-color: #f0f3f6;
31
+ --md-code-hl-keyword-color: #ff9492;
32
+ --md-code-hl-string-color: #addcff;
33
+ --md-code-hl-name-color: #dbb7ff;
34
+ --md-code-hl-function-color: #dbb7ff;
35
+ --md-code-hl-number-color: #91cbff;
36
+ --md-code-hl-constant-color: #ffb757;
37
+ --md-code-hl-comment-color: #9ea7b3;
38
+ --md-code-hl-operator-color: #ff9492;
39
+ --md-code-hl-punctuation-color:#f0f3f6;
40
+ --md-code-hl-variable-color: #ffb757;
41
+ --md-code-hl-generic-color: #dbb7ff;
42
42
  }
@@ -145,8 +145,9 @@ class ConfigureCompany < CMDx::Task
145
145
  end
146
146
  ```
147
147
 
148
- ## More Examples
148
+ ## Useful Examples
149
149
 
150
150
  - [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
151
151
  - [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
152
+ - [Sidekiq Async Execution](https://github.com/drexed/cmdx/blob/main/examples/sidekiq_async_execution.md)
152
153
  - [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
@@ -0,0 +1,29 @@
1
+ # Sidekiq Async Execute
2
+
3
+ Execute tasks asynchronously using Sidekiq without creating separate job classes.
4
+
5
+ <https://github.com/sidekiq/sidekiq>
6
+
7
+ ### Setup
8
+
9
+ ```ruby
10
+ class MyTask < CMDx::Task
11
+ include Sidekiq::Job
12
+
13
+ def work
14
+ # Do work...
15
+ end
16
+
17
+ # Use execute! to trigger Sidekiq's retry logic on failures/exceptions.
18
+ def perform
19
+ self.class.execute!
20
+ end
21
+
22
+ end
23
+ ```
24
+
25
+ ### Usage
26
+
27
+ ```ruby
28
+ MyTask.perform_async
29
+ ```
data/lib/cmdx/executor.rb CHANGED
@@ -197,13 +197,6 @@ module CMDx
197
197
 
198
198
  private
199
199
 
200
- # Lazy loaded repeator instance to handle retries.
201
- #
202
- # @rbs () -> untyped
203
- def repeator
204
- @repeator ||= Repeator.new(task)
205
- end
206
-
207
200
  # Performs pre-execution tasks including validation and attribute verification.
208
201
  #
209
202
  # @rbs () -> void
data/lib/cmdx/version.rb CHANGED
@@ -5,6 +5,6 @@ module CMDx
5
5
  # @return [String] the version of the CMDx gem
6
6
  #
7
7
  # @rbs return: String
8
- VERSION = "1.10.0"
8
+ VERSION = "1.10.1"
9
9
 
10
10
  end