cmdx 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -76
- data/LICENSE.txt +3 -20
- data/README.md +8 -7
- data/lib/cmdx/attribute.rb +21 -5
- data/lib/cmdx/context.rb +16 -0
- data/lib/cmdx/executor.rb +9 -9
- data/lib/cmdx/result.rb +27 -7
- data/lib/cmdx/task.rb +19 -0
- data/lib/cmdx/version.rb +1 -1
- data/mkdocs.yml +62 -36
- metadata +3 -57
- data/.cursor/prompts/docs.md +0 -12
- data/.cursor/prompts/llms.md +0 -8
- data/.cursor/prompts/rspec.md +0 -24
- data/.cursor/prompts/yardoc.md +0 -15
- data/.cursor/rules/cursor-instructions.mdc +0 -68
- data/.irbrc +0 -18
- data/.rspec +0 -4
- data/.rubocop.yml +0 -95
- data/.ruby-version +0 -1
- data/.yard-lint.yml +0 -174
- data/.yardopts +0 -7
- data/docs/.DS_Store +0 -0
- data/docs/assets/favicon.ico +0 -0
- data/docs/assets/favicon.svg +0 -1
- data/docs/attributes/coercions.md +0 -155
- data/docs/attributes/defaults.md +0 -77
- data/docs/attributes/definitions.md +0 -283
- data/docs/attributes/naming.md +0 -68
- data/docs/attributes/transformations.md +0 -63
- data/docs/attributes/validations.md +0 -336
- data/docs/basics/chain.md +0 -108
- data/docs/basics/context.md +0 -121
- data/docs/basics/execution.md +0 -152
- data/docs/basics/setup.md +0 -107
- data/docs/callbacks.md +0 -157
- data/docs/configuration.md +0 -314
- data/docs/deprecation.md +0 -143
- data/docs/getting_started.md +0 -137
- data/docs/index.md +0 -134
- data/docs/internationalization.md +0 -126
- data/docs/interruptions/exceptions.md +0 -52
- data/docs/interruptions/faults.md +0 -169
- data/docs/interruptions/halt.md +0 -216
- data/docs/logging.md +0 -90
- data/docs/middlewares.md +0 -191
- data/docs/outcomes/result.md +0 -197
- data/docs/outcomes/states.md +0 -66
- data/docs/outcomes/statuses.md +0 -65
- data/docs/retries.md +0 -121
- data/docs/stylesheets/extra.css +0 -42
- data/docs/tips_and_tricks.md +0 -157
- data/docs/workflows.md +0 -226
- data/examples/active_record_database_transaction.md +0 -27
- data/examples/active_record_query_tagging.md +0 -46
- data/examples/flipper_feature_flags.md +0 -50
- data/examples/paper_trail_whatdunnit.md +0 -39
- data/examples/redis_idempotency.md +0 -71
- data/examples/sentry_error_tracking.md +0 -46
- data/examples/sidekiq_async_execution.md +0 -29
- data/examples/stoplight_circuit_breaker.md +0 -36
- data/src/cmdx-dark-logo.png +0 -0
- data/src/cmdx-favicon.svg +0 -1
- data/src/cmdx-light-logo.png +0 -0
- data/src/cmdx-logo.svg +0 -1
data/docs/outcomes/states.md
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# Outcomes - States
|
|
2
|
-
|
|
3
|
-
States track where a task is in its execution lifecycle—from creation through completion or interruption.
|
|
4
|
-
|
|
5
|
-
## Definitions
|
|
6
|
-
|
|
7
|
-
| State | Description |
|
|
8
|
-
| ----- | ----------- |
|
|
9
|
-
| `initialized` | Task created but execution not yet started. Default state for new tasks. |
|
|
10
|
-
| `executing` | Task is actively running its business logic. Transient state during execution. |
|
|
11
|
-
| `complete` | Task finished execution successfully without any interruption or halt. |
|
|
12
|
-
| `interrupted` | Task execution was stopped due to a fault, exception, or explicit halt. |
|
|
13
|
-
|
|
14
|
-
State-Status combinations:
|
|
15
|
-
|
|
16
|
-
| State | Status | Meaning |
|
|
17
|
-
| ----- | ------ | ------- |
|
|
18
|
-
| `initialized` | `success` | Task created, not yet executed |
|
|
19
|
-
| `executing` | `success` | Task currently running |
|
|
20
|
-
| `complete` | `success` | Task finished successfully |
|
|
21
|
-
| `complete` | `skipped` | Task finished by skipping execution |
|
|
22
|
-
| `interrupted` | `failed` | Task stopped due to failure |
|
|
23
|
-
| `interrupted` | `skipped` | Task stopped by skip condition |
|
|
24
|
-
|
|
25
|
-
## Transitions
|
|
26
|
-
|
|
27
|
-
!!! danger "Caution"
|
|
28
|
-
|
|
29
|
-
States are managed automatically—never modify them manually.
|
|
30
|
-
|
|
31
|
-
```ruby
|
|
32
|
-
# Valid state transition flow
|
|
33
|
-
initialized → executing → complete (successful execution)
|
|
34
|
-
initialized → executing → interrupted (skipped/failed execution)
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Predicates
|
|
38
|
-
|
|
39
|
-
Use state predicates to check the current execution lifecycle:
|
|
40
|
-
|
|
41
|
-
```ruby
|
|
42
|
-
result = ProcessVideoUpload.execute
|
|
43
|
-
|
|
44
|
-
# Individual state checks
|
|
45
|
-
result.initialized? #=> false (after execution)
|
|
46
|
-
result.executing? #=> false (after execution)
|
|
47
|
-
result.complete? #=> true (successful completion)
|
|
48
|
-
result.interrupted? #=> false (no interruption)
|
|
49
|
-
|
|
50
|
-
# State categorization
|
|
51
|
-
result.executed? #=> true (complete OR interrupted)
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Handlers
|
|
55
|
-
|
|
56
|
-
Handle lifecycle events with state-based handlers. Use `on(:executed)` for cleanup that runs regardless of outcome:
|
|
57
|
-
|
|
58
|
-
```ruby
|
|
59
|
-
result = ProcessVideoUpload.execute
|
|
60
|
-
|
|
61
|
-
# Individual state handlers
|
|
62
|
-
result
|
|
63
|
-
.on(:complete) { |result| send_upload_notification(result) }
|
|
64
|
-
.on(:interrupted) { |result| cleanup_temp_files(result) }
|
|
65
|
-
.on(:executed) { |result| log_upload_metrics(result) } #=> .on(:complete, :interrupted)
|
|
66
|
-
```
|
data/docs/outcomes/statuses.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Outcomes - Statuses
|
|
2
|
-
|
|
3
|
-
Statuses represent the business outcome—did the task succeed, skip, or fail? This differs from state, which tracks the execution lifecycle.
|
|
4
|
-
|
|
5
|
-
## Definitions
|
|
6
|
-
|
|
7
|
-
| Status | Description |
|
|
8
|
-
| ------ | ----------- |
|
|
9
|
-
| `success` | Task execution completed successfully with expected business outcome. Default status for all tasks. |
|
|
10
|
-
| `skipped` | Task intentionally stopped execution because conditions weren't met or continuation was unnecessary. |
|
|
11
|
-
| `failed` | Task stopped execution due to business rule violations, validation errors, or exceptions. |
|
|
12
|
-
|
|
13
|
-
## Transitions
|
|
14
|
-
|
|
15
|
-
!!! warning "Important"
|
|
16
|
-
|
|
17
|
-
Status transitions are final and unidirectional. Once skipped or failed, tasks can't return to success.
|
|
18
|
-
|
|
19
|
-
```ruby
|
|
20
|
-
# Valid status transitions
|
|
21
|
-
success → skipped # via skip!
|
|
22
|
-
success → failed # via fail! or exception
|
|
23
|
-
|
|
24
|
-
# Invalid transitions (will raise errors)
|
|
25
|
-
skipped → success # ❌ Cannot transition
|
|
26
|
-
skipped → failed # ❌ Cannot transition
|
|
27
|
-
failed → success # ❌ Cannot transition
|
|
28
|
-
failed → skipped # ❌ Cannot transition
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Predicates
|
|
32
|
-
|
|
33
|
-
Use status predicates to check execution outcomes:
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
result = ProcessNotification.execute
|
|
37
|
-
|
|
38
|
-
# Individual status checks
|
|
39
|
-
result.success? #=> true/false
|
|
40
|
-
result.skipped? #=> true/false
|
|
41
|
-
result.failed? #=> true/false
|
|
42
|
-
|
|
43
|
-
# Outcome categorization
|
|
44
|
-
result.good? #=> true if success OR skipped
|
|
45
|
-
result.bad? #=> true if skipped OR failed (not success)
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Handlers
|
|
49
|
-
|
|
50
|
-
Branch business logic with status-based handlers. Use `on(:good)` and `on(:bad)` for success/skip vs failed outcomes:
|
|
51
|
-
|
|
52
|
-
```ruby
|
|
53
|
-
result = ProcessNotification.execute
|
|
54
|
-
|
|
55
|
-
# Individual status handlers
|
|
56
|
-
result
|
|
57
|
-
.on(:success) { |result| mark_notification_sent(result) }
|
|
58
|
-
.on(:skipped) { |result| log_notification_skipped(result) }
|
|
59
|
-
.on(:failed){ |result| queue_retry_notification(result) }
|
|
60
|
-
|
|
61
|
-
# Outcome-based handlers
|
|
62
|
-
result
|
|
63
|
-
.on(:good) { |result| update_message_stats(result) } #=> .on(:success, :skipped)
|
|
64
|
-
.on(:bad) { |result| track_delivery_failure(result) } #=> .on(:failed, :skipped)
|
|
65
|
-
```
|
data/docs/retries.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# Retries
|
|
2
|
-
|
|
3
|
-
CMDx provides automatic retry functionality for tasks that encounter transient failures. This is essential for handling temporary issues like network timeouts, rate limits, or database locks without manual intervention.
|
|
4
|
-
|
|
5
|
-
## Basic Usage
|
|
6
|
-
|
|
7
|
-
Configure retries upto n attempts without any delay.
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
class FetchExternalData < CMDx::Task
|
|
11
|
-
settings retries: 3
|
|
12
|
-
|
|
13
|
-
def work
|
|
14
|
-
response = HTTParty.get("https://api.example.com/data")
|
|
15
|
-
context.data = response.parsed_response
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
```
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
## Selective Retries
|
|
23
|
-
|
|
24
|
-
By default, CMDx retries on `StandardError` and its subclasses. Narrow this to specific exception types:
|
|
25
|
-
|
|
26
|
-
```ruby
|
|
27
|
-
class ProcessPayment < CMDx::Task
|
|
28
|
-
settings retries: 5, retry_on: [Stripe::RateLimitError, Net::ReadTimeout]
|
|
29
|
-
|
|
30
|
-
def work
|
|
31
|
-
# Your logic here...
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
!!! warning "Important"
|
|
37
|
-
|
|
38
|
-
Only exceptions matching the `retry_on` configuration will trigger retries. Uncaught exceptions immediately fail the task.
|
|
39
|
-
|
|
40
|
-
## Retry Jitter
|
|
41
|
-
|
|
42
|
-
Add delays between retry attempts to avoid overwhelming external services or to implement exponential backoff strategies.
|
|
43
|
-
|
|
44
|
-
### Fixed Value
|
|
45
|
-
|
|
46
|
-
Use a numeric value to calculate linear delay (`jitter * current_retry`):
|
|
47
|
-
|
|
48
|
-
```ruby
|
|
49
|
-
class ImportRecords < CMDx::Task
|
|
50
|
-
settings retries: 3, retry_jitter: 0.5
|
|
51
|
-
|
|
52
|
-
def work
|
|
53
|
-
# Delays: 0s, 0.5s (retry 1), 1.0s (retry 2), 1.5s (retry 3)
|
|
54
|
-
context.records = ExternalAPI.fetch_records
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### Symbol References
|
|
60
|
-
|
|
61
|
-
Define an instance method for custom delay logic:
|
|
62
|
-
|
|
63
|
-
```ruby
|
|
64
|
-
class SyncInventory < CMDx::Task
|
|
65
|
-
settings retries: 5, retry_jitter: :exponential_backoff
|
|
66
|
-
|
|
67
|
-
def work
|
|
68
|
-
context.inventory = InventoryAPI.sync
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
private
|
|
72
|
-
|
|
73
|
-
def exponential_backoff(current_retry)
|
|
74
|
-
2 ** current_retry # 2s, 4s, 8s, 16s, 32s
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
### Proc or Lambda
|
|
80
|
-
|
|
81
|
-
Pass a proc for inline delay calculations:
|
|
82
|
-
|
|
83
|
-
```ruby
|
|
84
|
-
class PollJobStatus < CMDx::Task
|
|
85
|
-
# Proc
|
|
86
|
-
settings retries: 10, retry_jitter: proc { |retry_count| [retry_count * 0.5, 5.0].min }
|
|
87
|
-
|
|
88
|
-
# Lambda
|
|
89
|
-
settings retries: 10, retry_jitter: ->(retry_count) { [retry_count * 0.5, 5.0].min }
|
|
90
|
-
|
|
91
|
-
def work
|
|
92
|
-
# Delays: 0.5s, 1.0s, 1.5s, 2.0s, 2.5s, 3.0s, 3.5s, 4.0s, 4.5s, 5.0s (capped)
|
|
93
|
-
context.status = JobAPI.check_status(context.job_id)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Class or Module
|
|
99
|
-
|
|
100
|
-
Implement reusable delay logic in dedicated modules and classes:
|
|
101
|
-
|
|
102
|
-
```ruby
|
|
103
|
-
class ExponentialBackoff
|
|
104
|
-
def call(task, retry_count)
|
|
105
|
-
base_delay = task.context.base_delay || 1.0
|
|
106
|
-
[base_delay * (2 ** retry_count), 60.0].min
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
class FetchUserProfile < CMDx::Task
|
|
111
|
-
# Class or Module
|
|
112
|
-
settings retries: 4, retry_jitter: ExponentialBackoff
|
|
113
|
-
|
|
114
|
-
# Instance
|
|
115
|
-
settings retries: 4, retry_jitter: ExponentialBackoff.new
|
|
116
|
-
|
|
117
|
-
def work
|
|
118
|
-
# Your logic here...
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
```
|
data/docs/stylesheets/extra.css
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
:root > * {
|
|
2
|
-
/* Primary color shades */
|
|
3
|
-
--md-primary-fg-color: #fe1817;
|
|
4
|
-
--md-primary-fg-color--light: #fe1817;
|
|
5
|
-
--md-primary-fg-color--dark: #fe1817;
|
|
6
|
-
|
|
7
|
-
/* Accent color shades */
|
|
8
|
-
--md-accent-fg-color: hsla(#{hex2hsl(#fe1817)}, 1);
|
|
9
|
-
--md-accent-fg-color--transparent: hsla(#{hex2hsl(#fe1817)}, 0.1);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/* GitHub High Contrast Light syntax highlighting */
|
|
13
|
-
[data-md-color-scheme="default"] {
|
|
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
|
-
}
|
|
27
|
-
|
|
28
|
-
/* GitHub High Contrast Dark syntax highlighting */
|
|
29
|
-
[data-md-color-scheme="slate"] {
|
|
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
|
-
}
|
data/docs/tips_and_tricks.md
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# Tips and Tricks
|
|
2
|
-
|
|
3
|
-
Best practices, patterns, and techniques to build maintainable CMDx applications.
|
|
4
|
-
|
|
5
|
-
## Project Organization
|
|
6
|
-
|
|
7
|
-
### Directory Structure
|
|
8
|
-
|
|
9
|
-
Create a well-organized command structure for maintainable applications:
|
|
10
|
-
|
|
11
|
-
```text
|
|
12
|
-
/app/
|
|
13
|
-
└── /tasks/
|
|
14
|
-
├── /invoices/
|
|
15
|
-
│ ├── calculate_tax.rb
|
|
16
|
-
│ ├── validate_invoice.rb
|
|
17
|
-
│ ├── send_invoice.rb
|
|
18
|
-
│ └── process_invoice.rb # workflow
|
|
19
|
-
├── /reports/
|
|
20
|
-
│ ├── generate_pdf.rb
|
|
21
|
-
│ ├── compile_data.rb
|
|
22
|
-
│ ├── export_csv.rb
|
|
23
|
-
│ └── create_reports.rb # workflow
|
|
24
|
-
├── application_task.rb # base class
|
|
25
|
-
├── authenticate_session.rb
|
|
26
|
-
└── activate_account.rb
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Naming Conventions
|
|
30
|
-
|
|
31
|
-
Follow consistent naming patterns for clarity and maintainability:
|
|
32
|
-
|
|
33
|
-
```ruby
|
|
34
|
-
# Verb + Noun
|
|
35
|
-
class ExportData < CMDx::Task; end
|
|
36
|
-
class CompressFile < CMDx::Task; end
|
|
37
|
-
class ValidateSchema < CMDx::Task; end
|
|
38
|
-
|
|
39
|
-
# Use present tense verbs for actions
|
|
40
|
-
class GenerateToken < CMDx::Task; end # ✓ Good
|
|
41
|
-
class GeneratingToken < CMDx::Task; end # ❌ Avoid
|
|
42
|
-
class TokenGeneration < CMDx::Task; end # ❌ Avoid
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Story Telling
|
|
46
|
-
|
|
47
|
-
Break down complex logic into descriptive methods that read like a narrative:
|
|
48
|
-
|
|
49
|
-
```ruby
|
|
50
|
-
class ProcessOrder < CMDx::Task
|
|
51
|
-
def work
|
|
52
|
-
charge_payment_method
|
|
53
|
-
assign_to_warehouse
|
|
54
|
-
send_notification
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
private
|
|
58
|
-
|
|
59
|
-
def charge_payment_method
|
|
60
|
-
order.primary_payment_method.charge!
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def assign_to_warehouse
|
|
64
|
-
order.ready_for_shipping!
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def send_notification
|
|
68
|
-
if order.products_out_of_stock?
|
|
69
|
-
OrderMailer.pending(order).deliver
|
|
70
|
-
else
|
|
71
|
-
OrderMailer.preparing(order).deliver
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Style Guide
|
|
78
|
-
|
|
79
|
-
Follow this order for consistent, readable tasks:
|
|
80
|
-
|
|
81
|
-
```ruby
|
|
82
|
-
class ExportReport < CMDx::Task
|
|
83
|
-
|
|
84
|
-
# 1. Register functions
|
|
85
|
-
register :middleware, CMDx::Middlewares::Correlate
|
|
86
|
-
register :validator, :format, FormatValidator
|
|
87
|
-
|
|
88
|
-
# 2. Define callbacks
|
|
89
|
-
before_execution :find_report
|
|
90
|
-
on_complete :track_export_metrics, if: ->(task) { Current.tenant.analytics? }
|
|
91
|
-
|
|
92
|
-
# 3. Declare attributes
|
|
93
|
-
attributes :user_id
|
|
94
|
-
required :report_id
|
|
95
|
-
optional :format_type
|
|
96
|
-
|
|
97
|
-
# 4. Define work method
|
|
98
|
-
def work
|
|
99
|
-
report.compile!
|
|
100
|
-
report.export!
|
|
101
|
-
|
|
102
|
-
context.exported_at = Time.now
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# TIP: Favor private business logic to reduce the surface of the public API.
|
|
106
|
-
private
|
|
107
|
-
|
|
108
|
-
# 5. Build helper functions
|
|
109
|
-
def find_report
|
|
110
|
-
@report ||= Report.find(report_id)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
def track_export_metrics
|
|
114
|
-
Analytics.increment(:report_exported)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
end
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
## Attribute Options
|
|
121
|
-
|
|
122
|
-
Use `with_options` to reduce duplication:
|
|
123
|
-
|
|
124
|
-
```ruby
|
|
125
|
-
class ConfigureCompany < CMDx::Task
|
|
126
|
-
# Apply common options to multiple attributes
|
|
127
|
-
with_options(type: :string, presence: true) do
|
|
128
|
-
attributes :website, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
|
|
129
|
-
required :company_name, :industry
|
|
130
|
-
optional :description, format: { with: /\A[\w\s\-\.,!?]+\z/ }
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# Nested attributes with shared prefix
|
|
134
|
-
required :headquarters do
|
|
135
|
-
with_options(prefix: :hq_) do
|
|
136
|
-
attributes :street, :city, :zip_code, type: :string
|
|
137
|
-
required :country, type: :string, inclusion: { in: VALID_COUNTRIES }
|
|
138
|
-
optional :region, type: :string
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def work
|
|
143
|
-
# Your logic here...
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
## Useful Examples
|
|
149
|
-
|
|
150
|
-
- [Active Record Database Transaction](https://github.com/drexed/cmdx/blob/main/examples/active_record_database_transaction.md)
|
|
151
|
-
- [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
|
|
152
|
-
- [Flipper Feature Flags](https://github.com/drexed/cmdx/blob/main/examples/flipper_feature_flags.md)
|
|
153
|
-
- [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
|
|
154
|
-
- [Redis Idempotency](https://github.com/drexed/cmdx/blob/main/examples/redis_idempotency.md)
|
|
155
|
-
- [Sentry Error Tracking](https://github.com/drexed/cmdx/blob/main/examples/sentry_error_tracking.md)
|
|
156
|
-
- [Sidekiq Async Execution](https://github.com/drexed/cmdx/blob/main/examples/sidekiq_async_execution.md)
|
|
157
|
-
- [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
|
data/docs/workflows.md
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
# Workflows
|
|
2
|
-
|
|
3
|
-
Compose multiple tasks into powerful, sequential pipelines. Workflows provide a declarative way to build complex business processes with conditional execution, shared context, and flexible error handling.
|
|
4
|
-
|
|
5
|
-
## Declarations
|
|
6
|
-
|
|
7
|
-
Tasks run in declaration order (FIFO), sharing a common context across the pipeline.
|
|
8
|
-
|
|
9
|
-
!!! warning
|
|
10
|
-
|
|
11
|
-
Don't define a `work` method in workflows—the module handles execution automatically.
|
|
12
|
-
|
|
13
|
-
### Task
|
|
14
|
-
|
|
15
|
-
```ruby
|
|
16
|
-
class OnboardingWorkflow < CMDx::Task
|
|
17
|
-
include CMDx::Workflow
|
|
18
|
-
|
|
19
|
-
task CreateUserProfile
|
|
20
|
-
task SetupAccountPreferences
|
|
21
|
-
|
|
22
|
-
tasks SendWelcomeEmail, SendWelcomeSms, CreateDashboard
|
|
23
|
-
end
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
!!! tip
|
|
27
|
-
|
|
28
|
-
Execute tasks in parallel via the [cmdx-parallel](https://github.com/drexed/cmdx-parallel) gem.
|
|
29
|
-
|
|
30
|
-
### Group
|
|
31
|
-
|
|
32
|
-
Group related tasks to share configuration:
|
|
33
|
-
|
|
34
|
-
!!! warning "Important"
|
|
35
|
-
|
|
36
|
-
Settings and conditionals apply to all tasks in the group.
|
|
37
|
-
|
|
38
|
-
```ruby
|
|
39
|
-
class ContentModerationWorkflow < CMDx::Task
|
|
40
|
-
include CMDx::Workflow
|
|
41
|
-
|
|
42
|
-
# Screening phase
|
|
43
|
-
tasks ScanForProfanity, CheckForSpam, ValidateImages, breakpoints: ["skipped"]
|
|
44
|
-
|
|
45
|
-
# Review phase
|
|
46
|
-
tasks ApplyFilters, ScoreContent, FlagSuspicious
|
|
47
|
-
|
|
48
|
-
# Decision phase
|
|
49
|
-
tasks PublishContent, QueueForReview, NotifyModerators
|
|
50
|
-
end
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Conditionals
|
|
54
|
-
|
|
55
|
-
Conditionals support multiple syntaxes for flexible execution control:
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
class ContentAccessCheck
|
|
59
|
-
def call(task)
|
|
60
|
-
task.context.user.can?(:publish_content)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
class OnboardingWorkflow < CMDx::Task
|
|
65
|
-
include CMDx::Workflow
|
|
66
|
-
|
|
67
|
-
# If and/or Unless
|
|
68
|
-
task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
|
|
69
|
-
|
|
70
|
-
# Proc
|
|
71
|
-
task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
|
|
72
|
-
|
|
73
|
-
# Lambda
|
|
74
|
-
task SendWelcomeEmail, if: proc { context.features_enabled? }
|
|
75
|
-
|
|
76
|
-
# Class or Module
|
|
77
|
-
task SendWelcomeEmail, unless: ContentAccessCheck
|
|
78
|
-
|
|
79
|
-
# Instance
|
|
80
|
-
task SendWelcomeEmail, if: ContentAccessCheck.new
|
|
81
|
-
|
|
82
|
-
# Conditional applies to all tasks of this declaration group
|
|
83
|
-
tasks SendWelcomeEmail, CreateDashboard, SetupTutorial, if: :email_configured?
|
|
84
|
-
|
|
85
|
-
private
|
|
86
|
-
|
|
87
|
-
def email_configured?
|
|
88
|
-
context.user.email_address == true
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def email_disabled?
|
|
92
|
-
context.user.communication_preference == :disabled
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
## Halt Behavior
|
|
98
|
-
|
|
99
|
-
By default, skipped tasks don't stop the workflow—they're treated as no-ops. Configure breakpoints globally or per-task to customize this behavior.
|
|
100
|
-
|
|
101
|
-
```ruby
|
|
102
|
-
class AnalyticsWorkflow < CMDx::Task
|
|
103
|
-
include CMDx::Workflow
|
|
104
|
-
|
|
105
|
-
task CollectMetrics # If fails → workflow stops
|
|
106
|
-
task FilterOutliers # If skipped → workflow continues
|
|
107
|
-
task GenerateDashboard # Only runs if no failures occurred
|
|
108
|
-
end
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Task Configuration
|
|
112
|
-
|
|
113
|
-
Configure halt behavior for the entire workflow:
|
|
114
|
-
|
|
115
|
-
```ruby
|
|
116
|
-
class SecurityWorkflow < CMDx::Task
|
|
117
|
-
include CMDx::Workflow
|
|
118
|
-
|
|
119
|
-
# Halt on both failed and skipped results
|
|
120
|
-
settings(workflow_breakpoints: ["skipped", "failed"])
|
|
121
|
-
|
|
122
|
-
task PerformSecurityScan
|
|
123
|
-
task ValidateSecurityRules
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
class OptionalTasksWorkflow < CMDx::Task
|
|
127
|
-
include CMDx::Workflow
|
|
128
|
-
|
|
129
|
-
# Never halt, always continue
|
|
130
|
-
settings(breakpoints: [])
|
|
131
|
-
|
|
132
|
-
task TryBackupData
|
|
133
|
-
task TryCleanupLogs
|
|
134
|
-
task TryOptimizeCache
|
|
135
|
-
end
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Group Configuration
|
|
139
|
-
|
|
140
|
-
Different task groups can have different halt behavior:
|
|
141
|
-
|
|
142
|
-
```ruby
|
|
143
|
-
class SubscriptionWorkflow < CMDx::Task
|
|
144
|
-
include CMDx::Workflow
|
|
145
|
-
|
|
146
|
-
task CreateSubscription, ValidatePayment, workflow_breakpoints: ["skipped", "failed"]
|
|
147
|
-
|
|
148
|
-
# Never halt, always continue
|
|
149
|
-
task SendConfirmationEmail, UpdateBilling, breakpoints: []
|
|
150
|
-
end
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Nested Workflows
|
|
154
|
-
|
|
155
|
-
Build hierarchical workflows by composing workflows within workflows:
|
|
156
|
-
|
|
157
|
-
```ruby
|
|
158
|
-
class EmailPreparationWorkflow < CMDx::Task
|
|
159
|
-
include CMDx::Workflow
|
|
160
|
-
|
|
161
|
-
task ValidateRecipients
|
|
162
|
-
task CompileTemplate
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
class EmailDeliveryWorkflow < CMDx::Task
|
|
166
|
-
include CMDx::Workflow
|
|
167
|
-
|
|
168
|
-
tasks SendEmails, TrackDeliveries
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
class CompleteEmailWorkflow < CMDx::Task
|
|
172
|
-
include CMDx::Workflow
|
|
173
|
-
|
|
174
|
-
task EmailPreparationWorkflow
|
|
175
|
-
task EmailDeliveryWorkflow, if: proc { context.preparation_successful? }
|
|
176
|
-
task GenerateDeliveryReport
|
|
177
|
-
end
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
## Parallel Execution
|
|
181
|
-
|
|
182
|
-
Run tasks concurrently using the [Parallel](https://github.com/grosser/parallel) gem. It automatically uses all available processors for maximum throughput.
|
|
183
|
-
|
|
184
|
-
!!! warning
|
|
185
|
-
|
|
186
|
-
Context is read-only during parallel execution. Load all required data beforehand.
|
|
187
|
-
|
|
188
|
-
```ruby
|
|
189
|
-
class SendWelcomeNotifications < CMDx::Task
|
|
190
|
-
include CMDx::Workflow
|
|
191
|
-
|
|
192
|
-
# Default options (dynamically calculated to available processors)
|
|
193
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel
|
|
194
|
-
|
|
195
|
-
# Fix number of threads
|
|
196
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_threads: 2
|
|
197
|
-
|
|
198
|
-
# Fix number of forked processes
|
|
199
|
-
tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_processes: 2
|
|
200
|
-
|
|
201
|
-
# NOTE: Reactors are not supported
|
|
202
|
-
end
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## Task Generator
|
|
206
|
-
|
|
207
|
-
Generate new CMDx workflow tasks quickly using the built-in generator:
|
|
208
|
-
|
|
209
|
-
```bash
|
|
210
|
-
rails generate cmdx:workflow SendNotifications
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
This creates a new workflow task file with the basic structure:
|
|
214
|
-
|
|
215
|
-
```ruby
|
|
216
|
-
# app/tasks/send_notifications.rb
|
|
217
|
-
class SendNotifications < CMDx::Task
|
|
218
|
-
include CMDx::Workflow
|
|
219
|
-
|
|
220
|
-
tasks Task1, Task2
|
|
221
|
-
end
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
!!! tip
|
|
225
|
-
|
|
226
|
-
Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# Active Record Query Tagging
|
|
2
|
-
|
|
3
|
-
Wrap task or workflow execution in a database transaction. This is essential for data integrity when multiple steps modify the database.
|
|
4
|
-
|
|
5
|
-
### Setup
|
|
6
|
-
|
|
7
|
-
```ruby
|
|
8
|
-
# lib/cmdx_database_transaction_middleware.rb
|
|
9
|
-
class CmdxDatabaseTransactionMiddleware
|
|
10
|
-
def self.call(task, **options, &)
|
|
11
|
-
ActiveRecord::Base.transaction(requires_new: true, &)
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
### Usage
|
|
17
|
-
|
|
18
|
-
```ruby
|
|
19
|
-
class MyTask < CMDx::Task
|
|
20
|
-
register :middleware, CmdxDatabaseTransactionMiddleware
|
|
21
|
-
|
|
22
|
-
def work
|
|
23
|
-
# Do work...
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
end
|
|
27
|
-
```
|