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
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
# Attributes - Validations
|
|
2
|
-
|
|
3
|
-
Ensure inputs meet requirements before execution. Validations run after coercions, giving you declarative data integrity checks.
|
|
4
|
-
|
|
5
|
-
See [Global Configuration](../getting_started.md#validators) for custom validator setup.
|
|
6
|
-
|
|
7
|
-
## Usage
|
|
8
|
-
|
|
9
|
-
Define validation rules on attributes to enforce data requirements:
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class ProcessSubscription < CMDx::Task
|
|
13
|
-
# Required field with presence validation
|
|
14
|
-
attribute :user_id, presence: true
|
|
15
|
-
|
|
16
|
-
# String with length constraints
|
|
17
|
-
optional :preferences, length: { minimum: 10, maximum: 500 }
|
|
18
|
-
|
|
19
|
-
# Numeric range validation
|
|
20
|
-
required :tier_level, inclusion: { in: 1..5 }
|
|
21
|
-
|
|
22
|
-
# Format validation for email
|
|
23
|
-
attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
24
|
-
|
|
25
|
-
def work
|
|
26
|
-
user_id #=> "98765"
|
|
27
|
-
preferences #=> "Send weekly digest emails"
|
|
28
|
-
tier_level #=> 3
|
|
29
|
-
contact_email #=> "user@company.com"
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
ProcessSubscription.execute(
|
|
34
|
-
user_id: "98765",
|
|
35
|
-
preferences: "Send weekly digest emails",
|
|
36
|
-
tier_level: 3,
|
|
37
|
-
contact_email: "user@company.com"
|
|
38
|
-
)
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
!!! tip
|
|
42
|
-
|
|
43
|
-
Validations run after coercions, so you can validate the final coerced values rather than raw input.
|
|
44
|
-
|
|
45
|
-
## Built-in Validators
|
|
46
|
-
|
|
47
|
-
### Common Options
|
|
48
|
-
|
|
49
|
-
```ruby
|
|
50
|
-
class ProcessProduct < CMDx::Task
|
|
51
|
-
# Allow nil
|
|
52
|
-
attribute :tier_level, inclusion: {
|
|
53
|
-
in: 1..5,
|
|
54
|
-
allow_nil: true
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
# Conditionals
|
|
58
|
-
optional :contact_email, format: {
|
|
59
|
-
with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
|
|
60
|
-
if: ->(value) { value.includes?("@") }
|
|
61
|
-
}
|
|
62
|
-
required :status, exclusion: {
|
|
63
|
-
in: %w[recalled archived],
|
|
64
|
-
unless: :product_sunsetted?
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
# Custom message
|
|
68
|
-
attribute :title, length: {
|
|
69
|
-
within: 5..100,
|
|
70
|
-
message: "must be in optimal size"
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
def work
|
|
74
|
-
# Your logic here...
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
private
|
|
78
|
-
|
|
79
|
-
def product_defunct?(value)
|
|
80
|
-
context.company.out_of_business? || value == "deprecated"
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
This list of options is available to all validators:
|
|
86
|
-
|
|
87
|
-
| Option | Description |
|
|
88
|
-
|--------|-------------|
|
|
89
|
-
| `:allow_nil` | Skip validation when value is `nil` |
|
|
90
|
-
| `:if` | Symbol, proc, lambda, or callable determining when to validate |
|
|
91
|
-
| `:unless` | Symbol, proc, lambda, or callable determining when to skip validation |
|
|
92
|
-
| `:message` | Custom error message for validation failures |
|
|
93
|
-
|
|
94
|
-
### Exclusion
|
|
95
|
-
|
|
96
|
-
```ruby
|
|
97
|
-
class ProcessProduct < CMDx::Task
|
|
98
|
-
attribute :status, exclusion: { in: %w[recalled archived] }
|
|
99
|
-
|
|
100
|
-
def work
|
|
101
|
-
# Your logic here...
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
| Options | Description |
|
|
107
|
-
|---------|-------------|
|
|
108
|
-
| `:in` | The collection of forbidden values or range |
|
|
109
|
-
| `:within` | Alias for :in option |
|
|
110
|
-
| `:of_message` | Custom message for discrete value exclusions |
|
|
111
|
-
| `:in_message` | Custom message for range-based exclusions |
|
|
112
|
-
| `:within_message` | Alias for :in_message option |
|
|
113
|
-
|
|
114
|
-
### Format
|
|
115
|
-
|
|
116
|
-
```ruby
|
|
117
|
-
class ProcessProduct < CMDx::Task
|
|
118
|
-
attribute :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/
|
|
119
|
-
|
|
120
|
-
attribute :sku, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ }
|
|
121
|
-
|
|
122
|
-
def work
|
|
123
|
-
# Your logic here...
|
|
124
|
-
end
|
|
125
|
-
end
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
| Options | Description |
|
|
129
|
-
|---------|-------------|
|
|
130
|
-
| `regexp` | Alias for :with option |
|
|
131
|
-
| `:with` | Regex pattern that the value must match |
|
|
132
|
-
| `:without` | Regex pattern that the value must not match |
|
|
133
|
-
|
|
134
|
-
### Inclusion
|
|
135
|
-
|
|
136
|
-
```ruby
|
|
137
|
-
class ProcessProduct < CMDx::Task
|
|
138
|
-
attribute :availability, inclusion: { in: %w[available limited] }
|
|
139
|
-
|
|
140
|
-
def work
|
|
141
|
-
# Your logic here...
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
| Options | Description |
|
|
147
|
-
|---------|-------------|
|
|
148
|
-
| `:in` | The collection of allowed values or range |
|
|
149
|
-
| `:within` | Alias for :in option |
|
|
150
|
-
| `:of_message` | Custom message for discrete value inclusions |
|
|
151
|
-
| `:in_message` | Custom message for range-based inclusions |
|
|
152
|
-
| `:within_message` | Alias for :in_message option |
|
|
153
|
-
|
|
154
|
-
### Length
|
|
155
|
-
|
|
156
|
-
```ruby
|
|
157
|
-
class CreateBlogPost < CMDx::Task
|
|
158
|
-
attribute :title, length: { within: 5..100 }
|
|
159
|
-
|
|
160
|
-
def work
|
|
161
|
-
# Your logic here...
|
|
162
|
-
end
|
|
163
|
-
end
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
| Options | Description |
|
|
167
|
-
|---------|-------------|
|
|
168
|
-
| `:within` | Range that the length must fall within (inclusive) |
|
|
169
|
-
| `:not_within` | Range that the length must not fall within |
|
|
170
|
-
| `:in` | Alias for :within |
|
|
171
|
-
| `:not_in` | Range that the length must not fall within |
|
|
172
|
-
| `:min` | Minimum allowed length |
|
|
173
|
-
| `:max` | Maximum allowed length |
|
|
174
|
-
| `:is` | Exact required length |
|
|
175
|
-
| `:is_not` | Length that is not allowed |
|
|
176
|
-
| `:within_message` | Custom message for within/range validations |
|
|
177
|
-
| `:in_message` | Custom message for :in validation |
|
|
178
|
-
| `:not_within_message` | Custom message for not_within validation |
|
|
179
|
-
| `:not_in_message` | Custom message for not_in validation |
|
|
180
|
-
| `:min_message` | Custom message for minimum length validation |
|
|
181
|
-
| `:max_message` | Custom message for maximum length validation |
|
|
182
|
-
| `:is_message` | Custom message for exact length validation |
|
|
183
|
-
| `:is_not_message` | Custom message for is_not validation |
|
|
184
|
-
|
|
185
|
-
### Numeric
|
|
186
|
-
|
|
187
|
-
```ruby
|
|
188
|
-
class CreateBlogPost < CMDx::Task
|
|
189
|
-
attribute :word_count, numeric: { min: 100 }
|
|
190
|
-
|
|
191
|
-
def work
|
|
192
|
-
# Your logic here...
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
| Options | Description |
|
|
198
|
-
|---------|-------------|
|
|
199
|
-
| `:within` | Range that the value must fall within (inclusive) |
|
|
200
|
-
| `:not_within` | Range that the value must not fall within |
|
|
201
|
-
| `:in` | Alias for :within option |
|
|
202
|
-
| `:not_in` | Alias for :not_within option |
|
|
203
|
-
| `:min` | Minimum allowed value (inclusive, >=) |
|
|
204
|
-
| `:max` | Maximum allowed value (inclusive, <=) |
|
|
205
|
-
| `:is` | Exact value that must match |
|
|
206
|
-
| `:is_not` | Value that must not match |
|
|
207
|
-
| `:within_message` | Custom message for range validations |
|
|
208
|
-
| `:not_within_message` | Custom message for exclusion validations |
|
|
209
|
-
| `:min_message` | Custom message for minimum validation |
|
|
210
|
-
| `:max_message` | Custom message for maximum validation |
|
|
211
|
-
| `:is_message` | Custom message for exact match validation |
|
|
212
|
-
| `:is_not_message` | Custom message for exclusion validation |
|
|
213
|
-
|
|
214
|
-
### Presence
|
|
215
|
-
|
|
216
|
-
```ruby
|
|
217
|
-
class CreateBlogPost < CMDx::Task
|
|
218
|
-
attribute :content, presence: true
|
|
219
|
-
|
|
220
|
-
attribute :content, presence: { message: "cannot be blank" }
|
|
221
|
-
|
|
222
|
-
def work
|
|
223
|
-
# Your logic here...
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
| Options | Description |
|
|
229
|
-
|---------|-------------|
|
|
230
|
-
| `true` | Ensures value is not nil, empty string, or whitespace |
|
|
231
|
-
|
|
232
|
-
## Declarations
|
|
233
|
-
|
|
234
|
-
!!! warning "Important"
|
|
235
|
-
|
|
236
|
-
Custom validators must raise `CMDx::ValidationError` with a descriptive message.
|
|
237
|
-
|
|
238
|
-
### Proc or Lambda
|
|
239
|
-
|
|
240
|
-
Use anonymous functions for simple validation logic:
|
|
241
|
-
|
|
242
|
-
```ruby
|
|
243
|
-
class SetupApplication < CMDx::Task
|
|
244
|
-
# Proc
|
|
245
|
-
register :validator, :api_key, proc do |value, options = {}|
|
|
246
|
-
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
|
|
247
|
-
raise CMDx::ValidationError, "invalid API key format"
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
# Lambda
|
|
252
|
-
register :validator, :api_key, ->(value, options = {}) {
|
|
253
|
-
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
|
|
254
|
-
raise CMDx::ValidationError, "invalid API key format"
|
|
255
|
-
end
|
|
256
|
-
}
|
|
257
|
-
end
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
### Class or Module
|
|
261
|
-
|
|
262
|
-
Register custom validation logic for specialized requirements:
|
|
263
|
-
|
|
264
|
-
```ruby
|
|
265
|
-
class ApiKeyValidator
|
|
266
|
-
def self.call(value, options = {})
|
|
267
|
-
unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
|
|
268
|
-
raise CMDx::ValidationError, "invalid API key format"
|
|
269
|
-
end
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
class SetupApplication < CMDx::Task
|
|
274
|
-
register :validator, :api_key, ApiKeyValidator
|
|
275
|
-
|
|
276
|
-
attribute :access_key, api_key: true
|
|
277
|
-
end
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## Removals
|
|
281
|
-
|
|
282
|
-
Remove unwanted validators:
|
|
283
|
-
|
|
284
|
-
!!! warning
|
|
285
|
-
|
|
286
|
-
Each `deregister` call removes one validator. Use multiple calls for batch removals.
|
|
287
|
-
|
|
288
|
-
```ruby
|
|
289
|
-
class SetupApplication < CMDx::Task
|
|
290
|
-
deregister :validator, :api_key
|
|
291
|
-
end
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
## Error Handling
|
|
295
|
-
|
|
296
|
-
Validation failures provide detailed, structured error messages:
|
|
297
|
-
|
|
298
|
-
```ruby
|
|
299
|
-
class CreateProject < CMDx::Task
|
|
300
|
-
attribute :project_name,
|
|
301
|
-
presence: true,
|
|
302
|
-
length: { minimum: 3, maximum: 50 }
|
|
303
|
-
optional :budget,
|
|
304
|
-
numeric: { greater_than: 1000, less_than: 1000000 }
|
|
305
|
-
required :priority,
|
|
306
|
-
inclusion: { in: [:low, :medium, :high] }
|
|
307
|
-
attribute :contact_email,
|
|
308
|
-
format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
309
|
-
|
|
310
|
-
def work
|
|
311
|
-
# Your logic here...
|
|
312
|
-
end
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
result = CreateProject.execute(
|
|
316
|
-
project_name: "AB", # Too short
|
|
317
|
-
budget: 500, # Too low
|
|
318
|
-
priority: :urgent, # Not in allowed list
|
|
319
|
-
contact_email: "invalid-email" # Invalid format
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
result.state #=> "interrupted"
|
|
323
|
-
result.status #=> "failed"
|
|
324
|
-
result.reason #=> "Invalid"
|
|
325
|
-
result.metadata #=> {
|
|
326
|
-
# errors: {
|
|
327
|
-
# 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.",
|
|
328
|
-
# messages: {
|
|
329
|
-
# project_name: ["is too short (minimum is 3 characters)"],
|
|
330
|
-
# budget: ["must be greater than 1000"],
|
|
331
|
-
# priority: ["is not included in the list"],
|
|
332
|
-
# contact_email: ["is invalid"]
|
|
333
|
-
# }
|
|
334
|
-
# }
|
|
335
|
-
# }
|
|
336
|
-
```
|
data/docs/basics/chain.md
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
# Basics - Chain
|
|
2
|
-
|
|
3
|
-
Chains automatically track related task executions within a thread. Think of them as execution traces that help you understand what happened and in what order.
|
|
4
|
-
|
|
5
|
-
## Management
|
|
6
|
-
|
|
7
|
-
Each thread maintains its own isolated chain using thread-local storage.
|
|
8
|
-
|
|
9
|
-
!!! warning
|
|
10
|
-
|
|
11
|
-
Chains are thread-local. Don't share chain references across threads—it causes race conditions.
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
# Thread A
|
|
15
|
-
Thread.new do
|
|
16
|
-
result = ImportDataset.execute(file_path: "/data/batch1.csv")
|
|
17
|
-
result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# Thread B (completely separate chain)
|
|
21
|
-
Thread.new do
|
|
22
|
-
result = ImportDataset.execute(file_path: "/data/batch2.csv")
|
|
23
|
-
result.chain.id #=> "z3a42b95-c821-7892-b156-dd7c921fe2a3"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Access current thread's chain
|
|
27
|
-
CMDx::Chain.current #=> Returns current chain or nil
|
|
28
|
-
CMDx::Chain.clear #=> Clears current thread's chain
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Links
|
|
32
|
-
|
|
33
|
-
Tasks automatically create or join the current thread's chain:
|
|
34
|
-
|
|
35
|
-
!!! warning "Important"
|
|
36
|
-
|
|
37
|
-
Chain management is automatic—no manual lifecycle handling needed.
|
|
38
|
-
|
|
39
|
-
```ruby
|
|
40
|
-
class ImportDataset < CMDx::Task
|
|
41
|
-
def work
|
|
42
|
-
# First task creates new chain
|
|
43
|
-
result1 = ValidateHeaders.execute(file_path: context.file_path)
|
|
44
|
-
result1.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
|
45
|
-
result1.chain.results.size #=> 1
|
|
46
|
-
|
|
47
|
-
# Second task joins existing chain
|
|
48
|
-
result2 = SendNotification.execute(to: "admin@company.com")
|
|
49
|
-
result2.chain.id == result1.chain.id #=> true
|
|
50
|
-
result2.chain.results.size #=> 2
|
|
51
|
-
|
|
52
|
-
# Both results reference the same chain
|
|
53
|
-
result1.chain.results == result2.chain.results #=> true
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
## Inheritance
|
|
59
|
-
|
|
60
|
-
Subtasks automatically inherit the current thread's chain, building a unified execution trail:
|
|
61
|
-
|
|
62
|
-
```ruby
|
|
63
|
-
class ImportDataset < CMDx::Task
|
|
64
|
-
def work
|
|
65
|
-
context.dataset = Dataset.find(context.dataset_id)
|
|
66
|
-
|
|
67
|
-
# Subtasks automatically inherit current chain
|
|
68
|
-
ValidateSchema.execute
|
|
69
|
-
TransformData.execute!(context)
|
|
70
|
-
SaveToDatabase.execute(dataset_id: context.dataset_id)
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
result = ImportDataset.execute(dataset_id: 456)
|
|
75
|
-
chain = result.chain
|
|
76
|
-
|
|
77
|
-
# All tasks share the same chain
|
|
78
|
-
chain.results.size #=> 4 (main task + 3 subtasks)
|
|
79
|
-
chain.results.map { |r| r.task.class }
|
|
80
|
-
#=> [ImportDataset, ValidateSchema, TransformData, SaveToDatabase]
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Structure
|
|
84
|
-
|
|
85
|
-
Chains expose comprehensive execution information:
|
|
86
|
-
|
|
87
|
-
!!! warning "Important"
|
|
88
|
-
|
|
89
|
-
Chain state reflects the first (outermost) task result. Subtasks maintain their own states.
|
|
90
|
-
|
|
91
|
-
```ruby
|
|
92
|
-
result = ImportDataset.execute(dataset_id: 456)
|
|
93
|
-
chain = result.chain
|
|
94
|
-
|
|
95
|
-
# Chain identification
|
|
96
|
-
chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
|
|
97
|
-
chain.results #=> Array of all results in execution order
|
|
98
|
-
|
|
99
|
-
# State delegation (from first/outer-most result)
|
|
100
|
-
chain.state #=> "complete"
|
|
101
|
-
chain.status #=> "success"
|
|
102
|
-
chain.outcome #=> "success"
|
|
103
|
-
|
|
104
|
-
# Access individual results
|
|
105
|
-
chain.results.each_with_index do |result, index|
|
|
106
|
-
puts "#{index}: #{result.task.class} - #{result.status}"
|
|
107
|
-
end
|
|
108
|
-
```
|
data/docs/basics/context.md
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# Basics - Context
|
|
2
|
-
|
|
3
|
-
Context is your data container for inputs, intermediate values, and outputs. It makes sharing data between tasks effortless.
|
|
4
|
-
|
|
5
|
-
## Assigning Data
|
|
6
|
-
|
|
7
|
-
Context automatically captures all task inputs, normalizing keys to symbols:
|
|
8
|
-
|
|
9
|
-
```ruby
|
|
10
|
-
# Direct execution
|
|
11
|
-
CalculateShipping.execute(weight: 2.5, destination: "CA")
|
|
12
|
-
|
|
13
|
-
# Instance creation
|
|
14
|
-
CalculateShipping.new(weight: 2.5, "destination" => "CA")
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
!!! warning "Important"
|
|
18
|
-
|
|
19
|
-
String keys convert to symbols automatically. Prefer symbols for consistency.
|
|
20
|
-
|
|
21
|
-
## Accessing Data
|
|
22
|
-
|
|
23
|
-
Access context data using method notation, hash keys, or safe accessors:
|
|
24
|
-
|
|
25
|
-
```ruby
|
|
26
|
-
class CalculateShipping < CMDx::Task
|
|
27
|
-
def work
|
|
28
|
-
# Method style access (preferred)
|
|
29
|
-
weight = context.weight
|
|
30
|
-
destination = context.destination
|
|
31
|
-
|
|
32
|
-
# Hash style access
|
|
33
|
-
service_type = context[:service_type]
|
|
34
|
-
options = context["options"]
|
|
35
|
-
|
|
36
|
-
# Safe access with defaults
|
|
37
|
-
rush_delivery = context.fetch!(:rush_delivery, false)
|
|
38
|
-
carrier = context.dig(:options, :carrier)
|
|
39
|
-
|
|
40
|
-
# Shorter alias
|
|
41
|
-
cost = ctx.weight * ctx.rate_per_pound # ctx aliases context
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
!!! warning "Important"
|
|
47
|
-
|
|
48
|
-
Undefined attributes return `nil` instead of raising errors—perfect for optional data.
|
|
49
|
-
|
|
50
|
-
## Modifying Context
|
|
51
|
-
|
|
52
|
-
Context supports dynamic modification during task execution:
|
|
53
|
-
|
|
54
|
-
```ruby
|
|
55
|
-
class CalculateShipping < CMDx::Task
|
|
56
|
-
def work
|
|
57
|
-
# Direct assignment
|
|
58
|
-
context.carrier = Carrier.find_by(code: context.carrier_code)
|
|
59
|
-
context.package = Package.new(weight: context.weight)
|
|
60
|
-
context.calculated_at = Time.now
|
|
61
|
-
|
|
62
|
-
# Hash-style assignment
|
|
63
|
-
context[:status] = "calculating"
|
|
64
|
-
context["tracking_number"] = "SHIP#{SecureRandom.hex(6)}"
|
|
65
|
-
|
|
66
|
-
# Conditional assignment
|
|
67
|
-
context.insurance_included ||= false
|
|
68
|
-
|
|
69
|
-
# Batch updates
|
|
70
|
-
context.merge!(
|
|
71
|
-
status: "completed",
|
|
72
|
-
shipping_cost: calculate_cost,
|
|
73
|
-
estimated_delivery: Time.now + 3.days
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
# Remove sensitive data
|
|
77
|
-
context.delete!(:credit_card_token)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
82
|
-
def calculate_cost
|
|
83
|
-
base_rate = context.weight * context.rate_per_pound
|
|
84
|
-
base_rate + (base_rate * context.tax_percentage)
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
!!! tip
|
|
90
|
-
|
|
91
|
-
Use context for both input values and intermediate results. This creates natural data flow through your task execution pipeline.
|
|
92
|
-
|
|
93
|
-
## Data Sharing
|
|
94
|
-
|
|
95
|
-
Share context across tasks for seamless data flow:
|
|
96
|
-
|
|
97
|
-
```ruby
|
|
98
|
-
# During execution
|
|
99
|
-
class CalculateShipping < CMDx::Task
|
|
100
|
-
def work
|
|
101
|
-
# Validate shipping data
|
|
102
|
-
validation_result = ValidateAddress.execute(context)
|
|
103
|
-
|
|
104
|
-
# Via context
|
|
105
|
-
CalculateInsurance.execute(context)
|
|
106
|
-
|
|
107
|
-
# Via result
|
|
108
|
-
NotifyShippingCalculated.execute(validation_result)
|
|
109
|
-
|
|
110
|
-
# Context now contains accumulated data from all tasks
|
|
111
|
-
context.address_validated #=> true (from validation)
|
|
112
|
-
context.insurance_calculated #=> true (from insurance)
|
|
113
|
-
context.notification_sent #=> true (from notification)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# After execution
|
|
118
|
-
result = CalculateShipping.execute(destination: "New York, NY")
|
|
119
|
-
|
|
120
|
-
CreateShippingLabel.execute(result)
|
|
121
|
-
```
|
data/docs/basics/execution.md
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Basics - Execution
|
|
2
|
-
|
|
3
|
-
CMDx offers two execution methods with different error handling approaches. Choose based on your needs: safe result handling or exception-based control flow.
|
|
4
|
-
|
|
5
|
-
## Execution Methods
|
|
6
|
-
|
|
7
|
-
Both methods return results, but handle failures differently:
|
|
8
|
-
|
|
9
|
-
| Method | Returns | Exceptions | Use Case |
|
|
10
|
-
|--------|---------|------------|----------|
|
|
11
|
-
| `execute` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
|
|
12
|
-
| `execute!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` when skipped or failed | Exception-based control flow |
|
|
13
|
-
|
|
14
|
-
```mermaid
|
|
15
|
-
flowchart LR
|
|
16
|
-
subgraph Methods
|
|
17
|
-
E[execute]
|
|
18
|
-
EB[execute!]
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
subgraph Returns [Returns CMDx::Result]
|
|
22
|
-
Success
|
|
23
|
-
Failed
|
|
24
|
-
Skipped
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
subgraph Raises [Raises CMDx::Fault]
|
|
28
|
-
FailFault
|
|
29
|
-
SkipFault
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
E --> Success
|
|
33
|
-
E --> Failed
|
|
34
|
-
E --> Skipped
|
|
35
|
-
|
|
36
|
-
EB --> Success
|
|
37
|
-
EB --> FailFault
|
|
38
|
-
EB --> SkipFault
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Non-bang Execution
|
|
42
|
-
|
|
43
|
-
Always returns a `CMDx::Result`, never raises exceptions. Perfect for most use cases.
|
|
44
|
-
|
|
45
|
-
```ruby
|
|
46
|
-
result = CreateAccount.execute(email: "user@example.com")
|
|
47
|
-
|
|
48
|
-
# Check execution state
|
|
49
|
-
result.success? #=> true/false
|
|
50
|
-
result.failed? #=> true/false
|
|
51
|
-
result.skipped? #=> true/false
|
|
52
|
-
|
|
53
|
-
# Access result data
|
|
54
|
-
result.context.email #=> "user@example.com"
|
|
55
|
-
result.state #=> "complete"
|
|
56
|
-
result.status #=> "success"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Bang Execution
|
|
60
|
-
|
|
61
|
-
Raises `CMDx::Fault` exceptions on failure or skip. Returns results only on success.
|
|
62
|
-
|
|
63
|
-
| Exception | Raised When |
|
|
64
|
-
|-----------|-------------|
|
|
65
|
-
| `CMDx::FailFault` | Task execution fails |
|
|
66
|
-
| `CMDx::SkipFault` | Task execution is skipped |
|
|
67
|
-
|
|
68
|
-
!!! warning "Important"
|
|
69
|
-
|
|
70
|
-
Behavior depends on `task_breakpoints` or `workflow_breakpoints` config. Default: only failures raise exceptions.
|
|
71
|
-
|
|
72
|
-
```ruby
|
|
73
|
-
begin
|
|
74
|
-
result = CreateAccount.execute!(email: "user@example.com")
|
|
75
|
-
SendWelcomeEmail.execute(result.context)
|
|
76
|
-
rescue CMDx::FailFault => e
|
|
77
|
-
ScheduleAccountRetryJob.perform_later(e.result.context.email)
|
|
78
|
-
rescue CMDx::SkipFault => e
|
|
79
|
-
Rails.logger.info("Account creation skipped: #{e.result.reason}")
|
|
80
|
-
rescue Exception => e
|
|
81
|
-
ErrorTracker.capture(unhandled_exception: e)
|
|
82
|
-
end
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Direct Instantiation
|
|
86
|
-
|
|
87
|
-
Tasks can be instantiated directly for advanced use cases, testing, and custom execution patterns:
|
|
88
|
-
|
|
89
|
-
```ruby
|
|
90
|
-
# Direct instantiation
|
|
91
|
-
task = CreateAccount.new(email: "user@example.com", send_welcome: true)
|
|
92
|
-
|
|
93
|
-
# Access properties before execution
|
|
94
|
-
task.id #=> "abc123..." (unique task ID)
|
|
95
|
-
task.context.email #=> "user@example.com"
|
|
96
|
-
task.context.send_welcome #=> true
|
|
97
|
-
task.result.state #=> "initialized"
|
|
98
|
-
task.result.status #=> "success"
|
|
99
|
-
|
|
100
|
-
# Manual execution
|
|
101
|
-
task.execute
|
|
102
|
-
# or
|
|
103
|
-
task.execute!
|
|
104
|
-
|
|
105
|
-
task.result.success? #=> true/false
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Result Details
|
|
109
|
-
|
|
110
|
-
The `Result` object provides comprehensive execution information:
|
|
111
|
-
|
|
112
|
-
```ruby
|
|
113
|
-
result = CreateAccount.execute(email: "user@example.com")
|
|
114
|
-
|
|
115
|
-
# Execution metadata
|
|
116
|
-
result.id #=> "abc123..." (unique execution ID)
|
|
117
|
-
result.task #=> CreateAccount instance (frozen)
|
|
118
|
-
result.chain #=> Task execution chain
|
|
119
|
-
|
|
120
|
-
# Context and metadata
|
|
121
|
-
result.context #=> Context with all task data
|
|
122
|
-
result.metadata #=> Hash with execution metadata
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
## Dry Run
|
|
126
|
-
|
|
127
|
-
Execute tasks in dry-run mode to simulate execution without performing side effects. Pass `dry_run: true` in the context when initializing or executing the task.
|
|
128
|
-
|
|
129
|
-
Inside your task, use the `dry_run?` method to conditionally skip side effects.
|
|
130
|
-
|
|
131
|
-
```ruby
|
|
132
|
-
class CloseStripeCard < CMDx::Task
|
|
133
|
-
def work
|
|
134
|
-
context.stripe_result =
|
|
135
|
-
if dry_run?
|
|
136
|
-
FactoryBot.build(:stripe_closed_card)
|
|
137
|
-
else
|
|
138
|
-
StripeApi.close_card(context.card_id)
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Execute in dry-run mode
|
|
144
|
-
result = CloseStripeCard.execute(card_id: "card_abc123", dry_run: true)
|
|
145
|
-
result.success? # => true
|
|
146
|
-
|
|
147
|
-
# FactoryBot object
|
|
148
|
-
result.context.stripe_result = {
|
|
149
|
-
card_id: "card_abc123",
|
|
150
|
-
status: "closed"
|
|
151
|
-
}
|
|
152
|
-
```
|