cmdx 1.11.0 → 1.13.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/.cursor/rules/cursor-instructions.mdc +8 -0
- data/.yard-lint.yml +174 -0
- data/CHANGELOG.md +24 -0
- data/docs/attributes/definitions.md +5 -1
- data/docs/attributes/validations.md +47 -6
- data/docs/basics/execution.md +56 -0
- data/docs/basics/setup.md +26 -3
- data/docs/deprecation.md +0 -2
- data/docs/getting_started.md +11 -0
- data/docs/logging.md +6 -10
- data/docs/outcomes/result.md +12 -8
- data/docs/outcomes/states.md +4 -4
- data/docs/outcomes/statuses.md +6 -6
- data/docs/tips_and_tricks.md +4 -0
- data/examples/active_record_database_transaction.md +27 -0
- data/examples/flipper_feature_flags.md +50 -0
- data/examples/redis_idempotency.md +71 -0
- data/examples/sentry_error_tracking.md +46 -0
- data/lib/cmdx/attribute.rb +6 -0
- data/lib/cmdx/callback_registry.rb +2 -1
- data/lib/cmdx/chain.rb +20 -7
- data/lib/cmdx/coercion_registry.rb +2 -1
- data/lib/cmdx/coercions/boolean.rb +3 -3
- data/lib/cmdx/coercions/complex.rb +1 -0
- data/lib/cmdx/coercions/string.rb +1 -0
- data/lib/cmdx/coercions/symbol.rb +1 -0
- data/lib/cmdx/configuration.rb +1 -3
- data/lib/cmdx/context.rb +5 -2
- data/lib/cmdx/executor.rb +29 -24
- data/lib/cmdx/middleware_registry.rb +1 -0
- data/lib/cmdx/result.rb +32 -89
- data/lib/cmdx/task.rb +10 -11
- data/lib/cmdx/utils/call.rb +3 -1
- data/lib/cmdx/utils/condition.rb +0 -3
- data/lib/cmdx/utils/format.rb +1 -1
- data/lib/cmdx/validators/exclusion.rb +2 -0
- data/lib/cmdx/validators/inclusion.rb +2 -0
- data/lib/cmdx/validators/length.rb +6 -0
- data/lib/cmdx/validators/numeric.rb +6 -0
- data/lib/cmdx/version.rb +1 -1
- data/lib/generators/cmdx/locale_generator.rb +6 -5
- data/mkdocs.yml +5 -1
- metadata +20 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b1cd7841d40aeac40b6eb36a7ff87a5ace03deae3496e1aef5cb10e34f8f42d
|
|
4
|
+
data.tar.gz: 303333d4635690575ceda40a2564be417b47f5ec4c2992af542c509710289ae3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18cde5cf4b6c7fa974ffb7a3827e09d0a5c2ac5f3c2c01427abbc94de71ef1bc03aee78e25032900761f09c8d39e89e52a4ea1d5fa33e07c86f060b483fe839f
|
|
7
|
+
data.tar.gz: 5ed05ecdb682ae160be47cf6399c76081486011b6143f95f9884a06b588474ba64e3e000efd72ba2dcd6abb343e3e12985d2691ca15ee7575873f90daff6e70f
|
|
@@ -17,6 +17,11 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
|
|
|
17
17
|
- Ruby 3.4+
|
|
18
18
|
- RSpec 3.1+
|
|
19
19
|
|
|
20
|
+
## Development Guidelines
|
|
21
|
+
- Performance is critical - benchmark any changes that could affect speed
|
|
22
|
+
- Follow existing code patterns and conventions
|
|
23
|
+
- Maintain backward compatibility for public API
|
|
24
|
+
|
|
20
25
|
## Code Style and Structure
|
|
21
26
|
- Write concise, idiomatic Ruby code with accurate examples
|
|
22
27
|
- Follow Ruby conventions and best practices
|
|
@@ -24,6 +29,7 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
|
|
|
24
29
|
- Prefer iteration and modularization over code duplication
|
|
25
30
|
- Use descriptive variable and method names (e.g., user_signed_in?, calculate_total)
|
|
26
31
|
- Write comprehensive code documentation using the Yardoc format
|
|
32
|
+
- Minimize object allocations in hot paths
|
|
27
33
|
|
|
28
34
|
## Naming Conventions
|
|
29
35
|
- Use snake_case for file names, method names, and variables
|
|
@@ -35,6 +41,7 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
|
|
|
35
41
|
- Use Ruby's expressive syntax (e.g., unless, ||=, &.)
|
|
36
42
|
- Prefer double quotes for strings
|
|
37
43
|
- Respect my Rubocop options
|
|
44
|
+
- Run `bundle exec rubocop .` before finalizing any code changes
|
|
38
45
|
|
|
39
46
|
## Performance Optimization
|
|
40
47
|
- Use memoization for expensive operations
|
|
@@ -50,6 +57,7 @@ Reference the CMDx documentation in https://github.com/drexed/cmdx/blob/main/LLM
|
|
|
50
57
|
- Don't test declarative configuration
|
|
51
58
|
- Use appropriate matchers
|
|
52
59
|
- Update tests and update Yardocs after you write code
|
|
60
|
+
- Run `bundle rspec .` before finalizing any code changes
|
|
53
61
|
|
|
54
62
|
## Documentation
|
|
55
63
|
- Utilize the YARDoc format when documenting Ruby code
|
data/.yard-lint.yml
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# YARD-Lint Configuration
|
|
2
|
+
# See https://github.com/mensfeld/yard-lint for documentation
|
|
3
|
+
|
|
4
|
+
# Global settings for all validators
|
|
5
|
+
AllValidators:
|
|
6
|
+
# YARD command-line options (applied to all validators by default)
|
|
7
|
+
YardOptions:
|
|
8
|
+
- --private
|
|
9
|
+
- --protected
|
|
10
|
+
|
|
11
|
+
# Global file exclusion patterns
|
|
12
|
+
Exclude:
|
|
13
|
+
- '\.git'
|
|
14
|
+
- 'vendor/**/*'
|
|
15
|
+
- 'node_modules/**/*'
|
|
16
|
+
- 'spec/**/*'
|
|
17
|
+
- 'test/**/*'
|
|
18
|
+
|
|
19
|
+
# Exit code behavior (error, warning, convention, never)
|
|
20
|
+
FailOnSeverity: warning
|
|
21
|
+
|
|
22
|
+
# Minimum documentation coverage percentage (0-100)
|
|
23
|
+
# Fails if coverage is below this threshold
|
|
24
|
+
# MinCoverage: 80.0
|
|
25
|
+
|
|
26
|
+
# Diff mode settings
|
|
27
|
+
DiffMode:
|
|
28
|
+
# Default base ref for --diff (auto-detects main/master if not specified)
|
|
29
|
+
DefaultBaseRef: ~
|
|
30
|
+
|
|
31
|
+
# Documentation validators
|
|
32
|
+
Documentation/UndocumentedObjects:
|
|
33
|
+
Description: 'Checks for classes, modules, and methods without documentation.'
|
|
34
|
+
Enabled: false
|
|
35
|
+
Severity: warning
|
|
36
|
+
ExcludedMethods:
|
|
37
|
+
- 'initialize/0' # Exclude parameter-less initialize
|
|
38
|
+
- '/^_/' # Exclude private methods (by convention)
|
|
39
|
+
|
|
40
|
+
Documentation/UndocumentedMethodArguments:
|
|
41
|
+
Description: 'Checks for method parameters without @param tags.'
|
|
42
|
+
Enabled: true
|
|
43
|
+
Severity: warning
|
|
44
|
+
|
|
45
|
+
Documentation/UndocumentedBooleanMethods:
|
|
46
|
+
Description: 'Checks that question mark methods document their boolean return.'
|
|
47
|
+
Enabled: true
|
|
48
|
+
Severity: warning
|
|
49
|
+
|
|
50
|
+
Documentation/UndocumentedOptions:
|
|
51
|
+
Description: 'Detects methods with options hash parameters but no @option tags.'
|
|
52
|
+
Enabled: true
|
|
53
|
+
Severity: warning
|
|
54
|
+
|
|
55
|
+
Documentation/MarkdownSyntax:
|
|
56
|
+
Description: 'Detects common markdown syntax errors in documentation.'
|
|
57
|
+
Enabled: true
|
|
58
|
+
Severity: warning
|
|
59
|
+
|
|
60
|
+
# Tags validators
|
|
61
|
+
Tags/Order:
|
|
62
|
+
Description: 'Enforces consistent ordering of YARD tags.'
|
|
63
|
+
Enabled: true
|
|
64
|
+
Severity: convention
|
|
65
|
+
EnforcedOrder:
|
|
66
|
+
- param
|
|
67
|
+
- option
|
|
68
|
+
- return
|
|
69
|
+
- raise
|
|
70
|
+
- example
|
|
71
|
+
|
|
72
|
+
Tags/InvalidTypes:
|
|
73
|
+
Description: 'Validates type definitions in @param, @return, @option tags.'
|
|
74
|
+
Enabled: true
|
|
75
|
+
Severity: warning
|
|
76
|
+
ValidatedTags:
|
|
77
|
+
- param
|
|
78
|
+
- option
|
|
79
|
+
- return
|
|
80
|
+
|
|
81
|
+
Tags/TypeSyntax:
|
|
82
|
+
Description: 'Validates YARD type syntax using YARD parser.'
|
|
83
|
+
Enabled: true
|
|
84
|
+
Severity: warning
|
|
85
|
+
ValidatedTags:
|
|
86
|
+
- param
|
|
87
|
+
- option
|
|
88
|
+
- return
|
|
89
|
+
- yieldreturn
|
|
90
|
+
|
|
91
|
+
Tags/MeaninglessTag:
|
|
92
|
+
Description: 'Detects @param/@option tags on classes, modules, or constants.'
|
|
93
|
+
Enabled: true
|
|
94
|
+
Severity: warning
|
|
95
|
+
CheckedTags:
|
|
96
|
+
- param
|
|
97
|
+
- option
|
|
98
|
+
InvalidObjectTypes:
|
|
99
|
+
- class
|
|
100
|
+
- module
|
|
101
|
+
- constant
|
|
102
|
+
|
|
103
|
+
Tags/CollectionType:
|
|
104
|
+
Description: 'Validates Hash collection syntax consistency.'
|
|
105
|
+
Enabled: true
|
|
106
|
+
Severity: convention
|
|
107
|
+
EnforcedStyle: long # 'long' for Hash{K => V} (YARD standard), 'short' for {K => V}
|
|
108
|
+
ValidatedTags:
|
|
109
|
+
- param
|
|
110
|
+
- option
|
|
111
|
+
- return
|
|
112
|
+
- yieldreturn
|
|
113
|
+
|
|
114
|
+
Tags/TagTypePosition:
|
|
115
|
+
Description: 'Validates type annotation position in tags.'
|
|
116
|
+
Enabled: true
|
|
117
|
+
Severity: convention
|
|
118
|
+
CheckedTags:
|
|
119
|
+
- param
|
|
120
|
+
- option
|
|
121
|
+
# EnforcedStyle: 'type_after_name' (YARD standard: @param name [Type])
|
|
122
|
+
# or 'type_first' (@param [Type] name)
|
|
123
|
+
EnforcedStyle: type_after_name
|
|
124
|
+
|
|
125
|
+
Tags/ApiTags:
|
|
126
|
+
Description: 'Enforces @api tags on public objects.'
|
|
127
|
+
Enabled: false # Opt-in validator
|
|
128
|
+
Severity: warning
|
|
129
|
+
AllowedApis:
|
|
130
|
+
- public
|
|
131
|
+
- private
|
|
132
|
+
- internal
|
|
133
|
+
|
|
134
|
+
Tags/OptionTags:
|
|
135
|
+
Description: 'Requires @option tags for methods with options parameters.'
|
|
136
|
+
Enabled: true
|
|
137
|
+
Severity: warning
|
|
138
|
+
|
|
139
|
+
# Warnings validators - catches YARD parser errors
|
|
140
|
+
Warnings/UnknownTag:
|
|
141
|
+
Description: 'Detects unknown YARD tags.'
|
|
142
|
+
Enabled: false
|
|
143
|
+
Severity: error
|
|
144
|
+
|
|
145
|
+
Warnings/UnknownDirective:
|
|
146
|
+
Description: 'Detects unknown YARD directives.'
|
|
147
|
+
Enabled: true
|
|
148
|
+
Severity: error
|
|
149
|
+
|
|
150
|
+
Warnings/InvalidTagFormat:
|
|
151
|
+
Description: 'Detects malformed tag syntax.'
|
|
152
|
+
Enabled: true
|
|
153
|
+
Severity: error
|
|
154
|
+
|
|
155
|
+
Warnings/InvalidDirectiveFormat:
|
|
156
|
+
Description: 'Detects malformed directive syntax.'
|
|
157
|
+
Enabled: true
|
|
158
|
+
Severity: error
|
|
159
|
+
|
|
160
|
+
Warnings/DuplicatedParameterName:
|
|
161
|
+
Description: 'Detects duplicate @param tags.'
|
|
162
|
+
Enabled: true
|
|
163
|
+
Severity: error
|
|
164
|
+
|
|
165
|
+
Warnings/UnknownParameterName:
|
|
166
|
+
Description: 'Detects @param tags for non-existent parameters.'
|
|
167
|
+
Enabled: true
|
|
168
|
+
Severity: error
|
|
169
|
+
|
|
170
|
+
# Semantic validators
|
|
171
|
+
Semantic/AbstractMethods:
|
|
172
|
+
Description: 'Ensures @abstract methods do not have real implementations.'
|
|
173
|
+
Enabled: true
|
|
174
|
+
Severity: warning
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [UNRELEASED]
|
|
8
8
|
|
|
9
|
+
## [1.13.0] - 2025-12-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added task execution rollback tracking and logging
|
|
14
|
+
- Added `dry_run` option to task execution with inheritance support for nested tasks
|
|
15
|
+
- Added context `delete` alias for `delete!`
|
|
16
|
+
- Added context `merge` alias for `merge!`
|
|
17
|
+
|
|
18
|
+
## [1.12.0] - 2025-12-18
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Added active record database transaction example
|
|
22
|
+
- Added Sentry error tracking example
|
|
23
|
+
- Added Redis idempotency example
|
|
24
|
+
- Added Flipper feature flag example
|
|
25
|
+
|
|
26
|
+
### Updated
|
|
27
|
+
- Remove `handle_*` methods and provide `on(*states_or_statuses)` method for more flexibility
|
|
28
|
+
- Optimize logging ancestor lookup
|
|
29
|
+
- Use chop instead of range for better string performance
|
|
30
|
+
- Update boolean coercion `TRUTHY` and `FALSEY` regexp to be case insensitive
|
|
31
|
+
- Improve YARD documentation with `yard-lint`
|
|
32
|
+
|
|
9
33
|
## [1.11.0] - 2025-11-08
|
|
10
34
|
|
|
11
35
|
### Updated
|
|
@@ -4,6 +4,10 @@ Attributes define your task's interface with automatic validation, type coercion
|
|
|
4
4
|
|
|
5
5
|
## Declarations
|
|
6
6
|
|
|
7
|
+
!!! warning "Important"
|
|
8
|
+
|
|
9
|
+
Attributes are order-dependent, so if you need to reference them as a source or use them in conditions, make sure they’re defined in the correct order.
|
|
10
|
+
|
|
7
11
|
!!! tip
|
|
8
12
|
|
|
9
13
|
Prefer using the `required` and `optional` alias for `attributes` for brevity and to clearly signal intent.
|
|
@@ -54,7 +58,7 @@ class PublishArticle < CMDx::Task
|
|
|
54
58
|
|
|
55
59
|
# Conditionally required
|
|
56
60
|
required :publisher, if: :magazine?
|
|
57
|
-
|
|
61
|
+
attribute :approver, required: true, unless: proc { status == :published }
|
|
58
62
|
|
|
59
63
|
def work
|
|
60
64
|
title #=> "Getting Started with Ruby"
|
|
@@ -14,10 +14,10 @@ class ProcessSubscription < CMDx::Task
|
|
|
14
14
|
attribute :user_id, presence: true
|
|
15
15
|
|
|
16
16
|
# String with length constraints
|
|
17
|
-
|
|
17
|
+
optional :preferences, length: { minimum: 10, maximum: 500 }
|
|
18
18
|
|
|
19
19
|
# Numeric range validation
|
|
20
|
-
|
|
20
|
+
required :tier_level, inclusion: { in: 1..5 }
|
|
21
21
|
|
|
22
22
|
# Format validation for email
|
|
23
23
|
attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
|
|
@@ -46,6 +46,42 @@ ProcessSubscription.execute(
|
|
|
46
46
|
|
|
47
47
|
### Common Options
|
|
48
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
|
+
|
|
49
85
|
This list of options is available to all validators:
|
|
50
86
|
|
|
51
87
|
| Option | Description |
|
|
@@ -261,10 +297,15 @@ Validation failures provide detailed, structured error messages:
|
|
|
261
297
|
|
|
262
298
|
```ruby
|
|
263
299
|
class CreateProject < CMDx::Task
|
|
264
|
-
attribute :project_name,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
268
309
|
|
|
269
310
|
def work
|
|
270
311
|
# Your logic here...
|
data/docs/basics/execution.md
CHANGED
|
@@ -11,6 +11,33 @@ Both methods return results, but handle failures differently:
|
|
|
11
11
|
| `execute` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
|
|
12
12
|
| `execute!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` when skipped or failed | Exception-based control flow |
|
|
13
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
|
+
|
|
14
41
|
## Non-bang Execution
|
|
15
42
|
|
|
16
43
|
Always returns a `CMDx::Result`, never raises exceptions. Perfect for most use cases.
|
|
@@ -94,3 +121,32 @@ result.chain #=> Task execution chain
|
|
|
94
121
|
result.context #=> Context with all task data
|
|
95
122
|
result.metadata #=> Hash with execution metadata
|
|
96
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
|
+
```
|
data/docs/basics/setup.md
CHANGED
|
@@ -29,13 +29,14 @@ IncompleteTask.execute #=> raises CMDx::UndefinedMethodError
|
|
|
29
29
|
Undo any operations linked to the given status, helping to restore a pristine state.
|
|
30
30
|
|
|
31
31
|
```ruby
|
|
32
|
-
class
|
|
32
|
+
class ChargeCard < CMDx::Task
|
|
33
33
|
def work
|
|
34
|
-
# Your logic here
|
|
34
|
+
# Your logic here, ex: charge $100
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# Called automatically if a later step in the workflow fails
|
|
37
38
|
def rollback
|
|
38
|
-
# Your undo logic
|
|
39
|
+
# Your undo logic, ex: void $100 charge
|
|
39
40
|
end
|
|
40
41
|
end
|
|
41
42
|
```
|
|
@@ -70,6 +71,28 @@ end
|
|
|
70
71
|
|
|
71
72
|
Tasks follow a predictable execution pattern:
|
|
72
73
|
|
|
74
|
+
```mermaid
|
|
75
|
+
stateDiagram-v2
|
|
76
|
+
Initialized: Instantiation
|
|
77
|
+
Initialized --> Validating: execute
|
|
78
|
+
Validating --> Executing: Valid?
|
|
79
|
+
Validating --> Failed: Invalid
|
|
80
|
+
Executing --> Success: Work done
|
|
81
|
+
Executing --> Skipped: skip!
|
|
82
|
+
Executing --> Failed: fail! / Exception
|
|
83
|
+
Executed
|
|
84
|
+
|
|
85
|
+
state Executed {
|
|
86
|
+
Success
|
|
87
|
+
Skipped
|
|
88
|
+
Failed
|
|
89
|
+
Rollback
|
|
90
|
+
|
|
91
|
+
Skipped --> Rollback
|
|
92
|
+
Failed --> Rollback
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
73
96
|
!!! danger "Caution"
|
|
74
97
|
|
|
75
98
|
Tasks are single-use objects. Once executed, they're frozen and immutable.
|
data/docs/deprecation.md
CHANGED
data/docs/getting_started.md
CHANGED
|
@@ -43,6 +43,13 @@ If not using Rails, manually copy the [configuration file](https://github.com/dr
|
|
|
43
43
|
|
|
44
44
|
CMDx embraces the Compose, Execute, React, Observe (CERO, pronounced "zero") pattern—a simple yet powerful approach to building reliable business logic.
|
|
45
45
|
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart LR
|
|
48
|
+
Compose --> Execute
|
|
49
|
+
Execute --> React
|
|
50
|
+
Execute -.-> Observe
|
|
51
|
+
```
|
|
52
|
+
|
|
46
53
|
### Compose
|
|
47
54
|
|
|
48
55
|
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.
|
|
@@ -93,6 +100,10 @@ I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
|
|
|
93
100
|
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
|
|
94
101
|
```
|
|
95
102
|
|
|
103
|
+
!!! note
|
|
104
|
+
|
|
105
|
+
This represents a log-only event-sourcing approach, enabling full traceability and a complete, time-ordered view of system behavior.
|
|
106
|
+
|
|
96
107
|
## Task Generator
|
|
97
108
|
|
|
98
109
|
Generate new CMDx tasks quickly using the built-in generator:
|
data/docs/logging.md
CHANGED
|
@@ -18,20 +18,16 @@ Sample output:
|
|
|
18
18
|
|
|
19
19
|
```log
|
|
20
20
|
<!-- Success (INFO level) -->
|
|
21
|
-
I, [
|
|
22
|
-
index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
|
|
21
|
+
I, [2025-12-23T17:04:07.292614Z #20108] INFO -- cmdx: {index: 1, chain_id: "019b4c2b-087b-79be-8ef2-96c11b659df5", type: "Task", tags: [], class: "GenerateInvoice", dry_run: false, id: "019b4c2b-0878-704d-ba0b-daa5410123ec", state: "complete", status: "success", outcome: "success", metadata: {runtime: 187}}
|
|
23
22
|
|
|
24
|
-
<!-- Skipped (
|
|
25
|
-
|
|
26
|
-
index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
|
|
23
|
+
<!-- Skipped (INFO level) -->
|
|
24
|
+
I, [2025-12-23T17:04:11.496881Z #20139] INFO -- cmdx: {index: 2, chain_id: "019b4c2b-18e8-7af6-a38b-63b042c4fbed", type: "Task", tags: [], class: "ValidateCustomer", dry_run: false, id: "019b4c2b-18e5-7230-af7e-5b4a4bd7cda2", state: "interrupted", status: "skipped", outcome: "skipped", metadata: {}, reason: "Customer already validated", cause: #<CMDx::SkipFault: Customer already validated>, rolled_back: false}
|
|
27
25
|
|
|
28
|
-
<!-- Failed (
|
|
29
|
-
|
|
30
|
-
index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
|
|
26
|
+
<!-- Failed (INFO level) -->
|
|
27
|
+
I, [2025-12-23T17:04:15.875306Z #20173] INFO -- cmdx: {index: 3, chain_id: "019b4c2b-2a02-7dbc-b713-b20a7379704f", type: "Task", tags: [], class: "CalculateTax", dry_run: false, id: "019b4c2b-2a00-70b7-9fab-2f14db9139ef", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}
|
|
31
28
|
|
|
32
29
|
<!-- Failed Chain -->
|
|
33
|
-
|
|
34
|
-
index=3 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="BillingWorkflow" state="interrupted" status="failed" caused_failure={index: 2, class: "CalculateTax", status: "failed"} threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
|
|
30
|
+
I, [2025-12-23T17:04:20.972539Z #20209] INFO -- cmdx: {index: 0, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Workflow", tags: [], class: "BillingWorkflow", dry_run: false, id: "019b4c2b-3de6-70b9-9c16-5be13b1a463c", state: "interrupted", status: "failed", outcome: "interrupted", metadata: {}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false, threw_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}, caused_failure: {index: 3, chain_id: "019b4c2b-3de9-71f7-bcc3-2a98836bcfd7", type: "Task", tags: [], class: "CalculateTax", id: "019b4c2b-3dec-70b3-969b-c5b7896e3b27", state: "interrupted", status: "failed", outcome: "failed", metadata: {error_code: "TAX_SERVICE_UNAVAILABLE"}, reason: "Validation failed", cause: #<CMDx::FailFault: Validation failed>, rolled_back: false}}
|
|
35
31
|
```
|
|
36
32
|
|
|
37
33
|
!!! tip
|
data/docs/outcomes/result.md
CHANGED
|
@@ -30,7 +30,7 @@ result.metadata #=> { error_code: "BUILD_TOOL.NOT_FOUND" }
|
|
|
30
30
|
|
|
31
31
|
## Lifecycle Information
|
|
32
32
|
|
|
33
|
-
Check execution state and
|
|
33
|
+
Check execution state, status, and rollback with predicate methods:
|
|
34
34
|
|
|
35
35
|
```ruby
|
|
36
36
|
result = BuildApplication.execute(version: "1.2.3")
|
|
@@ -48,6 +48,9 @@ result.skipped? #=> false (not skipped)
|
|
|
48
48
|
# Outcome categorization
|
|
49
49
|
result.good? #=> true (success or skipped)
|
|
50
50
|
result.bad? #=> false (skipped or failed)
|
|
51
|
+
|
|
52
|
+
# Rollback Status
|
|
53
|
+
result.rolled_back? #=> true (execution was rolled back)
|
|
51
54
|
```
|
|
52
55
|
|
|
53
56
|
## Outcome Analysis
|
|
@@ -126,19 +129,20 @@ result = BuildApplication.execute(version: "1.2.3")
|
|
|
126
129
|
|
|
127
130
|
# Status-based handlers
|
|
128
131
|
result
|
|
129
|
-
.
|
|
130
|
-
.
|
|
131
|
-
.
|
|
132
|
+
.on(:success) { |result| notify_deployment_ready(result) }
|
|
133
|
+
.on(:failed) { |result| handle_build_failure(result) }
|
|
134
|
+
.on(:skipped) { |result| log_skip_reason(result) }
|
|
132
135
|
|
|
133
136
|
# State-based handlers
|
|
134
137
|
result
|
|
135
|
-
.
|
|
136
|
-
.
|
|
138
|
+
.on(:complete) { |result| update_build_status(result) }
|
|
139
|
+
.on(:interrupted) { |result| cleanup_partial_artifacts(result) }
|
|
140
|
+
.on(:executed) { |result| alert_operations_team(result) } #=> .on(:complete, :interrupted)
|
|
137
141
|
|
|
138
142
|
# Outcome-based handlers
|
|
139
143
|
result
|
|
140
|
-
.
|
|
141
|
-
.
|
|
144
|
+
.on(:good) { |result| increment_success_counter(result) } #=> .on(:success, :skipped)
|
|
145
|
+
.on(:bad) { |result| alert_operations_team(result) } #=> .on(:failed, :skipped)
|
|
142
146
|
```
|
|
143
147
|
|
|
144
148
|
## Pattern Matching
|
data/docs/outcomes/states.md
CHANGED
|
@@ -53,14 +53,14 @@ result.executed? #=> true (complete OR interrupted)
|
|
|
53
53
|
|
|
54
54
|
## Handlers
|
|
55
55
|
|
|
56
|
-
Handle lifecycle events with state-based handlers. Use `
|
|
56
|
+
Handle lifecycle events with state-based handlers. Use `on(:executed)` for cleanup that runs regardless of outcome:
|
|
57
57
|
|
|
58
58
|
```ruby
|
|
59
59
|
result = ProcessVideoUpload.execute
|
|
60
60
|
|
|
61
61
|
# Individual state handlers
|
|
62
62
|
result
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
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
66
|
```
|
data/docs/outcomes/statuses.md
CHANGED
|
@@ -47,19 +47,19 @@ result.bad? #=> true if skipped OR failed (not success)
|
|
|
47
47
|
|
|
48
48
|
## Handlers
|
|
49
49
|
|
|
50
|
-
Branch business logic with status-based handlers. Use `
|
|
50
|
+
Branch business logic with status-based handlers. Use `on(:good)` and `on(:bad)` for success/skip vs failed outcomes:
|
|
51
51
|
|
|
52
52
|
```ruby
|
|
53
53
|
result = ProcessNotification.execute
|
|
54
54
|
|
|
55
55
|
# Individual status handlers
|
|
56
56
|
result
|
|
57
|
-
.
|
|
58
|
-
.
|
|
59
|
-
.
|
|
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
60
|
|
|
61
61
|
# Outcome-based handlers
|
|
62
62
|
result
|
|
63
|
-
.
|
|
64
|
-
.
|
|
63
|
+
.on(:good) { |result| update_message_stats(result) } #=> .on(:success, :skipped)
|
|
64
|
+
.on(:bad) { |result| track_delivery_failure(result) } #=> .on(:failed, :skipped)
|
|
65
65
|
```
|
data/docs/tips_and_tricks.md
CHANGED
|
@@ -147,7 +147,11 @@ end
|
|
|
147
147
|
|
|
148
148
|
## Useful Examples
|
|
149
149
|
|
|
150
|
+
- [Active Record Database Transaction](https://github.com/drexed/cmdx/blob/main/examples/active_record_database_transaction.md)
|
|
150
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)
|
|
151
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)
|
|
152
156
|
- [Sidekiq Async Execution](https://github.com/drexed/cmdx/blob/main/examples/sidekiq_async_execution.md)
|
|
153
157
|
- [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
```
|