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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -76
  3. data/LICENSE.txt +3 -20
  4. data/README.md +8 -7
  5. data/lib/cmdx/attribute.rb +21 -5
  6. data/lib/cmdx/context.rb +16 -0
  7. data/lib/cmdx/executor.rb +9 -9
  8. data/lib/cmdx/result.rb +27 -7
  9. data/lib/cmdx/task.rb +19 -0
  10. data/lib/cmdx/version.rb +1 -1
  11. data/mkdocs.yml +62 -36
  12. metadata +3 -57
  13. data/.cursor/prompts/docs.md +0 -12
  14. data/.cursor/prompts/llms.md +0 -8
  15. data/.cursor/prompts/rspec.md +0 -24
  16. data/.cursor/prompts/yardoc.md +0 -15
  17. data/.cursor/rules/cursor-instructions.mdc +0 -68
  18. data/.irbrc +0 -18
  19. data/.rspec +0 -4
  20. data/.rubocop.yml +0 -95
  21. data/.ruby-version +0 -1
  22. data/.yard-lint.yml +0 -174
  23. data/.yardopts +0 -7
  24. data/docs/.DS_Store +0 -0
  25. data/docs/assets/favicon.ico +0 -0
  26. data/docs/assets/favicon.svg +0 -1
  27. data/docs/attributes/coercions.md +0 -155
  28. data/docs/attributes/defaults.md +0 -77
  29. data/docs/attributes/definitions.md +0 -283
  30. data/docs/attributes/naming.md +0 -68
  31. data/docs/attributes/transformations.md +0 -63
  32. data/docs/attributes/validations.md +0 -336
  33. data/docs/basics/chain.md +0 -108
  34. data/docs/basics/context.md +0 -121
  35. data/docs/basics/execution.md +0 -152
  36. data/docs/basics/setup.md +0 -107
  37. data/docs/callbacks.md +0 -157
  38. data/docs/configuration.md +0 -314
  39. data/docs/deprecation.md +0 -143
  40. data/docs/getting_started.md +0 -137
  41. data/docs/index.md +0 -134
  42. data/docs/internationalization.md +0 -126
  43. data/docs/interruptions/exceptions.md +0 -52
  44. data/docs/interruptions/faults.md +0 -169
  45. data/docs/interruptions/halt.md +0 -216
  46. data/docs/logging.md +0 -90
  47. data/docs/middlewares.md +0 -191
  48. data/docs/outcomes/result.md +0 -197
  49. data/docs/outcomes/states.md +0 -66
  50. data/docs/outcomes/statuses.md +0 -65
  51. data/docs/retries.md +0 -121
  52. data/docs/stylesheets/extra.css +0 -42
  53. data/docs/tips_and_tricks.md +0 -157
  54. data/docs/workflows.md +0 -226
  55. data/examples/active_record_database_transaction.md +0 -27
  56. data/examples/active_record_query_tagging.md +0 -46
  57. data/examples/flipper_feature_flags.md +0 -50
  58. data/examples/paper_trail_whatdunnit.md +0 -39
  59. data/examples/redis_idempotency.md +0 -71
  60. data/examples/sentry_error_tracking.md +0 -46
  61. data/examples/sidekiq_async_execution.md +0 -29
  62. data/examples/stoplight_circuit_breaker.md +0 -36
  63. data/src/cmdx-dark-logo.png +0 -0
  64. data/src/cmdx-favicon.svg +0 -1
  65. data/src/cmdx-light-logo.png +0 -0
  66. data/src/cmdx-logo.svg +0 -1
