cmdx 1.9.0 → 1.10.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/llms.md +3 -13
  3. data/.cursor/prompts/yardoc.md +1 -0
  4. data/CHANGELOG.md +16 -0
  5. data/LLM.md +436 -374
  6. data/README.md +7 -2
  7. data/docs/basics/setup.md +17 -0
  8. data/docs/callbacks.md +1 -1
  9. data/docs/getting_started.md +22 -2
  10. data/docs/index.md +13 -1
  11. data/docs/retries.md +121 -0
  12. data/docs/tips_and_tricks.md +2 -1
  13. data/examples/stoplight_circuit_breaker.md +36 -0
  14. data/lib/cmdx/attribute.rb +82 -1
  15. data/lib/cmdx/attribute_registry.rb +20 -0
  16. data/lib/cmdx/attribute_value.rb +25 -0
  17. data/lib/cmdx/callback_registry.rb +19 -0
  18. data/lib/cmdx/chain.rb +34 -1
  19. data/lib/cmdx/coercion_registry.rb +18 -0
  20. data/lib/cmdx/coercions/array.rb +2 -0
  21. data/lib/cmdx/coercions/big_decimal.rb +3 -0
  22. data/lib/cmdx/coercions/boolean.rb +5 -0
  23. data/lib/cmdx/coercions/complex.rb +2 -0
  24. data/lib/cmdx/coercions/date.rb +4 -0
  25. data/lib/cmdx/coercions/date_time.rb +5 -0
  26. data/lib/cmdx/coercions/float.rb +2 -0
  27. data/lib/cmdx/coercions/hash.rb +2 -0
  28. data/lib/cmdx/coercions/integer.rb +2 -0
  29. data/lib/cmdx/coercions/rational.rb +2 -0
  30. data/lib/cmdx/coercions/string.rb +2 -0
  31. data/lib/cmdx/coercions/symbol.rb +2 -0
  32. data/lib/cmdx/coercions/time.rb +5 -0
  33. data/lib/cmdx/configuration.rb +126 -3
  34. data/lib/cmdx/context.rb +36 -0
  35. data/lib/cmdx/deprecator.rb +3 -0
  36. data/lib/cmdx/errors.rb +22 -0
  37. data/lib/cmdx/executor.rb +71 -11
  38. data/lib/cmdx/faults.rb +14 -0
  39. data/lib/cmdx/identifier.rb +2 -0
  40. data/lib/cmdx/locale.rb +3 -0
  41. data/lib/cmdx/log_formatters/json.rb +2 -0
  42. data/lib/cmdx/log_formatters/key_value.rb +2 -0
  43. data/lib/cmdx/log_formatters/line.rb +2 -0
  44. data/lib/cmdx/log_formatters/logstash.rb +2 -0
  45. data/lib/cmdx/log_formatters/raw.rb +2 -0
  46. data/lib/cmdx/middleware_registry.rb +20 -0
  47. data/lib/cmdx/middlewares/correlate.rb +11 -0
  48. data/lib/cmdx/middlewares/runtime.rb +4 -0
  49. data/lib/cmdx/middlewares/timeout.rb +4 -0
  50. data/lib/cmdx/pipeline.rb +20 -1
  51. data/lib/cmdx/railtie.rb +4 -0
  52. data/lib/cmdx/result.rb +123 -1
  53. data/lib/cmdx/task.rb +91 -1
  54. data/lib/cmdx/utils/call.rb +2 -0
  55. data/lib/cmdx/utils/condition.rb +3 -0
  56. data/lib/cmdx/utils/format.rb +5 -0
  57. data/lib/cmdx/validator_registry.rb +18 -0
  58. data/lib/cmdx/validators/exclusion.rb +2 -0
  59. data/lib/cmdx/validators/format.rb +2 -0
  60. data/lib/cmdx/validators/inclusion.rb +2 -0
  61. data/lib/cmdx/validators/length.rb +14 -0
  62. data/lib/cmdx/validators/numeric.rb +14 -0
  63. data/lib/cmdx/validators/presence.rb +2 -0
  64. data/lib/cmdx/version.rb +4 -1
  65. data/lib/cmdx/workflow.rb +10 -0
  66. data/lib/cmdx.rb +8 -0
  67. data/lib/generators/cmdx/locale_generator.rb +0 -1
  68. data/mkdocs.yml +3 -1
  69. metadata +3 -1
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Build business logic that’s powerful, predictable, and maintainable.
8
8
 
