cmdx 1.12.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 +88 -71
- data/LICENSE.txt +3 -20
- data/README.md +8 -7
- data/lib/cmdx/attribute.rb +21 -5
- data/lib/cmdx/chain.rb +18 -4
- data/lib/cmdx/context.rb +18 -0
- data/lib/cmdx/executor.rb +35 -30
- data/lib/cmdx/result.rb +45 -2
- data/lib/cmdx/task.rb +22 -1
- data/lib/cmdx/version.rb +1 -1
- data/mkdocs.yml +67 -37
- 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 -96
- data/docs/basics/setup.md +0 -84
- data/docs/callbacks.md +0 -157
- data/docs/configuration.md +0 -314
- data/docs/deprecation.md +0 -145
- data/docs/getting_started.md +0 -126
- 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 -94
- data/docs/middlewares.md +0 -191
- data/docs/outcomes/result.md +0 -194
- 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/attributes/naming.md
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# Attributes - Naming
|
|
2
|
-
|
|
3
|
-
Customize accessor method names to avoid conflicts and improve clarity. Affixing changes only the generated methods—not the original attribute names.
|
|
4
|
-
|
|
5
|
-
!!! note
|
|
6
|
-
|
|
7
|
-
Use naming when attributes conflict with existing methods or need better clarity in your code.
|
|
8
|
-
|
|
9
|
-
## Prefix
|
|
10
|
-
|
|
11
|
-
Adds a prefix to the generated accessor method name.
|
|
12
|
-
|
|
13
|
-
```ruby
|
|
14
|
-
class GenerateReport < CMDx::Task
|
|
15
|
-
# Dynamic from attribute source
|
|
16
|
-
attribute :template, prefix: true
|
|
17
|
-
|
|
18
|
-
# Static
|
|
19
|
-
attribute :format, prefix: "report_"
|
|
20
|
-
|
|
21
|
-
def work
|
|
22
|
-
context_template #=> "monthly_sales"
|
|
23
|
-
report_format #=> "pdf"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Attributes passed as original attribute names
|
|
28
|
-
GenerateReport.execute(template: "monthly_sales", format: "pdf")
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Suffix
|
|
32
|
-
|
|
33
|
-
Adds a suffix to the generated accessor method name.
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
class DeployApplication < CMDx::Task
|
|
37
|
-
# Dynamic from attribute source
|
|
38
|
-
attribute :branch, suffix: true
|
|
39
|
-
|
|
40
|
-
# Static
|
|
41
|
-
attribute :version, suffix: "_tag"
|
|
42
|
-
|
|
43
|
-
def work
|
|
44
|
-
branch_context #=> "main"
|
|
45
|
-
version_tag #=> "v1.2.3"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Attributes passed as original attribute names
|
|
50
|
-
DeployApplication.execute(branch: "main", version: "v1.2.3")
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## As
|
|
54
|
-
|
|
55
|
-
Completely renames the generated accessor method.
|
|
56
|
-
|
|
57
|
-
```ruby
|
|
58
|
-
class ScheduleMaintenance < CMDx::Task
|
|
59
|
-
attribute :scheduled_at, as: :when
|
|
60
|
-
|
|
61
|
-
def work
|
|
62
|
-
when #=> <DateTime>
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
# Attributes passed as original attribute names
|
|
67
|
-
ScheduleMaintenance.execute(scheduled_at: DateTime.new(2024, 12, 15, 2, 0, 0))
|
|
68
|
-
```
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# Attributes - Transformations
|
|
2
|
-
|
|
3
|
-
Modify attribute values after coercion but before validation. Perfect for normalization, formatting, and data cleanup.
|
|
4
|
-
|
|
5
|
-
## Declarations
|
|
6
|
-
|
|
7
|
-
### Symbol References
|
|
8
|
-
|
|
9
|
-
Reference instance methods by symbol for dynamic value transformations:
|
|
10
|
-
|
|
11
|
-
```ruby
|
|
12
|
-
class ProcessAnalytics < CMDx::Task
|
|
13
|
-
attribute :options, transform: :compact_blank
|
|
14
|
-
end
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
### Proc or Lambda
|
|
18
|
-
|
|
19
|
-
Use anonymous functions for dynamic value transformations:
|
|
20
|
-
|
|
21
|
-
```ruby
|
|
22
|
-
class CacheContent < CMDx::Task
|
|
23
|
-
# Proc
|
|
24
|
-
attribute :expire_hours, transform: proc { |v| v * 2 }
|
|
25
|
-
|
|
26
|
-
# Lambda
|
|
27
|
-
attribute :compression, transform: ->(v) { v.to_s.upcase.strip[0..2] }
|
|
28
|
-
end
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
### Class or Module
|
|
32
|
-
|
|
33
|
-
Use any object that responds to `call` for reusable transformation logic:
|
|
34
|
-
|
|
35
|
-
```ruby
|
|
36
|
-
class EmailNormalizer
|
|
37
|
-
def call(value)
|
|
38
|
-
value.to_s.downcase.strip
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
class ProcessContacts < CMDx::Task
|
|
43
|
-
# Class or Module
|
|
44
|
-
attribute :email, transform: EmailNormalizer
|
|
45
|
-
|
|
46
|
-
# Instance
|
|
47
|
-
attribute :email, transform: EmailNormalizer.new
|
|
48
|
-
end
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Validations
|
|
52
|
-
|
|
53
|
-
Validations run on transformed values, ensuring data consistency:
|
|
54
|
-
|
|
55
|
-
```ruby
|
|
56
|
-
class ScheduleBackup < CMDx::Task
|
|
57
|
-
# Coercions
|
|
58
|
-
attribute :retention_days, type: :integer, transform: proc { |v| v.clamp(1, 5) }
|
|
59
|
-
|
|
60
|
-
# Validations
|
|
61
|
-
optional :frequency, transform: :downcase, inclusion: { in: %w[hourly daily weekly monthly] }
|
|
62
|
-
end
|
|
63
|
-
```
|
|
@@ -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
|
-
```
|