@@ -1,155 +0,0 @@
1
- # Attributes - Coercions
2
-
3
- Automatically convert inputs to expected types. Coercions handle everything from simple string-to-integer conversions to JSON parsing.
4
-
5
- See [Global Configuration](../getting_started.md#coercions) for custom coercion setup.
6
-
7
- ## Usage
8
-
9
- Define attribute types to enable automatic coercion:
10
-
11
- ```ruby
12
- class ParseMetrics < CMDx::Task
13
- # Coerce into a symbol
14
- attribute :measurement_type, type: :symbol
15
-
16
- # Coerce into a rational fallback to big decimal
17
- attribute :value, type: [:rational, :big_decimal]
18
-
19
- # Coerce with options
20
- attribute :recorded_at, type: :date, strptime: "%m-%d-%Y"
21
-
22
- def work
23
- measurement_type #=> :temperature
24
- recorded_at #=> <Date 2024-01-23>
25
- value #=> 98.6 (Float)
26
- end
27
- end
28
-
29
- ParseMetrics.execute(
30
- measurement_type: "temperature",
31
- recorded_at: "01-23-2020",
32
- value: "98.6"
33
- )
34
- ```
35
-
36
- !!! tip
37
-
38
- Specify multiple coercion types for attributes that could be a variety of value formats. CMDx attempts each type in order until one succeeds.
39
-
40
- ## Built-in Coercions
41
-
42
- | Type | Options | Description | Examples |
43
- |------|---------|-------------|----------|
44
- | `:array` | | Array conversion with JSON support | `"val"` → `["val"]`<br>`"[1,2,3]"` → `[1, 2, 3]` |
45
- | `:big_decimal` | `:precision` | High-precision decimal | `"123.456"` → `BigDecimal("123.456")` |
46
- | `:boolean` | | Boolean with text patterns | `"yes"` → `true`, `"no"` → `false` |
47
- | `:complex` | | Complex numbers | `"1+2i"` → `Complex(1, 2)` |
48
- | `:date` | `:strptime` | Date objects | `"2024-01-23"` → `Date.new(2024, 1, 23)` |
49
- | `:datetime` | `:strptime` | DateTime objects | `"2024-01-23 10:30"` → `DateTime.new(2024, 1, 23, 10, 30)` |
50
- | `:float` | | Floating-point numbers | `"123.45"` → `123.45` |
51
- | `:hash` | | Hash conversion with JSON support | `'{"a":1}'` → `{"a" => 1}` |
52
- | `:integer` | | Integer with hex/octal support | `"0xFF"` → `255`, `"077"` → `63` |
53
- | `:rational` | | Rational numbers | `"1/2"` → `Rational(1, 2)` |
54
- | `:string` | | String conversion | `123` → `"123"` |
55
- | `:symbol` | | Symbol conversion | `"abc"` → `:abc` |
56
- | `:time` | `:strptime` | Time objects | `"10:30:00"` → `Time.new(2024, 1, 23, 10, 30)` |
57
-
58
- ## Declarations
59
-
60
- !!! warning "Important"
61
-
62
- Custom coercions must raise `CMDx::CoercionError` with a descriptive message.
63
-
64
- ### Proc or Lambda
65
-
66
- Use anonymous functions for simple coercion logic:
67
-
68
- ```ruby
69
- class TransformCoordinates < CMDx::Task
70
- # Proc
71
- register :callback, :geolocation, proc do |value, options = {}|
72
- begin
73
- Geolocation(value)
74
- rescue StandardError
75
- raise CMDx::CoercionError, "could not convert into a geolocation"
76
- end
77
- end
78
-
79
- # Lambda
80
- register :callback, :geolocation, ->(value, options = {}) {
81
- begin
82
- Geolocation(value)
83
- rescue StandardError
84
- raise CMDx::CoercionError, "could not convert into a geolocation"
85
- end
86
- }
87
- end
88
- ```
89
-
90
- ### Class or Module
91
-
92
- Register custom coercion logic for specialized type handling:
93
-
94
- ```ruby
95
- class GeolocationCoercion
96
- def self.call(value, options = {})
97
- Geolocation(value)
98
- rescue StandardError
99
- raise CMDx::CoercionError, "could not convert into a geolocation"
100
- end
101
- end
102
-
103
- class TransformCoordinates < CMDx::Task
104
- register :coercion, :geolocation, GeolocationCoercion
105
-
106
- attribute :latitude, type: :geolocation
107
- end
108
- ```
109
-
110
- ## Removals
111
-
112
- Remove unwanted coercions:
113
-
114
- !!! warning
115
-
116
- Each `deregister` call removes one coercion. Use multiple calls for batch removals.
117
-
118
- ```ruby
119
- class TransformCoordinates < CMDx::Task
120
- deregister :coercion, :geolocation
121
- end
122
- ```
123
-
124
- ## Error Handling
125
-
126
- Coercion failures provide detailed error information including attribute paths, attempted types, and specific failure reasons:
127
-
128
- ```ruby
129
- class AnalyzePerformance < CMDx::Task
130
- attribute :iterations, type: :integer
131
- attribute :score, type: [:float, :big_decimal]
132
-
133
- def work
134
- # Your logic here...
135
- end
136
- end
137
-
138
- result = AnalyzePerformance.execute(
139
- iterations: "not-a-number",
140
- score: "invalid-float"
141
- )
142
-
143
- result.state #=> "interrupted"
144
- result.status #=> "failed"
145
- result.reason #=> "Invalid"
146
- result.metadata #=> {
147
- # errors: {
148
- # full_message: "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal.",
149
- # messages: {
150
- # iterations: ["could not coerce into an integer"],
151
- # score: ["could not coerce into one of: float, big_decimal"]
152
- # }
153
- # }
154
- # }
155
- ```
@@ -1,77 +0,0 @@
1
- # Attributes - Defaults
2
-
3
- Provide fallback values for optional attributes. Defaults kick in when values aren't provided or are `nil`.
4
-
5
- ## Declarations
6
-
7
- Defaults work seamlessly with coercions, validations, and nested attributes:
8
-
9
- ### Static Values
10
-
11
- ```ruby
12
- class OptimizeDatabase < CMDx::Task
13
- attribute :strategy, default: :incremental
14
- attribute :level, default: "basic"
15
- attribute :notify_admin, default: true
16
- attribute :timeout_minutes, default: 30
17
- attribute :indexes, default: []
18
- attribute :options, default: {}
19
-
20
- def work
21
- strategy #=> :incremental
22
- level #=> "basic"
23
- notify_admin #=> true
24
- timeout_minutes #=> 30
25
- indexes #=> []
26
- options #=> {}
27
- end
28
- end
29
- ```
30
-
31
- ### Symbol References
32
-
33
- Reference instance methods by symbol for dynamic default values:
34
-
35
- ```ruby
36
- class ProcessAnalytics < CMDx::Task
37
- attribute :granularity, default: :default_granularity
38
-
39
- def work
40
- # Your logic here...
41
- end
42
-
43
- private
44
-
45
- def default_granularity
46
- Current.user.premium? ? "hourly" : "daily"
47
- end
48
- end
49
- ```
50
-
51
- ### Proc or Lambda
52
-
53
- Use anonymous functions for dynamic default values:
54
-
55
- ```ruby
56
- class CacheContent < CMDx::Task
57
- # Proc
58
- attribute :expire_hours, default: proc { Current.tenant.cache_duration || 24 }
59
-
60
- # Lambda
61
- attribute :compression, default: -> { Current.tenant.premium? ? "gzip" : "none" }
62
- end
63
- ```
64
-
65
- ## Coercions and Validations
66
-
67
- Defaults follow the same coercion and validation rules as provided values:
68
-
69
- ```ruby
70
- class ScheduleBackup < CMDx::Task
71
- # Coercions
72
- attribute :retention_days, default: "7", type: :integer
73
-
74
- # Validations
75
- optional :frequency, default: "daily", inclusion: { in: %w[hourly daily weekly monthly] }
76
- end
77
- ```
@@ -1,283 +0,0 @@
1
- # Attributes - Definitions
2
-
3
- Attributes define your task's interface with automatic validation, type coercion, and accessor generation. They're the contract between callers and your business logic.
4
-
5
- ## Declarations
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
-
11
- !!! tip
12
-
13
- Prefer using the `required` and `optional` alias for `attributes` for brevity and to clearly signal intent.
14
-
15
- ### Optional
16
-
17
- Optional attributes return `nil` when not provided.
18
-
19
- ```ruby
20
- class ScheduleEvent < CMDx::Task
21
- attribute :title
22
- attributes :duration, :location
23
-
24
- # Alias for attributes (preferred)
25
- optional :description
26
- optional :visibility, :attendees
27
-
28
- def work
29
- title #=> "Team Standup"
30
- duration #=> 30
31
- location #=> nil
32
- description #=> nil
33
- visibility #=> nil
34
- attendees #=> ["alice@company.com", "bob@company.com"]
35
- end
36
- end
37
-
38
- # Attributes passed as keyword arguments
39
- ScheduleEvent.execute(
40
- title: "Team Standup",
41
- duration: 30,
42
- attendees: ["alice@company.com", "bob@company.com"]
43
- )
44
- ```
45
-
46
- ### Required
47
-
48
- Required attributes must be provided in call arguments or task execution will fail.
49
-
50
- ```ruby
51
- class PublishArticle < CMDx::Task
52
- attribute :title, required: true
53
- attributes :content, :author_id, required: true
54
-
55
- # Alias for attributes => required: true (preferred)
56
- required :category
57
- required :status, :tags
58
-
59
- # Conditionally required
60
- required :publisher, if: :magazine?
61
- attribute :approver, required: true, unless: proc { status == :published }
62
-
63
- def work
64
- title #=> "Getting Started with Ruby"
65
- content #=> "This is a comprehensive guide..."
66
- author_id #=> 42
67
- category #=> "programming"
68
- status #=> :published
69
- tags #=> ["ruby", "beginner"]
70
- publisher #=> "Eastbay"
71
- approver #=> #<Editor ...>
72
- end
73
-
74
- private
75
-
76
- def magazine?
77
- context.title.ends_with?("[M]")
78
- end
79
- end
80
- ```
81
-
82
- !!! note
83
-
84
- When a required attribute's condition evaluates to `false`, the attribute behaves as optional. All other attribute features such as coercions, validations, defaults, and transformations still apply normally.
85
-
86
- ## Sources
87
-
88
- Attributes read from any accessible object—not just context. Use sources to pull data from models, services, or any callable:
89
-
90
- ### Context
91
-
92
- ```ruby
93
- class BackupDatabase < CMDx::Task
94
- # Default source is :context
95
- required :database_name
96
- optional :compression_level
97
-
98
- # Explicitly specify context source
99
- attribute :backup_path, source: :context
100
-
101
- def work
102
- database_name #=> context.database_name
103
- backup_path #=> context.backup_path
104
- compression_level #=> context.compression_level
105
- end
106
- end
107
- ```
108
-
109
- ### Symbol References
110
-
111
- Reference instance methods by symbol for dynamic source values:
112
-
113
- ```ruby
114
- class BackupDatabase < CMDx::Task
115
- attributes :host, :credentials, source: :database_config
116
-
117
- # Access from declared attributes
118
- attribute :connection_string, source: :credentials
119
-
120
- def work
121
- # Your logic here...
122
- end
123
-
124
- private
125
-
126
- def database_config
127
- @database_config ||= DatabaseConfig.find(context.database_name)
128
- end
129
- end
130
- ```
131
-
132
- ### Proc or Lambda
133
-
134
- Use anonymous functions for dynamic source values:
135
-
136
- ```ruby
137
- class BackupDatabase < CMDx::Task
138
- # Proc
139
- attribute :timestamp, source: proc { Time.current }
140
-
141
- # Lambda
142
- attribute :server, source: -> { Current.server }
143
- end
144
- ```
145
-
146
- ### Class or Module
147
-
148
- For complex source logic, use classes or modules:
149
-
150
- ```ruby
151
- class DatabaseResolver
152
- def self.call(task)
153
- Database.find(task.context.database_name)
154
- end
155
- end
156
-
157
- class BackupDatabase < CMDx::Task
158
- # Class or Module
159
- attribute :schema, source: DatabaseResolver
160
-
161
- # Instance
162
- attribute :metadata, source: DatabaseResolver.new
163
- end
164
- ```
165
-
166
- ## Nesting
167
-
168
- Build complex structures with nested attributes. Children inherit their parent as source and support all attribute options:
169
-
170
- !!! note
171
-
172
- Nested attributes support all features: naming, coercions, validations, defaults, and more.
173
-
174
- ```ruby
175
- class ConfigureServer < CMDx::Task
176
- # Required parent with required children
177
- required :network_config do
178
- required :hostname, :port, :protocol, :subnet
179
- optional :load_balancer
180
- attribute :firewall_rules
181
- end
182
-
183
- # Optional parent with conditional children
184
- optional :ssl_config do
185
- required :certificate_path, :private_key # Only required if ssl_config provided
186
- optional :enable_http2, prefix: true
187
- end
188
-
189
- # Multi-level nesting
190
- attribute :monitoring do
191
- required :provider
192
-
193
- optional :alerting do
194
- required :threshold_percentage
195
- optional :notification_channel
196
- end
197
- end
198
-
199
- def work
200
- network_config #=> { hostname: "api.company.com" ... }
201
- hostname #=> "api.company.com"
202
- load_balancer #=> nil
203
- end
204
- end
205
-
206
- ConfigureServer.execute(
207
- server_id: "srv-001",
208
- network_config: {
209
- hostname: "api.company.com",
210
- port: 443,
211
- protocol: "https",
212
- subnet: "10.0.1.0/24",
213
- firewall_rules: "allow_web_traffic"
214
- },
215
- monitoring: {
216
- provider: "datadog",
217
- alerting: {
218
- threshold_percentage: 85.0,
219
- notification_channel: "slack"
220
- }
221
- }
222
- )
223
- ```
224
-
225
- !!! warning "Important"
226
-
227
- Child requirements only apply when the parent is provided—perfect for optional structures.
228
-
229
- ## Error Handling
230
-
231
- Validation failures provide detailed, structured error messages:
232
-
233
- !!! note
234
-
235
- Nested attributes are only validated when their parent is present and valid.
236
-
237
- ```ruby
238
- class ConfigureServer < CMDx::Task
239
- required :server_id, :environment
240
- required :network_config do
241
- required :hostname, :port
242
- end
243
-
244
- def work
245
- # Your logic here...
246
- end
247
- end
248
-
249
- # Missing required top-level attributes
250
- result = ConfigureServer.execute(server_id: "srv-001")
251
-
252
- result.state #=> "interrupted"
253
- result.status #=> "failed"
254
- result.reason #=> "Invalid"
255
- result.metadata #=> {
256
- # errors: {
257
- # full_message: "environment is required. network_config is required.",
258
- # messages: {
259
- # environment: ["is required"],
260
- # network_config: ["is required"]
261
- # }
262
- # }
263
- # }
264
-
265
- # Missing required nested attributes
266
- result = ConfigureServer.execute(
267
- server_id: "srv-001",
268
- environment: "production",
269
- network_config: { hostname: "api.company.com" } # Missing port
270
- )
271
-
272
- result.state #=> "interrupted"
273
- result.status #=> "failed"
274
- result.reason #=> "Invalid"
275
- result.metadata #=> {
276
- # errors: {
277
- # full_message: "port is required.",
278
- # messages: {
279
- # port: ["is required"]
280
- # }
281
- # }
282
- # }
283
- ```
@@ -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
- ```