cmdx 1.1.2 → 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.
Files changed (192) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +4 -1
  4. data/.cursor/prompts/llms.md +20 -0
  5. data/.cursor/prompts/rspec.md +4 -1
  6. data/.cursor/prompts/yardoc.md +3 -2
  7. data/.cursor/rules/cursor-instructions.mdc +56 -1
  8. data/.irbrc +6 -0
  9. data/.rubocop.yml +29 -18
  10. data/CHANGELOG.md +5 -133
  11. data/LLM.md +3317 -0
  12. data/README.md +68 -44
  13. data/docs/attributes/coercions.md +162 -0
  14. data/docs/attributes/defaults.md +90 -0
  15. data/docs/attributes/definitions.md +281 -0
  16. data/docs/attributes/naming.md +78 -0
  17. data/docs/attributes/validations.md +309 -0
  18. data/docs/basics/chain.md +56 -249
  19. data/docs/basics/context.md +56 -289
  20. data/docs/basics/execution.md +114 -0
  21. data/docs/basics/setup.md +37 -334
  22. data/docs/callbacks.md +89 -467
  23. data/docs/deprecation.md +91 -174
  24. data/docs/getting_started.md +212 -202
  25. data/docs/internationalization.md +11 -647
  26. data/docs/interruptions/exceptions.md +23 -198
  27. data/docs/interruptions/faults.md +71 -151
  28. data/docs/interruptions/halt.md +109 -186
  29. data/docs/logging.md +44 -256
  30. data/docs/middlewares.md +113 -426
  31. data/docs/outcomes/result.md +81 -228
  32. data/docs/outcomes/states.md +33 -221
  33. data/docs/outcomes/statuses.md +21 -311
  34. data/docs/tips_and_tricks.md +120 -70
  35. data/docs/workflows.md +99 -283
  36. data/lib/cmdx/.DS_Store +0 -0
  37. data/lib/cmdx/attribute.rb +229 -0
  38. data/lib/cmdx/attribute_registry.rb +94 -0
  39. data/lib/cmdx/attribute_value.rb +193 -0
  40. data/lib/cmdx/callback_registry.rb +69 -77
  41. data/lib/cmdx/chain.rb +56 -73
  42. data/lib/cmdx/coercion_registry.rb +52 -68
  43. data/lib/cmdx/coercions/array.rb +19 -18
  44. data/lib/cmdx/coercions/big_decimal.rb +20 -24
  45. data/lib/cmdx/coercions/boolean.rb +26 -25
  46. data/lib/cmdx/coercions/complex.rb +21 -22
  47. data/lib/cmdx/coercions/date.rb +25 -23
  48. data/lib/cmdx/coercions/date_time.rb +24 -25
  49. data/lib/cmdx/coercions/float.rb +25 -22
  50. data/lib/cmdx/coercions/hash.rb +31 -32
  51. data/lib/cmdx/coercions/integer.rb +30 -24
  52. data/lib/cmdx/coercions/rational.rb +29 -24
  53. data/lib/cmdx/coercions/string.rb +19 -22
  54. data/lib/cmdx/coercions/symbol.rb +37 -0
  55. data/lib/cmdx/coercions/time.rb +26 -25
  56. data/lib/cmdx/configuration.rb +49 -108
  57. data/lib/cmdx/context.rb +222 -44
  58. data/lib/cmdx/deprecator.rb +61 -0
  59. data/lib/cmdx/errors.rb +42 -252
  60. data/lib/cmdx/exceptions.rb +39 -0
  61. data/lib/cmdx/faults.rb +78 -39
  62. data/lib/cmdx/freezer.rb +51 -0
  63. data/lib/cmdx/identifier.rb +30 -0
  64. data/lib/cmdx/locale.rb +52 -0
  65. data/lib/cmdx/log_formatters/json.rb +21 -22
  66. data/lib/cmdx/log_formatters/key_value.rb +20 -22
  67. data/lib/cmdx/log_formatters/line.rb +15 -22
  68. data/lib/cmdx/log_formatters/logstash.rb +22 -23
  69. data/lib/cmdx/log_formatters/raw.rb +16 -22
  70. data/lib/cmdx/middleware_registry.rb +70 -74
  71. data/lib/cmdx/middlewares/correlate.rb +90 -54
  72. data/lib/cmdx/middlewares/runtime.rb +58 -0
  73. data/lib/cmdx/middlewares/timeout.rb +48 -68
  74. data/lib/cmdx/railtie.rb +12 -45
  75. data/lib/cmdx/result.rb +229 -314
  76. data/lib/cmdx/task.rb +194 -366
  77. data/lib/cmdx/utils/call.rb +49 -0
  78. data/lib/cmdx/utils/condition.rb +71 -0
  79. data/lib/cmdx/utils/format.rb +61 -0
  80. data/lib/cmdx/validator_registry.rb +63 -72
  81. data/lib/cmdx/validators/exclusion.rb +38 -67
  82. data/lib/cmdx/validators/format.rb +48 -49
  83. data/lib/cmdx/validators/inclusion.rb +43 -74
  84. data/lib/cmdx/validators/length.rb +91 -154
  85. data/lib/cmdx/validators/numeric.rb +87 -162
  86. data/lib/cmdx/validators/presence.rb +37 -50
  87. data/lib/cmdx/version.rb +1 -1
  88. data/lib/cmdx/worker.rb +178 -0
  89. data/lib/cmdx/workflow.rb +85 -81
  90. data/lib/cmdx.rb +19 -13
  91. data/lib/generators/cmdx/install_generator.rb +14 -13
  92. data/lib/generators/cmdx/task_generator.rb +25 -50
  93. data/lib/generators/cmdx/templates/install.rb +11 -46
  94. data/lib/generators/cmdx/templates/task.rb.tt +3 -2
  95. data/lib/locales/en.yml +18 -4
  96. data/src/cmdx-logo.png +0 -0
  97. metadata +32 -116
  98. data/docs/ai_prompts.md +0 -393
  99. data/docs/basics/call.md +0 -317
  100. data/docs/configuration.md +0 -344
  101. data/docs/parameters/coercions.md +0 -396
  102. data/docs/parameters/defaults.md +0 -335
  103. data/docs/parameters/definitions.md +0 -446
  104. data/docs/parameters/namespacing.md +0 -378
  105. data/docs/parameters/validations.md +0 -405
  106. data/docs/testing.md +0 -553
  107. data/lib/cmdx/callback.rb +0 -53
  108. data/lib/cmdx/chain_inspector.rb +0 -56
  109. data/lib/cmdx/chain_serializer.rb +0 -63
  110. data/lib/cmdx/coercion.rb +0 -57
  111. data/lib/cmdx/coercions/virtual.rb +0 -29
  112. data/lib/cmdx/core_ext/hash.rb +0 -83
  113. data/lib/cmdx/core_ext/module.rb +0 -98
  114. data/lib/cmdx/core_ext/object.rb +0 -125
  115. data/lib/cmdx/correlator.rb +0 -122
  116. data/lib/cmdx/error.rb +0 -67
  117. data/lib/cmdx/fault.rb +0 -140
  118. data/lib/cmdx/immutator.rb +0 -52
  119. data/lib/cmdx/lazy_struct.rb +0 -246
  120. data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
  121. data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
  122. data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
  123. data/lib/cmdx/logger.rb +0 -49
  124. data/lib/cmdx/logger_ansi.rb +0 -68
  125. data/lib/cmdx/logger_serializer.rb +0 -116
  126. data/lib/cmdx/middleware.rb +0 -70
  127. data/lib/cmdx/parameter.rb +0 -312
  128. data/lib/cmdx/parameter_evaluator.rb +0 -231
  129. data/lib/cmdx/parameter_inspector.rb +0 -66
  130. data/lib/cmdx/parameter_registry.rb +0 -106
  131. data/lib/cmdx/parameter_serializer.rb +0 -59
  132. data/lib/cmdx/result_ansi.rb +0 -71
  133. data/lib/cmdx/result_inspector.rb +0 -71
  134. data/lib/cmdx/result_logger.rb +0 -59
  135. data/lib/cmdx/result_serializer.rb +0 -104
  136. data/lib/cmdx/rspec/matchers.rb +0 -28
  137. data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
  138. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
  139. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
  140. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
  141. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
  142. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
  143. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
  144. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
  145. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
  146. data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
  147. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
  148. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
  149. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
  150. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
  151. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
  152. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
  153. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
  154. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
  155. data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
  156. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
  157. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
  158. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
  159. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
  160. data/lib/cmdx/task_deprecator.rb +0 -58
  161. data/lib/cmdx/task_processor.rb +0 -246
  162. data/lib/cmdx/task_serializer.rb +0 -57
  163. data/lib/cmdx/utils/ansi_color.rb +0 -73
  164. data/lib/cmdx/utils/log_timestamp.rb +0 -36
  165. data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
  166. data/lib/cmdx/utils/name_affix.rb +0 -52
  167. data/lib/cmdx/validator.rb +0 -57
  168. data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
  169. data/lib/generators/cmdx/workflow_generator.rb +0 -84
  170. data/lib/locales/ar.yml +0 -35
  171. data/lib/locales/cs.yml +0 -35
  172. data/lib/locales/da.yml +0 -35
  173. data/lib/locales/de.yml +0 -35
  174. data/lib/locales/el.yml +0 -35
  175. data/lib/locales/es.yml +0 -35
  176. data/lib/locales/fi.yml +0 -35
  177. data/lib/locales/fr.yml +0 -35
  178. data/lib/locales/he.yml +0 -35
  179. data/lib/locales/hi.yml +0 -35
  180. data/lib/locales/it.yml +0 -35
  181. data/lib/locales/ja.yml +0 -35
  182. data/lib/locales/ko.yml +0 -35
  183. data/lib/locales/nl.yml +0 -35
  184. data/lib/locales/no.yml +0 -35
  185. data/lib/locales/pl.yml +0 -35
  186. data/lib/locales/pt.yml +0 -35
  187. data/lib/locales/ru.yml +0 -35
  188. data/lib/locales/sv.yml +0 -35
  189. data/lib/locales/th.yml +0 -35
  190. data/lib/locales/tr.yml +0 -35
  191. data/lib/locales/vi.yml +0 -35
  192. data/lib/locales/zh.yml +0 -35
