cmdx 1.1.1 → 1.5.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/.DS_Store +0 -0
- data/.cursor/prompts/docs.md +4 -1
- data/.cursor/prompts/llms.md +20 -0
- data/.cursor/prompts/rspec.md +4 -1
- data/.cursor/prompts/yardoc.md +3 -2
- data/.cursor/rules/cursor-instructions.mdc +56 -1
- data/.irbrc +6 -0
- data/.rubocop.yml +29 -18
- data/.ruby-version +1 -1
- data/CHANGELOG.md +6 -128
- data/LLM.md +3317 -0
- data/README.md +68 -44
- data/docs/attributes/coercions.md +162 -0
- data/docs/attributes/defaults.md +90 -0
- data/docs/attributes/definitions.md +281 -0
- data/docs/attributes/naming.md +78 -0
- data/docs/attributes/validations.md +309 -0
- data/docs/basics/chain.md +56 -249
- data/docs/basics/context.md +56 -289
- data/docs/basics/execution.md +114 -0
- data/docs/basics/setup.md +37 -334
- data/docs/callbacks.md +89 -467
- data/docs/deprecation.md +91 -174
- data/docs/getting_started.md +212 -202
- data/docs/internationalization.md +11 -647
- data/docs/interruptions/exceptions.md +23 -198
- data/docs/interruptions/faults.md +71 -151
- data/docs/interruptions/halt.md +109 -186
- data/docs/logging.md +44 -256
- data/docs/middlewares.md +113 -426
- data/docs/outcomes/result.md +81 -228
- data/docs/outcomes/states.md +33 -221
- data/docs/outcomes/statuses.md +21 -311
- data/docs/tips_and_tricks.md +120 -70
- data/docs/workflows.md +99 -283
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/attribute.rb +229 -0
- data/lib/cmdx/attribute_registry.rb +94 -0
- data/lib/cmdx/attribute_value.rb +193 -0
- data/lib/cmdx/callback_registry.rb +69 -77
- data/lib/cmdx/chain.rb +56 -73
- data/lib/cmdx/coercion_registry.rb +52 -68
- data/lib/cmdx/coercions/array.rb +19 -18
- data/lib/cmdx/coercions/big_decimal.rb +20 -24
- data/lib/cmdx/coercions/boolean.rb +26 -25
- data/lib/cmdx/coercions/complex.rb +21 -22
- data/lib/cmdx/coercions/date.rb +25 -23
- data/lib/cmdx/coercions/date_time.rb +24 -25
- data/lib/cmdx/coercions/float.rb +25 -22
- data/lib/cmdx/coercions/hash.rb +31 -32
- data/lib/cmdx/coercions/integer.rb +30 -24
- data/lib/cmdx/coercions/rational.rb +29 -24
- data/lib/cmdx/coercions/string.rb +19 -22
- data/lib/cmdx/coercions/symbol.rb +37 -0
- data/lib/cmdx/coercions/time.rb +26 -25
- data/lib/cmdx/configuration.rb +49 -108
- data/lib/cmdx/context.rb +222 -44
- data/lib/cmdx/deprecator.rb +61 -0
- data/lib/cmdx/errors.rb +42 -252
- data/lib/cmdx/exceptions.rb +39 -0
- data/lib/cmdx/faults.rb +78 -39
- data/lib/cmdx/freezer.rb +51 -0
- data/lib/cmdx/identifier.rb +30 -0
- data/lib/cmdx/locale.rb +52 -0
- data/lib/cmdx/log_formatters/json.rb +21 -22
- data/lib/cmdx/log_formatters/key_value.rb +20 -22
- data/lib/cmdx/log_formatters/line.rb +15 -22
- data/lib/cmdx/log_formatters/logstash.rb +22 -23
- data/lib/cmdx/log_formatters/raw.rb +16 -22
- data/lib/cmdx/middleware_registry.rb +70 -74
- data/lib/cmdx/middlewares/correlate.rb +90 -54
- data/lib/cmdx/middlewares/runtime.rb +58 -0
- data/lib/cmdx/middlewares/timeout.rb +48 -68
- data/lib/cmdx/railtie.rb +12 -45
- data/lib/cmdx/result.rb +229 -314
- data/lib/cmdx/task.rb +194 -366
- data/lib/cmdx/utils/call.rb +49 -0
- data/lib/cmdx/utils/condition.rb +71 -0
- data/lib/cmdx/utils/format.rb +61 -0
- data/lib/cmdx/validator_registry.rb +63 -72
- data/lib/cmdx/validators/exclusion.rb +38 -67
- data/lib/cmdx/validators/format.rb +48 -49
- data/lib/cmdx/validators/inclusion.rb +43 -74
- data/lib/cmdx/validators/length.rb +91 -154
- data/lib/cmdx/validators/numeric.rb +87 -162
- data/lib/cmdx/validators/presence.rb +37 -50
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/worker.rb +178 -0
- data/lib/cmdx/workflow.rb +85 -81
- data/lib/cmdx.rb +19 -13
- data/lib/generators/cmdx/install_generator.rb +14 -13
- data/lib/generators/cmdx/task_generator.rb +25 -50
- data/lib/generators/cmdx/templates/install.rb +11 -46
- data/lib/generators/cmdx/templates/task.rb.tt +3 -2
- data/lib/locales/en.yml +18 -4
- data/src/cmdx-logo.png +0 -0
- metadata +32 -116
- data/docs/ai_prompts.md +0 -393
- data/docs/basics/call.md +0 -317
- data/docs/configuration.md +0 -344
- data/docs/parameters/coercions.md +0 -396
- data/docs/parameters/defaults.md +0 -335
- data/docs/parameters/definitions.md +0 -446
- data/docs/parameters/namespacing.md +0 -378
- data/docs/parameters/validations.md +0 -405
- data/docs/testing.md +0 -553
- data/lib/cmdx/callback.rb +0 -53
- data/lib/cmdx/chain_inspector.rb +0 -56
- data/lib/cmdx/chain_serializer.rb +0 -63
- data/lib/cmdx/coercion.rb +0 -57
- data/lib/cmdx/coercions/virtual.rb +0 -29
- data/lib/cmdx/core_ext/hash.rb +0 -83
- data/lib/cmdx/core_ext/module.rb +0 -98
- data/lib/cmdx/core_ext/object.rb +0 -125
- data/lib/cmdx/correlator.rb +0 -122
- data/lib/cmdx/error.rb +0 -60
- data/lib/cmdx/fault.rb +0 -140
- data/lib/cmdx/immutator.rb +0 -52
- data/lib/cmdx/lazy_struct.rb +0 -246
- data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
- data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
- data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
- data/lib/cmdx/logger.rb +0 -49
- data/lib/cmdx/logger_ansi.rb +0 -68
- data/lib/cmdx/logger_serializer.rb +0 -116
- data/lib/cmdx/middleware.rb +0 -70
- data/lib/cmdx/parameter.rb +0 -312
- data/lib/cmdx/parameter_evaluator.rb +0 -231
- data/lib/cmdx/parameter_inspector.rb +0 -66
- data/lib/cmdx/parameter_registry.rb +0 -106
- data/lib/cmdx/parameter_serializer.rb +0 -59
- data/lib/cmdx/result_ansi.rb +0 -71
- data/lib/cmdx/result_inspector.rb +0 -71
- data/lib/cmdx/result_logger.rb +0 -59
- data/lib/cmdx/result_serializer.rb +0 -104
- data/lib/cmdx/rspec/matchers.rb +0 -28
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
- data/lib/cmdx/task_deprecator.rb +0 -52
- data/lib/cmdx/task_processor.rb +0 -246
- data/lib/cmdx/task_serializer.rb +0 -57
- data/lib/cmdx/utils/ansi_color.rb +0 -73
- data/lib/cmdx/utils/log_timestamp.rb +0 -36
- data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
- data/lib/cmdx/utils/name_affix.rb +0 -52
- data/lib/cmdx/validator.rb +0 -57
- data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
- data/lib/generators/cmdx/workflow_generator.rb +0 -84
- data/lib/locales/ar.yml +0 -35
- data/lib/locales/cs.yml +0 -35
- data/lib/locales/da.yml +0 -35
- data/lib/locales/de.yml +0 -35
- data/lib/locales/el.yml +0 -35
- data/lib/locales/es.yml +0 -35
- data/lib/locales/fi.yml +0 -35
- data/lib/locales/fr.yml +0 -35
- data/lib/locales/he.yml +0 -35
- data/lib/locales/hi.yml +0 -35
- data/lib/locales/it.yml +0 -35
- data/lib/locales/ja.yml +0 -35
- data/lib/locales/ko.yml +0 -35
- data/lib/locales/nl.yml +0 -35
- data/lib/locales/no.yml +0 -35
- data/lib/locales/pl.yml +0 -35
- data/lib/locales/pt.yml +0 -35
- data/lib/locales/ru.yml +0 -35
- data/lib/locales/sv.yml +0 -35
- data/lib/locales/th.yml +0 -35
- data/lib/locales/tr.yml +0 -35
- data/lib/locales/vi.yml +0 -35
- data/lib/locales/zh.yml +0 -35
data/README.md
CHANGED
@@ -1,16 +1,25 @@
|
|
1
|
-
|
1
|
+
<p align="center">
|
2
|
+
<img src="./src/cmdx-logo.png" width="200" alt="CMDx Logo">
|
3
|
+
</p>
|
2
4
|
|
3
|
-
|
5
|
+
<p align="center">
|
6
|
+
<img alt="Version" src="https://img.shields.io/gem/v/cmdx">
|
7
|
+
<img alt="Build" src="https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg">
|
8
|
+
<img alt="License" src="https://img.shields.io/github/license/drexed/cmdx">
|
9
|
+
</p>
|
4
10
|
|
5
|
-
|
6
|
-
[](https://github.com/drexed/cmdx/actions/workflows/ci.yml)
|
7
|
-
[](http://makeapullrequest.com)
|
11
|
+
# CMDx
|
8
12
|
|
9
|
-
|
13
|
+
CMDx is a framework for building maintainable business processes. It simplifies building task objects by offering integrated:
|
10
14
|
|
11
|
-
|
15
|
+
- Flow controls
|
16
|
+
- Composable workflows
|
17
|
+
- Comprehensive logging
|
18
|
+
- Attribute definition
|
19
|
+
- Validations and coercions
|
20
|
+
- And much more...
|
12
21
|
|
13
|
-
|
22
|
+
## Installation
|
14
23
|
|
15
24
|
Add this line to your application's Gemfile:
|
16
25
|
|
@@ -28,83 +37,98 @@ Or install it yourself as:
|
|
28
37
|
|
29
38
|
## Quick Example
|
30
39
|
|
40
|
+
Here's how a quick 3 step process can open up a world of possibilities:
|
41
|
+
|
31
42
|
```ruby
|
32
|
-
# Setup task
|
33
|
-
|
34
|
-
|
43
|
+
# 1. Setup task
|
44
|
+
# ---------------------------------
|
45
|
+
class AnalyzeMetrics < CMDx::Task
|
46
|
+
register :middleware, CMDx::Middlewares::Correlate, id: -> { Current.request_id }
|
35
47
|
|
36
|
-
on_success :
|
48
|
+
on_success :track_analysis_completion!
|
37
49
|
|
38
|
-
required :
|
39
|
-
optional :
|
50
|
+
required :dataset_id, type: :integer, numeric: { min: 1 }
|
51
|
+
optional :analysis_type, default: "standard"
|
40
52
|
|
41
|
-
def
|
42
|
-
if
|
43
|
-
fail!(
|
44
|
-
elsif
|
45
|
-
skip!(
|
53
|
+
def work
|
54
|
+
if dataset.nil?
|
55
|
+
fail!("Dataset not found", code: 404)
|
56
|
+
elsif dataset.unprocessed?
|
57
|
+
skip!("Dataset not ready for analysis")
|
46
58
|
else
|
47
|
-
|
48
|
-
context.
|
59
|
+
context.result = PValueAnalyzer.analyze(dataset, analysis_type)
|
60
|
+
context.analyzed_at = Time.now
|
49
61
|
end
|
50
62
|
end
|
51
63
|
|
52
64
|
private
|
53
65
|
|
54
|
-
def
|
55
|
-
@
|
66
|
+
def dataset
|
67
|
+
@dataset ||= Dataset.find_by(id: dataset_id)
|
56
68
|
end
|
57
69
|
|
58
|
-
def
|
59
|
-
|
70
|
+
def track_analysis_completion!
|
71
|
+
dataset.update!(analysis_result_id: context.result.id)
|
60
72
|
end
|
61
73
|
end
|
62
74
|
|
63
|
-
# Execute task
|
64
|
-
|
75
|
+
# 2. Execute task
|
76
|
+
# ---------------------------------
|
77
|
+
result = AnalyzeMetrics.execute(
|
78
|
+
dataset_id: 123,
|
79
|
+
"analysis_type" => "advanced"
|
80
|
+
)
|
65
81
|
|
66
|
-
# Handle result
|
82
|
+
# 3. Handle result
|
83
|
+
# ---------------------------------
|
67
84
|
if result.success?
|
68
|
-
puts "
|
85
|
+
puts "Metrics analyzed at #{result.context.analyzed_at}"
|
69
86
|
elsif result.skipped?
|
70
|
-
puts "
|
87
|
+
puts "Skipping analyzation due to: #{result.reason}"
|
71
88
|
elsif result.failed?
|
72
|
-
puts "
|
89
|
+
puts "Analyzation failed due to: #{result.reason} with code #{result.metadata[:code]}"
|
73
90
|
end
|
74
91
|
```
|
75
92
|
|
76
93
|
## Table of contents
|
77
94
|
|
78
95
|
- [Getting Started](docs/getting_started.md)
|
79
|
-
- [Configuration](docs/configuration.md)
|
80
96
|
- Basics
|
81
97
|
- [Setup](docs/basics/setup.md)
|
82
|
-
- [
|
98
|
+
- [Execution](docs/basics/execution.md)
|
83
99
|
- [Context](docs/basics/context.md)
|
84
100
|
- [Chain](docs/basics/chain.md)
|
85
101
|
- Interruptions
|
86
102
|
- [Halt](docs/interruptions/halt.md)
|
87
103
|
- [Faults](docs/interruptions/faults.md)
|
88
104
|
- [Exceptions](docs/interruptions/exceptions.md)
|
89
|
-
- Parameters
|
90
|
-
- [Definitions](docs/parameters/definitions.md)
|
91
|
-
- [Namespacing](docs/parameters/namespacing.md)
|
92
|
-
- [Coercions](docs/parameters/coercions.md)
|
93
|
-
- [Validations](docs/parameters/validations.md)
|
94
|
-
- [Defaults](docs/parameters/defaults.md)
|
95
105
|
- Outcomes
|
96
|
-
- [Result](
|
106
|
+
- [Result](docs/outcomes/result.md)
|
97
107
|
- [States](docs/outcomes/states.md)
|
98
108
|
- [Statuses](docs/outcomes/statuses.md)
|
109
|
+
- Attributes
|
110
|
+
- [Definitions](docs/attributes/definitions.md)
|
111
|
+
- [Naming](docs/attributes/naming.md)
|
112
|
+
- [Coercions](docs/attributes/coercions.md)
|
113
|
+
- [Validations](docs/attributes/validations.md)
|
114
|
+
- [Defaults](docs/attributes/defaults.md)
|
99
115
|
- [Callbacks](docs/callbacks.md)
|
100
116
|
- [Middlewares](docs/middlewares.md)
|
101
|
-
- [Workflows](docs/workflows.md)
|
102
117
|
- [Logging](docs/logging.md)
|
103
118
|
- [Internationalization (i18n)](docs/internationalization.md)
|
104
|
-
- [Testing](docs/testing.md)
|
105
119
|
- [Deprecation](docs/deprecation.md)
|
106
|
-
- [
|
107
|
-
- [Tips
|
120
|
+
- [Workflows](docs/workflows.md)
|
121
|
+
- [Tips and Tricks](docs/tips_and_tricks.md)
|
122
|
+
|
123
|
+
## Ecosystem
|
124
|
+
|
125
|
+
The following gems are currently under development:
|
126
|
+
|
127
|
+
- `cmdx-i18n` I18n locales
|
128
|
+
- `cmdx-rspec` RSpec matchers
|
129
|
+
- `cmdx-minitest` Minitest matchers
|
130
|
+
- `cmdx-jobs` Background job integrations
|
131
|
+
- `cmdx-parallel` Parallel workflow task execution
|
108
132
|
|
109
133
|
## Development
|
110
134
|
|
@@ -0,0 +1,162 @@
|
|
1
|
+
# Attributes - Coercions
|
2
|
+
|
3
|
+
Attribute coercions automatically convert task arguments to expected types, ensuring type safety while providing flexible input handling. Coercions transform raw input values into the specified types, supporting simple conversions like string-to-integer and complex operations like JSON parsing.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Usage](#usage)
|
8
|
+
- [Built-in Coercions](#built-in-coercions)
|
9
|
+
- [Declarations](#declarations)
|
10
|
+
- [Proc or Lambda](#proc-or-lambda)
|
11
|
+
- [Class or Module](#class-or-module)
|
12
|
+
- [Removals](#removals)
|
13
|
+
- [Error Handling](#error-handling)
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
Define attribute types to enable automatic coercion:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class ParseMetrics < CMDx::Task
|
21
|
+
# Coerce into a symbol
|
22
|
+
attribute :measurement_type, type: :symbol
|
23
|
+
|
24
|
+
# Coerce into a rational fallback to big decimal
|
25
|
+
attribute :value, type: [:rational, :big_decimal]
|
26
|
+
|
27
|
+
# Coerce with options
|
28
|
+
attribute :recorded_at, type: :date, strptime: "%m-%d-%Y"
|
29
|
+
|
30
|
+
def work
|
31
|
+
measurement_type #=> :temperature
|
32
|
+
recorded_at #=> <Date 2024-01-23>
|
33
|
+
value #=> 98.6 (Float)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
ParseMetrics.execute(
|
38
|
+
measurement_type: "temperature",
|
39
|
+
recorded_at: "01-23-2020",
|
40
|
+
value: "98.6"
|
41
|
+
)
|
42
|
+
```
|
43
|
+
|
44
|
+
> [!TIP]
|
45
|
+
> Specify multiple coercion types for attributes that could be a variety of value formats. CMDx attempts each type in order until one succeeds.
|
46
|
+
|
47
|
+
## Built-in Coercions
|
48
|
+
|
49
|
+
| Type | Options | Description | Examples |
|
50
|
+
|------|---------|-------------|----------|
|
51
|
+
| `:array` | | Array conversion with JSON support | `"val"` → `["val"]`<br>`"[1,2,3]"` → `[1, 2, 3]` |
|
52
|
+
| `:big_decimal` | `:precision` | High-precision decimal | `"123.456"` → `BigDecimal("123.456")` |
|
53
|
+
| `:boolean` | | Boolean with text patterns | `"yes"` → `true`, `"no"` → `false` |
|
54
|
+
| `:complex` | | Complex numbers | `"1+2i"` → `Complex(1, 2)` |
|
55
|
+
| `:date` | `:strptime` | Date objects | `"2024-01-23"` → `Date.new(2024, 1, 23)` |
|
56
|
+
| `:datetime` | `:strptime` | DateTime objects | `"2024-01-23 10:30"` → `DateTime.new(2024, 1, 23, 10, 30)` |
|
57
|
+
| `:float` | | Floating-point numbers | `"123.45"` → `123.45` |
|
58
|
+
| `:hash` | | Hash conversion with JSON support | `'{"a":1}'` → `{"a" => 1}` |
|
59
|
+
| `:integer` | | Integer with hex/octal support | `"0xFF"` → `255`, `"077"` → `63` |
|
60
|
+
| `:rational` | | Rational numbers | `"1/2"` → `Rational(1, 2)` |
|
61
|
+
| `:string` | | String conversion | `123` → `"123"` |
|
62
|
+
| `:symbol` | | Symbol conversion | `"abc"` → `:abc` |
|
63
|
+
| `:time` | `:strptime` | Time objects | `"10:30:00"` → `Time.new(2024, 1, 23, 10, 30)` |
|
64
|
+
|
65
|
+
## Declarations
|
66
|
+
|
67
|
+
> [!IMPORTANT]
|
68
|
+
> Coercions must raise a CMDx::CoercionError and its message is used as part of the fault reason and metadata.
|
69
|
+
|
70
|
+
### Proc or Lambda
|
71
|
+
|
72
|
+
Use anonymous functions for simple coercion logic:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
class TransformCoordinates < CMDx::Task
|
76
|
+
# Proc
|
77
|
+
register :callback, :geolocation, proc do |value, options = {}|
|
78
|
+
begin
|
79
|
+
Geolocation(value)
|
80
|
+
rescue StandardError
|
81
|
+
raise CMDx::CoercionError, "could not convert into a geolocation"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Lambda
|
86
|
+
register :callback, :geolocation, ->(value, options = {}) {
|
87
|
+
begin
|
88
|
+
Geolocation(value)
|
89
|
+
rescue StandardError
|
90
|
+
raise CMDx::CoercionError, "could not convert into a geolocation"
|
91
|
+
end
|
92
|
+
}
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
### Class or Module
|
97
|
+
|
98
|
+
Register custom coercion logic for specialized type handling:
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
class GeolocationCoercion
|
102
|
+
def self.call(value, options = {})
|
103
|
+
Geolocation(value)
|
104
|
+
rescue StandardError
|
105
|
+
raise CMDx::CoercionError, "could not convert into a geolocation"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class TransformCoordinates < CMDx::Task
|
110
|
+
register :coercion, :geolocation, GeolocationCoercion
|
111
|
+
|
112
|
+
attribute :latitude, type: :geolocation
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
## Removals
|
117
|
+
|
118
|
+
Remove custom coercions when no longer needed:
|
119
|
+
|
120
|
+
> [!WARNING]
|
121
|
+
> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
|
122
|
+
|
123
|
+
```ruby
|
124
|
+
class TransformCoordinates < CMDx::Task
|
125
|
+
deregister :coercion, :geolocation
|
126
|
+
end
|
127
|
+
```
|
128
|
+
|
129
|
+
## Error Handling
|
130
|
+
|
131
|
+
Coercion failures provide detailed error information including attribute paths, attempted types, and specific failure reasons:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
class AnalyzePerformance < CMDx::Task
|
135
|
+
attribute :iterations, type: :integer
|
136
|
+
attribute :score, type: [:float, :big_decimal]
|
137
|
+
|
138
|
+
def work
|
139
|
+
# Your logic here...
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
result = AnalyzePerformance.execute(
|
144
|
+
iterations: "not-a-number",
|
145
|
+
score: "invalid-float"
|
146
|
+
)
|
147
|
+
|
148
|
+
result.state #=> "interrupted"
|
149
|
+
result.status #=> "failed"
|
150
|
+
result.reason #=> "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal."
|
151
|
+
result.metadata #=> {
|
152
|
+
# messages: {
|
153
|
+
# iterations: ["could not coerce into an integer"],
|
154
|
+
# score: ["could not coerce into one of: float, big_decimal"]
|
155
|
+
# }
|
156
|
+
# }
|
157
|
+
```
|
158
|
+
|
159
|
+
---
|
160
|
+
|
161
|
+
- **Prev:** [Attributes - Naming](naming.md)
|
162
|
+
- **Next:** [Attributes - Validations](validations.md)
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# Attributes - Defaults
|
2
|
+
|
3
|
+
Attribute defaults provide fallback values when arguments are not provided or resolve to `nil`. Defaults ensure tasks have sensible values for optional attributes while maintaining flexibility for callers to override when needed.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Declarations](#declarations)
|
8
|
+
- [Static Values](#static-values)
|
9
|
+
- [Symbol References](#symbol-references)
|
10
|
+
- [Proc or Lambda](#proc-or-lambda)
|
11
|
+
- [Coercions and Validations](#coercions-and-validations)
|
12
|
+
|
13
|
+
## Declarations
|
14
|
+
|
15
|
+
Defaults apply when attributes are not provided or resolve to `nil`. They work seamlessly with coercion, validation, and nested attributes.
|
16
|
+
|
17
|
+
### Static Values
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
class OptimizeDatabase < CMDx::Task
|
21
|
+
attribute :strategy, default: :incremental
|
22
|
+
attribute :level, default: "basic"
|
23
|
+
attribute :notify_admin, default: true
|
24
|
+
attribute :timeout_minutes, default: 30
|
25
|
+
attribute :indexes, default: []
|
26
|
+
attribute :options, default: {}
|
27
|
+
|
28
|
+
def work
|
29
|
+
strategy #=> :incremental
|
30
|
+
level #=> "basic"
|
31
|
+
notify_admin #=> true
|
32
|
+
timeout_minutes #=> 30
|
33
|
+
indexes #=> []
|
34
|
+
options #=> {}
|
35
|
+
end
|
36
|
+
end
|
37
|
+
```
|
38
|
+
|
39
|
+
### Symbol References
|
40
|
+
|
41
|
+
Reference instance methods by symbol for dynamic default values:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
class ProcessAnalytics < CMDx::Task
|
45
|
+
attribute :granularity, default: :default_granularity
|
46
|
+
|
47
|
+
def work
|
48
|
+
# Your logic here...
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def default_granularity
|
54
|
+
Current.user.premium? ? "hourly" : "daily"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
59
|
+
### Proc or Lambda
|
60
|
+
|
61
|
+
Use anonymous functions for dynamic default values:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
class CacheContent < CMDx::Task
|
65
|
+
# Proc
|
66
|
+
attribute :expire_hours, default: proc { Current.tenant.cache_duration || 24 }
|
67
|
+
|
68
|
+
# Lambda
|
69
|
+
attribute :compression, default: -> { Current.tenant.premium? ? "gzip" : "none" }
|
70
|
+
end
|
71
|
+
```
|
72
|
+
|
73
|
+
## Coercions and Validations
|
74
|
+
|
75
|
+
Defaults are subject to the same coercion and validation rules as provided values, ensuring consistency and catching configuration errors early.
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
class ScheduleBackup < CMDx::Task
|
79
|
+
# Coercions
|
80
|
+
attribute :retention_days, default: "7", type: :integer
|
81
|
+
|
82
|
+
# Validations
|
83
|
+
optional :frequency, default: "daily", inclusion: { in: %w[hourly daily weekly monthly] }
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
87
|
+
---
|
88
|
+
|
89
|
+
- **Prev:** [Attributes - Validations](validations.md)
|
90
|
+
- **Next:** [Callbacks](../callbacks.md)
|
@@ -0,0 +1,281 @@
|
|
1
|
+
# Attributes - Definitions
|
2
|
+
|
3
|
+
Attributes define the interface between task callers and implementation, enabling automatic validation, type coercion, and method generation. They provide a contract to verify that task execution arguments match expected requirements and structure.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Declarations](#declarations)
|
8
|
+
- [Optional](#optional)
|
9
|
+
- [Required](#required)
|
10
|
+
- [Sources](#sources)
|
11
|
+
- [Context](#context)
|
12
|
+
- [Symbol References](#symbol-references)
|
13
|
+
- [Proc or Lambda](#proc-or-lambda)
|
14
|
+
- [Class or Module](#class-or-module)
|
15
|
+
- [Nesting](#nesting)
|
16
|
+
- [Error Handling](#error-handling)
|
17
|
+
|
18
|
+
## Declarations
|
19
|
+
|
20
|
+
> [!TIP]
|
21
|
+
> Prefer using the `required` and `optional` alias for `attributes` for brevity and to clearly signal intent.
|
22
|
+
|
23
|
+
### Optional
|
24
|
+
|
25
|
+
Optional attributes return `nil` when not provided.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
class ScheduleEvent < CMDx::Task
|
29
|
+
attribute :title
|
30
|
+
attributes :duration, :location
|
31
|
+
|
32
|
+
# Alias for attributes (preferred)
|
33
|
+
optional :description
|
34
|
+
optional :visibility, :attendees
|
35
|
+
|
36
|
+
def work
|
37
|
+
title #=> "Team Standup"
|
38
|
+
duration #=> 30
|
39
|
+
location #=> nil
|
40
|
+
description #=> nil
|
41
|
+
visibility #=> nil
|
42
|
+
attendees #=> ["alice@company.com", "bob@company.com"]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Attributes passed as keyword arguments
|
47
|
+
ScheduleEvent.execute(
|
48
|
+
title: "Team Standup",
|
49
|
+
duration: 30,
|
50
|
+
attendees: ["alice@company.com", "bob@company.com"]
|
51
|
+
)
|
52
|
+
```
|
53
|
+
|
54
|
+
### Required
|
55
|
+
|
56
|
+
Required attributes must be provided in call arguments or task execution will fail.
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
class PublishArticle < CMDx::Task
|
60
|
+
attribute :title, required: true
|
61
|
+
attributes :content, :author_id, required: true
|
62
|
+
|
63
|
+
# Alias for attributes => required: true (preferred)
|
64
|
+
required :category
|
65
|
+
required :status, :tags
|
66
|
+
|
67
|
+
def work
|
68
|
+
title #=> "Getting Started with Ruby"
|
69
|
+
content #=> "This is a comprehensive guide..."
|
70
|
+
author_id #=> 42
|
71
|
+
category #=> "programming"
|
72
|
+
status #=> :published
|
73
|
+
tags #=> ["ruby", "beginner"]
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Attributes passed as keyword arguments
|
78
|
+
PublishArticle.execute(
|
79
|
+
title: "Getting Started with Ruby",
|
80
|
+
content: "This is a comprehensive guide...",
|
81
|
+
author_id: 42,
|
82
|
+
category: "programming",
|
83
|
+
status: :published,
|
84
|
+
tags: ["ruby", "beginner"]
|
85
|
+
)
|
86
|
+
```
|
87
|
+
|
88
|
+
## Sources
|
89
|
+
|
90
|
+
Attributes delegate to accessible objects within the task. The default source is `:context`, but any accessible method or object can serve as an attribute source.
|
91
|
+
|
92
|
+
### Context
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
class BackupDatabase < CMDx::Task
|
96
|
+
# Default source is :context
|
97
|
+
required :database_name
|
98
|
+
optional :compression_level
|
99
|
+
|
100
|
+
# Explicitly specify context source
|
101
|
+
attribute :backup_path, source: :context
|
102
|
+
|
103
|
+
def work
|
104
|
+
database_name #=> context.database_name
|
105
|
+
backup_path #=> context.backup_path
|
106
|
+
compression_level #=> context.compression_level
|
107
|
+
end
|
108
|
+
end
|
109
|
+
```
|
110
|
+
|
111
|
+
### Symbol References
|
112
|
+
|
113
|
+
Reference instance methods by symbol for dynamic source values:
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
class BackupDatabase < CMDx::Task
|
117
|
+
attributes :host, :credentials, source: :database_config
|
118
|
+
|
119
|
+
# Access from declared attributes
|
120
|
+
attribute :connection_string, source: :credentials
|
121
|
+
|
122
|
+
def work
|
123
|
+
# Your logic here...
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def database_config
|
129
|
+
@database_config ||= DatabaseConfig.find(context.database_name)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
### Proc or Lambda
|
135
|
+
|
136
|
+
Use anonymous functions for dynamic source values:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class BackupDatabase < CMDx::Task
|
140
|
+
# Proc
|
141
|
+
attribute :timestamp, source: proc { Time.current }
|
142
|
+
|
143
|
+
# Lambda
|
144
|
+
attribute :server, source: -> { Current.server }
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
### Class or Module
|
149
|
+
|
150
|
+
For complex source logic, use classes or modules:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
class DatabaseResolver
|
154
|
+
def self.call(task)
|
155
|
+
Database.find(task.context.database_name)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class BackupDatabase < CMDx::Task
|
160
|
+
# Class or Module
|
161
|
+
attribute :schema, source: DatabaseResolver
|
162
|
+
|
163
|
+
# Instance
|
164
|
+
attribute :metadata, source: DatabaseResolver.new
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
## Nesting
|
169
|
+
|
170
|
+
Nested attributes enable complex attribute structures where child attributes automatically inherit their parent as the source. This allows validation and access of structured data.
|
171
|
+
|
172
|
+
> [!NOTE]
|
173
|
+
> All options available to top-level attributes are available to nested attributes, eg: naming, coercions, and validations
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
class ConfigureServer < CMDx::Task
|
177
|
+
# Required parent with required children
|
178
|
+
required :network_config do
|
179
|
+
required :hostname, :port, :protocol, :subnet
|
180
|
+
optional :load_balancer
|
181
|
+
attribute :firewall_rules
|
182
|
+
end
|
183
|
+
|
184
|
+
# Optional parent with conditional children
|
185
|
+
optional :ssl_config do
|
186
|
+
required :certificate_path, :private_key # Only required if ssl_config provided
|
187
|
+
optional :enable_http2, prefix: true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Multi-level nesting
|
191
|
+
attribute :monitoring do
|
192
|
+
required :provider
|
193
|
+
|
194
|
+
optional :alerting do
|
195
|
+
required :threshold_percentage
|
196
|
+
optional :notification_channel
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def work
|
201
|
+
network_config #=> { hostname: "api.company.com" ... }
|
202
|
+
hostname #=> "api.company.com"
|
203
|
+
load_balancer #=> nil
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
ConfigureServer.execute(
|
208
|
+
server_id: "srv-001",
|
209
|
+
network_config: {
|
210
|
+
hostname: "api.company.com",
|
211
|
+
port: 443,
|
212
|
+
protocol: "https",
|
213
|
+
subnet: "10.0.1.0/24",
|
214
|
+
firewall_rules: "allow_web_traffic"
|
215
|
+
},
|
216
|
+
monitoring: {
|
217
|
+
provider: "datadog",
|
218
|
+
alerting: {
|
219
|
+
threshold_percentage: 85.0,
|
220
|
+
notification_channel: "slack"
|
221
|
+
}
|
222
|
+
}
|
223
|
+
)
|
224
|
+
```
|
225
|
+
|
226
|
+
> [!IMPORTANT]
|
227
|
+
> Child attributes are only required when their parent attribute is provided, enabling flexible optional structures.
|
228
|
+
|
229
|
+
## Error Handling
|
230
|
+
|
231
|
+
Attribute validation failures result in structured error information with details about each failed attribute.
|
232
|
+
|
233
|
+
> [!NOTE]
|
234
|
+
> Nested attributes are only ever evaluated when the parent attribute is available and valid.
|
235
|
+
|
236
|
+
```ruby
|
237
|
+
class ConfigureServer < CMDx::Task
|
238
|
+
required :server_id, :environment
|
239
|
+
required :network_config do
|
240
|
+
required :hostname, :port
|
241
|
+
end
|
242
|
+
|
243
|
+
def work
|
244
|
+
# Your logic here...
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
# Missing required top-level attributes
|
249
|
+
result = ConfigureServer.execute(server_id: "srv-001")
|
250
|
+
|
251
|
+
result.state #=> "interrupted"
|
252
|
+
result.status #=> "failed"
|
253
|
+
result.reason #=> "environment is required. network_config is required."
|
254
|
+
result.metadata #=> {
|
255
|
+
# messages: {
|
256
|
+
# environment: ["is required"],
|
257
|
+
# network_config: ["is required"]
|
258
|
+
# }
|
259
|
+
# }
|
260
|
+
|
261
|
+
# Missing required nested attributes
|
262
|
+
result = ConfigureServer.execute(
|
263
|
+
server_id: "srv-001",
|
264
|
+
environment: "production",
|
265
|
+
network_config: { hostname: "api.company.com" } # Missing port
|
266
|
+
)
|
267
|
+
|
268
|
+
result.state #=> "interrupted"
|
269
|
+
result.status #=> "failed"
|
270
|
+
result.reason #=> "port is required."
|
271
|
+
result.metadata #=> {
|
272
|
+
# messages: {
|
273
|
+
# port: ["is required"]
|
274
|
+
# }
|
275
|
+
# }
|
276
|
+
```
|
277
|
+
|
278
|
+
---
|
279
|
+
|
280
|
+
- **Prev:** [Outcomes - States](../outcomes/states.md)
|
281
|
+
- **Next:** [Attributes - Naming](naming.md)
|