cmdx 1.11.0 → 1.12.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 +15 -0
- data/docs/attributes/definitions.md +5 -1
- data/docs/attributes/validations.md +47 -6
- data/docs/outcomes/result.md +8 -7
- 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 +2 -3
- 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 +3 -2
- data/lib/cmdx/middleware_registry.rb +1 -0
- data/lib/cmdx/result.rb +8 -88
- data/lib/cmdx/task.rb +7 -10
- 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
- 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: 5dffef0df3a8abb190fc0ca6a194383614fdd81f3eebf22ecab59d8ec047d1d7
|
|
4
|
+
data.tar.gz: 0f38ccaa12a07c41b64fb4fd3f0685bafb45b7de0b3830243f85a53de4d331c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8649dadb0908d88f7441b80d6e3dce03e7f0d20095216d8ac24cc0bf8b1446f57b4552f021a324f47d860112e431b531695a5cd78e84a4b2cf0c3be0dff1224e
|
|
7
|
+
data.tar.gz: 5e88be6d5a3070d75abbbcb20e25d900db3a44f011f71ed55c61f748cda77e9be4297f12829ab9f28daac66a4b2c511725318ae4e1455793eef8b8f806c3864c
|
|
@@ -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,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [UNRELEASED]
|
|
8
8
|
|
|
9
|
+
## [1.12.0] - 2025-12-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Added active record database transaction example
|
|
13
|
+
- Added Sentry error tracking example
|
|
14
|
+
- Added Redis idempotency example
|
|
15
|
+
- Added Flipper feature flag example
|
|
16
|
+
|
|
17
|
+
### Updated
|
|
18
|
+
- Remove `handle_*` methods and provide `on(*states_or_statuses)` method for more flexibility
|
|
19
|
+
- Optimize logging ancestor lookup
|
|
20
|
+
- Use chop instead of range for better string performance
|
|
21
|
+
- Update boolean coercion `TRUTHY` and `FALSEY` regexp to be case insensitive
|
|
22
|
+
- Improve YARD documentation with `yard-lint`
|
|
23
|
+
|
|
9
24
|
## [1.11.0] - 2025-11-08
|
|
10
25
|
|
|
11
26
|
### 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/outcomes/result.md
CHANGED
|
@@ -126,19 +126,20 @@ result = BuildApplication.execute(version: "1.2.3")
|
|
|
126
126
|
|
|
127
127
|
# Status-based handlers
|
|
128
128
|
result
|
|
129
|
-
.
|
|
130
|
-
.
|
|
131
|
-
.
|
|
129
|
+
.on(:success) { |result| notify_deployment_ready(result) }
|
|
130
|
+
.on(:failed) { |result| handle_build_failure(result) }
|
|
131
|
+
.on(:skipped) { |result| log_skip_reason(result) }
|
|
132
132
|
|
|
133
133
|
# State-based handlers
|
|
134
134
|
result
|
|
135
|
-
.
|
|
136
|
-
.
|
|
135
|
+
.on(:complete) { |result| update_build_status(result) }
|
|
136
|
+
.on(:interrupted) { |result| cleanup_partial_artifacts(result) }
|
|
137
|
+
.on(:executed) { |result| alert_operations_team(result) } #=> .on(:complete, :interrupted)
|
|
137
138
|
|
|
138
139
|
# Outcome-based handlers
|
|
139
140
|
result
|
|
140
|
-
.
|
|
141
|
-
.
|
|
141
|
+
.on(:good) { |result| increment_success_counter(result) } #=> .on(:success, :skipped)
|
|
142
|
+
.on(:bad) { |result| alert_operations_team(result) } #=> .on(:failed, :skipped)
|
|
142
143
|
```
|
|
143
144
|
|
|
144
145
|
## 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
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Flipper Feature Flags
|
|
2
|
+
|
|
3
|
+
Control task execution based on Flipper feature flags.
|
|
4
|
+
|
|
5
|
+
<https://github.com/flippercloud/flipper>
|
|
6
|
+
|
|
7
|
+
### Setup
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# lib/cmdx_flipper_middleware.rb
|
|
11
|
+
class CmdxFlipperMiddleware
|
|
12
|
+
def self.call(task, **options, &)
|
|
13
|
+
feature_name = options.fetch(:feature)
|
|
14
|
+
actor = options.fetch(:actor, -> { task.context[:user] })
|
|
15
|
+
|
|
16
|
+
# Resolve actor if it's a proc
|
|
17
|
+
actor = actor.call if actor.respond_to?(:call)
|
|
18
|
+
|
|
19
|
+
if Flipper.enabled?(feature_name, actor)
|
|
20
|
+
yield
|
|
21
|
+
else
|
|
22
|
+
# Option 1: Skip the task
|
|
23
|
+
task.skip!("Feature #{feature_name} is disabled")
|
|
24
|
+
|
|
25
|
+
# Option 2: Fail the task
|
|
26
|
+
# task.fail!("Feature #{feature_name} is disabled")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Usage
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
class NewFeatureTask < CMDx::Task
|
|
36
|
+
# Execute only if :new_feature is enabled for the user in context
|
|
37
|
+
register :middleware, CmdxFlipperMiddleware,
|
|
38
|
+
feature: :new_feature
|
|
39
|
+
|
|
40
|
+
# Customize the actor resolution
|
|
41
|
+
register :middleware, CmdxFlipperMiddleware,
|
|
42
|
+
feature: :beta_access,
|
|
43
|
+
actor: -> { task.context[:company] }
|
|
44
|
+
|
|
45
|
+
def work
|
|
46
|
+
# ...
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
```
|
|
50
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Redis Idempotency
|
|
2
|
+
|
|
3
|
+
Ensure tasks are executed exactly once using Redis to store execution state. This is critical for non-idempotent operations like charging a credit card or sending an email.
|
|
4
|
+
|
|
5
|
+
### Setup
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
# lib/cmdx_redis_idempotency_middleware.rb
|
|
9
|
+
class CmdxRedisIdempotencyMiddleware
|
|
10
|
+
def self.call(task, **options, &block)
|
|
11
|
+
key = generate_key(task, options[:key])
|
|
12
|
+
ttl = options[:ttl] || 5.minutes.to_i
|
|
13
|
+
|
|
14
|
+
# Attempt to lock the key
|
|
15
|
+
if Redis.current.set(key, "processing", nx: true, ex: ttl)
|
|
16
|
+
begin
|
|
17
|
+
block.call.tap |result|
|
|
18
|
+
Redis.current.set(key, result.status, xx: true, ex: ttl)
|
|
19
|
+
end
|
|
20
|
+
rescue => e
|
|
21
|
+
Redis.current.del(key)
|
|
22
|
+
raise(e)
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
# Key exists, handle duplicate
|
|
26
|
+
status = Redis.current.get(key)
|
|
27
|
+
|
|
28
|
+
if status == "processing"
|
|
29
|
+
task.result.tap { |r| r.skip!("Duplicate request: currently processing", halt: true) }
|
|
30
|
+
else
|
|
31
|
+
task.result.tap { |r| r.skip!("Duplicate request: already processed (#{status})", halt: true) }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.generate_key(task, key_gen)
|
|
37
|
+
id = if key_gen.respond_to?(:call)
|
|
38
|
+
key_gen.call(task)
|
|
39
|
+
elsif key_gen.is_a?(Symbol)
|
|
40
|
+
task.send(key_gen)
|
|
41
|
+
else
|
|
42
|
+
task.context[:idempotency_key]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
"cmdx:idempotency:#{task.class.name}:#{id}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Usage
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class ChargeCustomer < CMDx::Task
|
|
54
|
+
# Use context[:payment_id] as the unique key
|
|
55
|
+
register :middleware, CmdxIdempotencyMiddleware,
|
|
56
|
+
key: ->(t) { t.context[:payment_id] }
|
|
57
|
+
|
|
58
|
+
def work
|
|
59
|
+
# Charge logic...
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# First run: Executes
|
|
64
|
+
ChargeCustomer.call(payment_id: "123")
|
|
65
|
+
# => Success
|
|
66
|
+
|
|
67
|
+
# Second run: Skips
|
|
68
|
+
ChargeCustomer.call(payment_id: "123")
|
|
69
|
+
# => Skipped (reason: "Duplicate request: already processed (success)")
|
|
70
|
+
```
|
|
71
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Sentry Error Tracking
|
|
2
|
+
|
|
3
|
+
Report unhandled exceptions and unexpected task failures to Sentry with detailed context.
|
|
4
|
+
|
|
5
|
+
<https://github.com/getsentry/sentry-ruby>
|
|
6
|
+
|
|
7
|
+
### Setup
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# lib/cmdx_sentry_middleware.rb
|
|
11
|
+
class CmdxSentryMiddleware
|
|
12
|
+
def self.call(task, **options, &)
|
|
13
|
+
Sentry.with_scope do |scope|
|
|
14
|
+
scope.set_tags(task: task.class.name)
|
|
15
|
+
scope.set_context(:user, Current.user.sentry_attributes)
|
|
16
|
+
|
|
17
|
+
yield.tap do |result|
|
|
18
|
+
# Optional: Report logical failures if needed
|
|
19
|
+
if Array(options[:report_on]).include?(result.status)
|
|
20
|
+
Sentry.capture_message("Task #{result.status}: #{result.reason}", level: :warning)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
rescue => e
|
|
25
|
+
Sentry.capture_exception(e)
|
|
26
|
+
raise(e) # Re-raise to let the task handle the error or bubble up
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Usage
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
class ProcessPayment < CMDx::Task
|
|
35
|
+
# Report exceptions only
|
|
36
|
+
register :middleware, CmdxSentryMiddleware
|
|
37
|
+
|
|
38
|
+
# Report exceptions AND logical failures (result.failure?)
|
|
39
|
+
register :middleware, CmdxSentryMiddleware, report_on: %w[failed skipped]
|
|
40
|
+
|
|
41
|
+
def work
|
|
42
|
+
# ...
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
data/lib/cmdx/attribute.rb
CHANGED
|
@@ -112,6 +112,7 @@ module CMDx
|
|
|
112
112
|
#
|
|
113
113
|
# @param names [Array<Symbol, String>] The names of the attributes to create
|
|
114
114
|
# @param options [Hash] Configuration options for the attributes
|
|
115
|
+
# @option options [Object] :* Any attribute configuration option
|
|
115
116
|
#
|
|
116
117
|
# @yield [self] Block to configure nested attributes
|
|
117
118
|
#
|
|
@@ -137,6 +138,7 @@ module CMDx
|
|
|
137
138
|
#
|
|
138
139
|
# @param names [Array<Symbol, String>] The names of the attributes to create
|
|
139
140
|
# @param options [Hash] Configuration options for the attributes
|
|
141
|
+
# @option options [Object] :* Any attribute configuration option
|
|
140
142
|
#
|
|
141
143
|
# @yield [self] Block to configure nested attributes
|
|
142
144
|
#
|
|
@@ -154,6 +156,7 @@ module CMDx
|
|
|
154
156
|
#
|
|
155
157
|
# @param names [Array<Symbol, String>] The names of the attributes to create
|
|
156
158
|
# @param options [Hash] Configuration options for the attributes
|
|
159
|
+
# @option options [Object] :* Any attribute configuration option
|
|
157
160
|
#
|
|
158
161
|
# @yield [self] Block to configure nested attributes
|
|
159
162
|
#
|
|
@@ -238,6 +241,7 @@ module CMDx
|
|
|
238
241
|
#
|
|
239
242
|
# @param names [Array<Symbol, String>] The names of the child attributes
|
|
240
243
|
# @param options [Hash] Configuration options for the child attributes
|
|
244
|
+
# @option options [Object] :* Any attribute configuration option
|
|
241
245
|
#
|
|
242
246
|
# @yield [self] Block to configure the child attributes
|
|
243
247
|
#
|
|
@@ -257,6 +261,7 @@ module CMDx
|
|
|
257
261
|
#
|
|
258
262
|
# @param names [Array<Symbol, String>] The names of the optional child attributes
|
|
259
263
|
# @param options [Hash] Configuration options for the child attributes
|
|
264
|
+
# @option options [Object] :* Any attribute configuration option
|
|
260
265
|
#
|
|
261
266
|
# @yield [self] Block to configure the child attributes
|
|
262
267
|
#
|
|
@@ -274,6 +279,7 @@ module CMDx
|
|
|
274
279
|
#
|
|
275
280
|
# @param names [Array<Symbol, String>] The names of the required child attributes
|
|
276
281
|
# @param options [Hash] Configuration options for the child attributes
|
|
282
|
+
# @option options [Object] :* Any attribute configuration option
|
|
277
283
|
#
|
|
278
284
|
# @yield [self] Block to configure the child attributes
|
|
279
285
|
#
|