data/README.md CHANGED
@@ -1,16 +1,25 @@
1
- # CMDx
1
+ <p align="center">
2
+ <img src="./src/cmdx-logo.png" width="200" alt="CMDx Logo">
3
+ </p>
2
4
 
3
- [![forthebadge](http://forthebadge.com/images/badges/made-with-ruby.svg)](http://forthebadge.com)
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
- [![Gem Version](https://badge.fury.io/rb/cmdx.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/cmdx)
6
- [![CI](https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg)](https://github.com/drexed/cmdx/actions/workflows/ci.yml)
7
- [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=shields)](http://makeapullrequest.com)
11
+ # CMDx
8
12
 
9
- `CMDx` is a Ruby framework for building maintainable, observable business logic through composable command objects. Design robust workflows with automatic parameter validation, structured error handling, comprehensive logging, and intelligent execution flow control that scales from simple tasks to complex multi-step processes.
13
+ CMDx is a framework for building maintainable business processes. It simplifies building task objects by offering integrated:
10
14
 
11
- ## Installation
15
+ - Flow controls
16
+ - Composable workflows
17
+ - Comprehensive logging
18
+ - Attribute definition
19
+ - Validations and coercions
20
+ - And much more...
12
21
 
13
- **Prerequisites:** This gem requires Ruby `>= 3.1` to be installed.
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
- class SendWelcomeEmailTask < CMDx::Task
34
- use :middleware, CMDx::Middlewares::Timeout, seconds: 5
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 :track_email_delivery!
48
+ on_success :track_analysis_completion!
37
49
 
38
- required :user_id, type: :integer, numeric: { min: 1 }
39
- optional :template, type: :string, default: "welcome"
50
+ required :dataset_id, type: :integer, numeric: { min: 1 }
51
+ optional :analysis_type, default: "standard"
40
52
 
41
- def call
42
- if user.nil?
43
- fail!(reason: "User not found", code: 404)
44
- elsif user.unconfirmed?
45
- skip!(reason: "Email not verified")
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
- response = UserMailer.welcome(user, template).deliver_now
48
- context.message_id = response.message_id
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 user
55
- @user ||= User.find_by(id: user_id)
66
+ def dataset
67
+ @dataset ||= Dataset.find_by(id: dataset_id)
56
68
  end
57
69
 
58
- def track_email_delivery!
59
- user.touch(:welcomed_at)
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
- result = SendWelcomeEmailTask.call(user_id: 123, template: "premium_welcome")
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 "Welcome email sent <message_id: #{result.context.message_id}>"
85
+ puts "Metrics analyzed at #{result.context.analyzed_at}"
69
86
  elsif result.skipped?
70
- puts "Skipped: #{result.metadata[:reason]}"
87
+ puts "Skipping analyzation due to: #{result.reason}"
71
88
  elsif result.failed?
72
- puts "Failed: #{result.metadata[:reason]} with code: #{result.metadata[:code]}"
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
- - [Call](docs/basics/call.md)
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](#results)
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
- - [AI Prompts](docs/ai_prompts.md)
107
- - [Tips & Tricks](docs/tips_and_tricks.md)
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)