9
- [Documentation](https://drexed.github.io/cmdx) · [Changelog](./CHANGELOG.md) · [Report Bug](https://github.com/drexed/cmdx/issues) · [Request Feature](https://github.com/drexed/cmdx/issues)
9
+ [Documentation](https://drexed.github.io/cmdx) · [Changelog](./CHANGELOG.md) · [Report Bug](https://github.com/drexed/cmdx/issues) · [Request Feature](https://github.com/drexed/cmdx/issues) · [LLM.md](https://raw.githubusercontent.com/drexed/cmdx/refs/heads/main/LLM.md)
10
10
 
11
11
  <img alt="Version" src="https://img.shields.io/gem/v/cmdx">
12
12
  <img alt="Build" src="https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg">
@@ -17,6 +17,9 @@
17
17
 
18
18
  Say goodbye to messy service objects. CMDx helps you design business logic with clarity and consistency—build faster, debug easier, and ship with confidence.
19
19
 
20
+ > [!NOTE]
21
+ > Documentation reflects the latest code on `main`. For version-specific documentation, please refer to the `docs/` directory within that version's tag.
22
+
20
23
  ## Requirements
21
24
 
22
25
  - Ruby: MRI 3.1+ or JRuby 9.4+.
@@ -105,6 +108,8 @@ I, [2022-07-17T18:43:15.000000 #3784] INFO -- CMDx:
105
108
  index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="AnalyzeMetrics" state="complete" status="success" metadata={runtime: 187}
106
109
  ```
107
110
 
111
+ Ready to dive in? Check out the [Getting Started](https://drexed.github.io/cmdx/getting_started/) guide to learn more.
112
+
108
113
  ## Ecosystem
109
114
 
110
115
  - [cmdx-rspec](https://github.com/drexed/cmdx-rspec) - RSpec test matchers
@@ -116,7 +121,7 @@ For backwards compatibility of certain functionality:
116
121
 
117
122
  ## Contributing
118
123
 
119
- Bug reports and pull requests are welcome at https://github.com/drexed/cmdx. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](CODE_OF_CONDUCT.md).
124
+ Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](CODE_OF_CONDUCT.md).
120
125
 
121
126
  ## License
122
127
 
data/docs/basics/setup.md CHANGED
@@ -24,6 +24,22 @@ end
24
24
  IncompleteTask.execute #=> raises CMDx::UndefinedMethodError
25
25
  ```
26
26
 
27
+ ## Rollback
28
+
29
+ Undo any operations linked to the given status, helping to restore a pristine state.
30
+
31
+ ```ruby
32
+ class ValidateDocument < CMDx::Task
33
+ def work
34
+ # Your logic here...
35
+ end
36
+
37
+ def rollback
38
+ # Your undo logic...
39
+ end
40
+ end
41
+ ```
42
+
27
43
  ## Inheritance
28
44
 
29
45
  Share configuration across tasks using inheritance:
@@ -65,3 +81,4 @@ Tasks follow a predictable execution pattern:
65
81
  | **Execution** | `executing` | `success`/`failed`/`skipped` | `work` method runs |
66
82
  | **Completion** | `executed` | `success`/`failed`/`skipped` | Result finalized |
67
83
  | **Freezing** | `executed` | `success`/`failed`/`skipped` | Task becomes immutable |
84
+ | **Rollback** | `executed` | `failed`/`skipped` | Work undone |
data/docs/callbacks.md CHANGED
@@ -73,7 +73,7 @@ end
73
73
 
74
74
  ### Class or Module
75
75
 
76
- Implement reusable callback logic in dedicated classes:
76
+ Implement reusable callback logic in dedicated modules and classes:
77
77
 
78
78
  ```ruby
79
79
  class BookingConfirmationCallback
@@ -78,6 +78,16 @@ CMDx.configure do |config|
78
78
  end
79
79
  ```
80
80
 
81
+ ### Rollback
82
+
83
+ Control when a `rollback` of task execution is called.
84
+
85
+ ```ruby
86
+ CMDx.configure do |config|
87
+ config.rollback_on = ["failed"] # String or Array[String]
88
+ end
89
+ ```
90
+
81
91
  ### Backtraces
82
92
 
83
93
  Enable detailed backtraces for non-fault exceptions to improve debugging. Optionally clean up stack traces to remove framework noise.
@@ -258,7 +268,8 @@ class GenerateInvoice < CMDx::Task
258
268
  deprecated: true, # Task deprecations
259
269
  retries: 3, # Non-fault exception retries
260
270
  retry_on: [External::ApiError], # List of exceptions to retry on
261
- retry_jitter: 1 # Space between retry iteration, eg: current retry num + 1
271
+ retry_jitter: 1, # Space between retry iteration, eg: current retry num + 1
272
+ rollback_on: ["failed", "skipped"], # Rollback on override
262
273
  )
263
274
 
264
275
  def work
@@ -269,7 +280,7 @@ end
269
280
 
270
281
  !!! warning "Important"
271
282
 
272
- Retries reuse the same context. By default, all `StandardError` exceptions are retried unless you specify `retry_on`.
283
+ Retries reuse the same context. By default, all `StandardError` exceptions (including faults) are retried unless you specify `retry_on` option for specific matches.
273
284
 
274
285
  ### Registrations
275
286
 
@@ -367,3 +378,12 @@ end
367
378
  !!! tip
368
379
 
369
380
  Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument`
381
+
382
+ ## Type safety
383
+
384
+ CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support.
385
+
386
+ - **Type checking** — Catch type errors before runtime using tools like Steep or TypeProf
387
+ - **Better IDE support** — Enhanced autocomplete, navigation, and inline documentation
388
+ - **Self-documenting code** — Clear method signatures and return types
389
+ - **Refactoring confidence** — Type-aware refactoring reduces bugs
data/docs/index.md CHANGED
@@ -6,8 +6,20 @@ Build business logic that's powerful, predictable, and maintainable.
6
6
  [![Build](https://github.com/drexed/cmdx/actions/workflows/ci.yml/badge.svg)](https://github.com/drexed/cmdx/actions/workflows/ci.yml)
7
7
  [![License](https://img.shields.io/github/license/drexed/cmdx)](https://github.com/drexed/cmdx/blob/main/LICENSE.txt)
8
8
 
9
+ ---
10
+
9
11
  Say goodbye to messy service objects. CMDx helps you design business logic with clarity and consistency—build faster, debug easier, and ship with confidence.
10
12
 
13
+ !!! note
14
+
15
+ Documentation reflects the latest code on `main`. For version-specific documentation, please refer to the `docs/` directory within that version's tag.
16
+
17
+ ## Requirements
18
+
19
+ - Ruby: MRI 3.1+ or JRuby 9.4+.
20
+
21
+ CMDx works with any Ruby framework. Rails support is built-in, but it's framework-agnostic at its core.
22
+
11
23
  ## Installation
12
24
 
13
25
  ```sh
@@ -113,7 +125,7 @@ For backwards compatibility of certain functionality:
113
125
 
114
126
  ## Contributing
115
127
 
116
- Bug reports and pull requests are welcome at https://github.com/drexed/cmdx. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](CODE_OF_CONDUCT.md).
128
+ Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx>. We're committed to fostering a welcoming, collaborative community. Please follow our [code of conduct](CODE_OF_CONDUCT.md).
117
129
 
118
130
  ## License
119
131
 
data/docs/retries.md ADDED
@@ -0,0 +1,121 @@
1
+ # Retries
2
+
3
+ CMDx provides automatic retry functionality for tasks that encounter transient failures. This is essential for handling temporary issues like network timeouts, rate limits, or database locks without manual intervention.
4
+
5
+ ## Basic Usage
6
+
7
+ Configure retries upto n attempts without any delay.
8
+
9
+ ```ruby
10
+ class FetchExternalData < CMDx::Task
11
+ settings retries: 3
12
+
13
+ def work
14
+ response = HTTParty.get("https://api.example.com/data")
15
+ context.data = response.parsed_response
16
+ end
17
+ end
18
+ ```
19
+
20
+ When an exception occurs during execution, CMDx automatically retries up to the configured limit.
21
+
22
+ ## Selective Retries
23
+
24
+ By default, CMDx retries on `StandardError` and its subclasses. Narrow this to specific exception types:
25
+
26
+ ```ruby
27
+ class ProcessPayment < CMDx::Task
28
+ settings retries: 5, retry_on: [Stripe::RateLimitError, Net::ReadTimeout]
29
+
30
+ def work
31
+ # Your logic here...
32
+ end
33
+ end
34
+ ```
35
+
36
+ !!! warning "Important"
37
+
38
+ Only exceptions matching the `retry_on` configuration will trigger retries. Uncaught exceptions immediately fail the task.
39
+
40
+ ## Retry Jitter
41
+
42
+ Add delays between retry attempts to avoid overwhelming external services or to implement exponential backoff strategies.
43
+
44
+ ### Fixed Value
45
+
46
+ Use a numeric value to calculate linear delay (`jitter * current_retry`):
47
+
48
+ ```ruby
49
+ class ImportRecords < CMDx::Task
50
+ settings retries: 3, retry_jitter: 0.5
51
+
52
+ def work
53
+ # Delays: 0s, 0.5s (retry 1), 1.0s (retry 2), 1.5s (retry 3)
54
+ context.records = ExternalAPI.fetch_records
55
+ end
56
+ end
57
+ ```
58
+
59
+ ### Symbol References
60
+
61
+ Define an instance method for custom delay logic:
62
+
63
+ ```ruby
64
+ class SyncInventory < CMDx::Task
65
+ settings retries: 5, retry_jitter: :exponential_backoff
66
+
67
+ def work
68
+ context.inventory = InventoryAPI.sync
69
+ end
70
+
71
+ private
72
+
73
+ def exponential_backoff(current_retry)
74
+ 2 ** current_retry # 2s, 4s, 8s, 16s, 32s
75
+ end
76
+ end
77
+ ```
78
+
79
+ ### Proc or Lambda
80
+
81
+ Pass a proc for inline delay calculations:
82
+
83
+ ```ruby
84
+ class PollJobStatus < CMDx::Task
85
+ # Proc
86
+ settings retries: 10, retry_jitter: proc { |retry_count| [retry_count * 0.5, 5.0].min }
87
+
88
+ # Lambda
89
+ settings retries: 10, retry_jitter: ->(retry_count) { [retry_count * 0.5, 5.0].min }
90
+
91
+ def work
92
+ # Delays: 0.5s, 1.0s, 1.5s, 2.0s, 2.5s, 3.0s, 3.5s, 4.0s, 4.5s, 5.0s (capped)
93
+ context.status = JobAPI.check_status(context.job_id)
94
+ end
95
+ end
96
+ ```
97
+
98
+ ### Class or Module
99
+
100
+ Implement reusable delay logic in dedicated modules and classes:
101
+
102
+ ```ruby
103
+ class ExponentialBackoff
104
+ def call(task, retry_count)
105
+ base_delay = task.context.base_delay || 1.0
106
+ [base_delay * (2 ** retry_count), 60.0].min
107
+ end
108
+ end
109
+
110
+ class FetchUserProfile < CMDx::Task
111
+ # Class or Module
112
+ settings retries: 4, retry_jitter: ExponentialBackoff
113
+
114
+ # Instance
115
+ settings retries: 4, retry_jitter: ExponentialBackoff.new
116
+
117
+ def work
118
+ # Your logic here...
119
+ end
120
+ end
121
+ ```
@@ -145,7 +145,8 @@ class ConfigureCompany < CMDx::Task
145
145
  end
146
146
  ```
147
147
 
148
- ## Advanced Examples
148
+ ## More Examples
149
149
 
150
150
  - [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
151
151
  - [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
152
+ - [Stoplight Circuit Breaker](https://github.com/drexed/cmdx/blob/main/examples/stoplight_circuit_breaker.md)
@@ -0,0 +1,36 @@
1
+ # Stoplight Circuit Breaker
2
+
3
+ Integrate circuit breakers to protect external service calls and prevent cascading failures when dependencies are unavailable.
4
+
5
+ <https://github.com/bolshakov/stoplight>
6
+
7
+ ### Setup
8
+
9
+ ```ruby
10
+ # lib/cmdx_stoplight_middleware.rb
11
+ class CmdxStoplightMiddleware
12
+ def self.call(task, **options, &)
13
+ light = Stoplight(options[:name] || task.class.name, **options)
14
+ light.run(&)
15
+ rescue Stoplight::Error::RedLight => e
16
+ task.result.tap { |r| r.fail!("[#{e.class}] #{e.message}", cause: e) }
17
+ end
18
+ end
19
+ ```
20
+
21
+ ### Usage
22
+
23
+ ```ruby
24
+ class MyTask < CMDx::Task
25
+ # With default options
26
+ register :middleware, CmdxStoplightMiddleware
27
+
28
+ # With stoplight options
29
+ register :middleware, CmdxStoplightMiddleware, cool_off_time: 10
30
+
31
+ def work
32
+ # Do work...
33
+ end
34
+
35
+ end
36
+ ```
@@ -6,14 +6,71 @@ module CMDx
6
6
  # They can be nested to create complex hierarchical data structures.
7
7
  class Attribute
8
8
 
9
+ # @rbs AFFIX: Proc
9
10
  AFFIX = proc do |value, &block|
10
11
  value == true ? block.call : value
11
12
  end.freeze
12
13
  private_constant :AFFIX
13
14
 
15
+ # Returns the task instance associated with this attribute.
16
+ #
17
+ # @return [CMDx::Task] The task instance
18
+ #
19
+ # @example
20
+ # attribute.task.context[:user_id] # => 42
21
+ #
22
+ # @rbs @task: Task
14
23
  attr_accessor :task
15
24
 
16
- attr_reader :name, :options, :children, :parent, :types
25
+ # Returns the name of this attribute.
26
+ #
27
+ # @return [Symbol] The attribute name
28
+ #
29
+ # @example
30
+ # attribute.name # => :user_id
31
+ #
32
+ # @rbs @name: Symbol
33
+ attr_reader :name
34
+
35
+ # Returns the configuration options for this attribute.
36
+ #
37
+ # @return [Hash{Symbol => Object}] Configuration options hash
38
+ #
39
+ # @example
40
+ # attribute.options # => { required: true, default: 0 }
41
+ #
42
+ # @rbs @options: Hash[Symbol, untyped]
43
+ attr_reader :options
44
+
45
+ # Returns the child attributes for nested structures.
46
+ #
47
+ # @return [Array<Attribute>] Array of child attributes
48
+ #
49
+ # @example
50
+ # attribute.children # => [#<Attribute @name=:street>, #<Attribute @name=:city>]
51
+ #
52
+ # @rbs @children: Array[Attribute]
53
+ attr_reader :children
54
+
55
+ # Returns the parent attribute if this is a nested attribute.
56
+ #
57
+ # @return [Attribute, nil] The parent attribute, or nil if root-level
58
+ #
59
+ # @example
60
+ # attribute.parent # => #<Attribute @name=:address>
61
+ #
62
+ # @rbs @parent: (Attribute | nil)
63
+ attr_reader :parent
64
+
65
+ # Returns the expected type(s) for this attribute's value.
66
+ #
67
+ # @return [Array<Class>] Array of expected type classes
68
+ #
69
+ # @example
70
+ # attribute.types # => [Integer, String]
71
+ #
72
+ # @rbs @types: Array[Class]
73
+ attr_reader :types
17
74
 
18
75
  # Creates a new attribute with the specified name and configuration.
19
76
  #
@@ -35,6 +92,8 @@ module CMDx
35
92
  # required :name, types: String
36
93
  # optional :email, types: String
37
94
  # end
95
+ #
96
+ # @rbs ((Symbol | String) name, ?Hash[Symbol, untyped] options) ?{ () -> void } -> void
38
97
  def initialize(name, options = {}, &)
39
98
  @parent = options.delete(:parent)
40
99
  @required = options.delete(:required) || false
@@ -62,6 +121,8 @@ module CMDx
62
121
  #
63
122
  # @example
64
123
  # Attribute.build(:first_name, :last_name, required: true, types: String)
124
+ #
125
+ # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
65
126
  def build(*names, **options, &)
66
127
  if names.none?
67
128
  raise ArgumentError, "no attributes given"
@@ -83,6 +144,8 @@ module CMDx
83
144
  #
84
145
  # @example
85
146
  # Attribute.optional(:description, :tags, types: String)
147
+ #
148
+ # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
86
149
  def optional(*names, **options, &)
87
150
  build(*names, **options.merge(required: false), &)
88
151
  end
@@ -98,6 +161,8 @@ module CMDx
98
161
  #
99
162
  # @example
100
163
  # Attribute.required(:id, :name, types: [Integer, String])
164
+ #
165
+ # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
101
166
  def required(*names, **options, &)
102
167
  build(*names, **options.merge(required: true), &)
103
168
  end
@@ -110,6 +175,8 @@ module CMDx
110
175
  #
111
176
  # @example
112
177
  # attribute.required? # => true
178
+ #
179
+ # @rbs () -> bool
113
180
  def required?
114
181
  !!@required
115
182
  end
@@ -120,6 +187,8 @@ module CMDx
120
187
  #
121
188
  # @example
122
189
  # attribute.source # => :context
190
+ #
191
+ # @rbs () -> untyped
123
192
  def source
124
193
  @source ||= parent&.method_name || begin
125
194
  value = options[:source]
@@ -140,6 +209,8 @@ module CMDx
140
209
  #
141
210
  # @example
142
211
  # attribute.method_name # => :user_name
212
+ #
213
+ # @rbs () -> Symbol
143
214
  def method_name
144
215
  @method_name ||= options[:as] || begin
145
216
  prefix = AFFIX.call(options[:prefix]) { "#{source}_" }
@@ -150,6 +221,8 @@ module CMDx
150
221
  end
151
222
 
152
223
  # Defines and verifies the entire attribute tree including nested children.
224
+ #
225
+ # @rbs () -> void
153
226
  def define_and_verify_tree
154
227
  define_and_verify
155
228
 
@@ -172,6 +245,8 @@ module CMDx
172
245
  #
173
246
  # @example
174
247
  # attributes :street, :city, :zip, types: String
248
+ #
249
+ # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
175
250
  def attributes(*names, **options, &)
176
251
  attrs = self.class.build(*names, **options.merge(parent: self), &)
177
252
  children.concat(attrs)
@@ -189,6 +264,8 @@ module CMDx
189
264
  #
190
265
  # @example
191
266
  # optional :middle_name, :nickname, types: String
267
+ #
268
+ # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
192
269
  def optional(*names, **options, &)
193
270
  attributes(*names, **options.merge(required: false), &)
194
271
  end
@@ -204,6 +281,8 @@ module CMDx
204
281
  #
205
282
  # @example
206
283
  # required :first_name, :last_name, types: String
284
+ #
285
+ # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
207
286
  def required(*names, **options, &)
208
287
  attributes(*names, **options.merge(required: true), &)
209
288
  end
@@ -211,6 +290,8 @@ module CMDx
211
290
  # Defines the attribute method on the task and validates the configuration.
212
291
  #
213
292
  # @raise [RuntimeError] When the method name is already defined on the task
293
+ #
294
+ # @rbs () -> void
214
295
  def define_and_verify
215
296
  if task.respond_to?(method_name, true)
216
297
  raise <<~MESSAGE
@@ -6,6 +6,14 @@ module CMDx
6
6
  # in a hierarchical structure, supporting nested attribute definitions.
7
7
  class AttributeRegistry
8
8
 
9
+ # Returns the collection of registered attributes.
10
+ #
11
+ # @return [Array<Attribute>] Array of registered attributes
12
+ #
13
+ # @example
14
+ # registry.registry # => [#<Attribute @name=:name>, #<Attribute @name=:email>]
15
+ #
16
+ # @rbs @registry: Array[Attribute]
9
17
  attr_reader :registry
10
18
  alias to_a registry
11
19
 
@@ -18,6 +26,8 @@ module CMDx
18
26
  # @example
19
27
  # registry = AttributeRegistry.new
20
28
  # registry = AttributeRegistry.new([attr1, attr2])
29
+ #
30
+ # @rbs (?Array[Attribute] registry) -> void
21
31
  def initialize(registry = [])
22
32
  @registry = registry
23
33
  end
@@ -28,6 +38,8 @@ module CMDx
28
38
  #
29
39
  # @example
30
40
  # new_registry = registry.dup
41
+ #
42
+ # @rbs () -> AttributeRegistry
31
43
  def dup
32
44
  self.class.new(registry.dup)
33
45
  end
@@ -41,6 +53,8 @@ module CMDx
41
53
  # @example
42
54
  # registry.register(attribute)
43
55
  # registry.register([attr1, attr2])
56
+ #
57
+ # @rbs (Attribute | Array[Attribute] attributes) -> self
44
58
  def register(attributes)
45
59
  @registry.concat(Array(attributes))
46
60
  self
@@ -56,6 +70,8 @@ module CMDx
56
70
  # @example
57
71
  # registry.deregister(:name)
58
72
  # registry.deregister(['name1', 'name2'])
73
+ #
74
+ # @rbs ((Symbol | String | Array[Symbol | String]) names) -> self
59
75
  def deregister(names)
60
76
  Array(names).each do |name|
61
77
  @registry.reject! { |attribute| matches_attribute_tree?(attribute, name.to_sym) }
@@ -69,6 +85,8 @@ module CMDx
69
85
  # and validate the attribute hierarchy.
70
86
  #
71
87
  # @param task [Task] The task to associate with all attributes
88
+ #
89
+ # @rbs (Task task) -> void
72
90
  def define_and_verify(task)
73
91
  registry.each do |attribute|
74
92
  attribute.task = task
@@ -84,6 +102,8 @@ module CMDx
84
102
  # @param name [Symbol] The name to match against
85
103
  #
86
104
  # @return [Boolean] True if the attribute or any child matches the name
105
+ #
106
+ # @rbs (Attribute attribute, Symbol name) -> bool
87
107
  def matches_attribute_tree?(attribute, name)
88
108
  return true if attribute.method_name == name
89
109
 
@@ -8,6 +8,14 @@ module CMDx
8
8
 
9
9
  extend Forwardable
10
10
 
11
+ # Returns the attribute managed by this value handler.
12
+ #
13
+ # @return [Attribute] The attribute instance
14
+ #
15
+ # @example
16
+ # attr_value.attribute.name # => :user_id
17
+ #
18
+ # @rbs @attribute: Attribute
11
19
  attr_reader :attribute
12
20
 
13
21
  def_delegators :attribute, :task, :parent, :name, :options, :types, :source, :method_name, :required?
@@ -20,6 +28,8 @@ module CMDx
20
28
  # @example
21
29
  # attr = Attribute.new(:user_id, required: true)
22
30
  # attr_value = AttributeValue.new(attr)
31
+ #
32
+ # @rbs (Attribute attribute) -> void
23
33
  def initialize(attribute)
24
34
  @attribute = attribute
25
35
  end
@@ -30,6 +40,8 @@ module CMDx
30
40
  #
31
41
  # @example
32
42
  # attr_value.value # => "john_doe"
43
+ #
44
+ # @rbs () -> untyped
33
45
  def value
34
46
  attributes[method_name]
35
47
  end
@@ -41,6 +53,8 @@ module CMDx
41
53
  #
42
54
  # @example
43
55
  # attr_value.generate # => 42
56
+ #
57
+ # @rbs () -> untyped
44
58
  def generate
45
59
  return value if attributes.key?(method_name)
46
60
 
@@ -64,6 +78,8 @@ module CMDx
64
78
  # @example
65
79
  # attr_value.validate
66
80
  # # Validates value against :presence, :format, etc.
81
+ #
82
+ # @rbs () -> void
67
83
  def validate
68
84
  registry = task.class.settings[:validators]
69
85
 
@@ -86,6 +102,7 @@ module CMDx
86
102
  # @example
87
103
  # # Sources from task method, proc, or direct value
88
104
  # source_value # => "raw_value"
105
+ # @rbs () -> untyped
89
106
  def source_value
90
107
  sourced_value =
91
108
  case source
@@ -115,6 +132,8 @@ module CMDx
115
132
  # @example
116
133
  # # Default can be symbol, proc, or direct value
117
134
  # -> { rand(100) } # => 23
135
+ #
136
+ # @rbs () -> untyped
118
137
  def default_value
119
138
  default = options[:default]
120
139
 
@@ -140,6 +159,8 @@ module CMDx
140
159
  # @example
141
160
  # # Derives from hash key, method call, or proc execution
142
161
  # context.user_id # => 42
162
+ #
163
+ # @rbs (untyped source_value) -> untyped
143
164
  def derive_value(source_value)
144
165
  derived_value =
145
166
  case source_value
@@ -163,6 +184,8 @@ module CMDx
163
184
  #
164
185
  # @example
165
186
  # :downcase # => "hello"
187
+ #
188
+ # @rbs (untyped derived_value) -> untyped
166
189
  def transform_value(derived_value)
167
190
  transform = options[:transform]
168
191
 
@@ -186,6 +209,8 @@ module CMDx
186
209
  # @example
187
210
  # # Coerces "42" to Integer, "true" to Boolean, etc.
188
211
  # coerce_value("42") # => 42
212
+ #
213
+ # @rbs (untyped transformed_value) -> untyped
189
214
  def coerce_value(transformed_value)
190
215
  return transformed_value if types.empty?
191
216