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.
- checksums.yaml +4 -4
- data/.cursor/prompts/llms.md +3 -13
- data/.cursor/prompts/yardoc.md +1 -0
- data/CHANGELOG.md +16 -0
- data/LLM.md +436 -374
- data/README.md +7 -2
- data/docs/basics/setup.md +17 -0
- data/docs/callbacks.md +1 -1
- data/docs/getting_started.md +22 -2
- data/docs/index.md +13 -1
- data/docs/retries.md +121 -0
- data/docs/tips_and_tricks.md +2 -1
- data/examples/stoplight_circuit_breaker.md +36 -0
- data/lib/cmdx/attribute.rb +82 -1
- data/lib/cmdx/attribute_registry.rb +20 -0
- data/lib/cmdx/attribute_value.rb +25 -0
- data/lib/cmdx/callback_registry.rb +19 -0
- data/lib/cmdx/chain.rb +34 -1
- data/lib/cmdx/coercion_registry.rb +18 -0
- data/lib/cmdx/coercions/array.rb +2 -0
- data/lib/cmdx/coercions/big_decimal.rb +3 -0
- data/lib/cmdx/coercions/boolean.rb +5 -0
- data/lib/cmdx/coercions/complex.rb +2 -0
- data/lib/cmdx/coercions/date.rb +4 -0
- data/lib/cmdx/coercions/date_time.rb +5 -0
- data/lib/cmdx/coercions/float.rb +2 -0
- data/lib/cmdx/coercions/hash.rb +2 -0
- data/lib/cmdx/coercions/integer.rb +2 -0
- data/lib/cmdx/coercions/rational.rb +2 -0
- data/lib/cmdx/coercions/string.rb +2 -0
- data/lib/cmdx/coercions/symbol.rb +2 -0
- data/lib/cmdx/coercions/time.rb +5 -0
- data/lib/cmdx/configuration.rb +126 -3
- data/lib/cmdx/context.rb +36 -0
- data/lib/cmdx/deprecator.rb +3 -0
- data/lib/cmdx/errors.rb +22 -0
- data/lib/cmdx/executor.rb +71 -11
- data/lib/cmdx/faults.rb +14 -0
- data/lib/cmdx/identifier.rb +2 -0
- data/lib/cmdx/locale.rb +3 -0
- data/lib/cmdx/log_formatters/json.rb +2 -0
- data/lib/cmdx/log_formatters/key_value.rb +2 -0
- data/lib/cmdx/log_formatters/line.rb +2 -0
- data/lib/cmdx/log_formatters/logstash.rb +2 -0
- data/lib/cmdx/log_formatters/raw.rb +2 -0
- data/lib/cmdx/middleware_registry.rb +20 -0
- data/lib/cmdx/middlewares/correlate.rb +11 -0
- data/lib/cmdx/middlewares/runtime.rb +4 -0
- data/lib/cmdx/middlewares/timeout.rb +4 -0
- data/lib/cmdx/pipeline.rb +20 -1
- data/lib/cmdx/railtie.rb +4 -0
- data/lib/cmdx/result.rb +123 -1
- data/lib/cmdx/task.rb +91 -1
- data/lib/cmdx/utils/call.rb +2 -0
- data/lib/cmdx/utils/condition.rb +3 -0
- data/lib/cmdx/utils/format.rb +5 -0
- data/lib/cmdx/validator_registry.rb +18 -0
- data/lib/cmdx/validators/exclusion.rb +2 -0
- data/lib/cmdx/validators/format.rb +2 -0
- data/lib/cmdx/validators/inclusion.rb +2 -0
- data/lib/cmdx/validators/length.rb +14 -0
- data/lib/cmdx/validators/numeric.rb +14 -0
- data/lib/cmdx/validators/presence.rb +2 -0
- data/lib/cmdx/version.rb +4 -1
- data/lib/cmdx/workflow.rb +10 -0
- data/lib/cmdx.rb +8 -0
- data/lib/generators/cmdx/locale_generator.rb +0 -1
- data/mkdocs.yml +3 -1
- 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
|
|
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
data/docs/getting_started.md
CHANGED
|
@@ -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
|
|
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
|
[](https://github.com/drexed/cmdx/actions/workflows/ci.yml)
|
|
7
7
|
[](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
|
|
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
|
+
```
|
data/docs/tips_and_tricks.md
CHANGED
|
@@ -145,7 +145,8 @@ class ConfigureCompany < CMDx::Task
|
|
|
145
145
|
end
|
|
146
146
|
```
|
|
147
147
|
|
|
148
|
-
##
|
|
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
|
+
```
|
data/lib/cmdx/attribute.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
data/lib/cmdx/attribute_value.rb
CHANGED
|
@@ -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
|
|