cmdx 1.8.0 → 1.9.1

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/prompts/docs.md +3 -3
  4. data/.cursor/prompts/llms.md +1 -3
  5. data/.cursor/prompts/yardoc.md +1 -0
  6. data/.irbrc +14 -2
  7. data/CHANGELOG.md +64 -45
  8. data/LLM.md +159 -53
  9. data/README.md +26 -83
  10. data/docs/.DS_Store +0 -0
  11. data/docs/assets/favicon.ico +0 -0
  12. data/docs/assets/favicon.svg +1 -0
  13. data/docs/attributes/coercions.md +12 -24
  14. data/docs/attributes/defaults.md +3 -16
  15. data/docs/attributes/definitions.md +16 -30
  16. data/docs/attributes/naming.md +3 -13
  17. data/docs/attributes/transformations.md +63 -0
  18. data/docs/attributes/validations.md +14 -33
  19. data/docs/basics/chain.md +14 -23
  20. data/docs/basics/context.md +13 -22
  21. data/docs/basics/execution.md +8 -26
  22. data/docs/basics/setup.md +8 -19
  23. data/docs/callbacks.md +19 -32
  24. data/docs/deprecation.md +8 -25
  25. data/docs/getting_started.md +109 -76
  26. data/docs/index.md +132 -0
  27. data/docs/internationalization.md +6 -18
  28. data/docs/interruptions/exceptions.md +10 -16
  29. data/docs/interruptions/faults.md +8 -25
  30. data/docs/interruptions/halt.md +12 -27
  31. data/docs/logging.md +7 -17
  32. data/docs/middlewares.md +13 -29
  33. data/docs/outcomes/result.md +21 -38
  34. data/docs/outcomes/states.md +8 -22
  35. data/docs/outcomes/statuses.md +10 -21
  36. data/docs/stylesheets/extra.css +42 -0
  37. data/docs/tips_and_tricks.md +7 -46
  38. data/docs/workflows.md +23 -38
  39. data/examples/active_record_query_tagging.md +46 -0
  40. data/examples/paper_trail_whatdunnit.md +39 -0
  41. data/lib/cmdx/attribute.rb +88 -6
  42. data/lib/cmdx/attribute_registry.rb +20 -0
  43. data/lib/cmdx/attribute_value.rb +56 -10
  44. data/lib/cmdx/callback_registry.rb +31 -2
  45. data/lib/cmdx/chain.rb +34 -1
  46. data/lib/cmdx/coercion_registry.rb +18 -0
  47. data/lib/cmdx/coercions/array.rb +2 -0
  48. data/lib/cmdx/coercions/big_decimal.rb +3 -0
  49. data/lib/cmdx/coercions/boolean.rb +5 -0
  50. data/lib/cmdx/coercions/complex.rb +2 -0
  51. data/lib/cmdx/coercions/date.rb +4 -0
  52. data/lib/cmdx/coercions/date_time.rb +5 -0
  53. data/lib/cmdx/coercions/float.rb +2 -0
  54. data/lib/cmdx/coercions/hash.rb +4 -0
  55. data/lib/cmdx/coercions/integer.rb +2 -0
  56. data/lib/cmdx/coercions/rational.rb +2 -0
  57. data/lib/cmdx/coercions/string.rb +2 -0
  58. data/lib/cmdx/coercions/symbol.rb +2 -0
  59. data/lib/cmdx/coercions/time.rb +5 -0
  60. data/lib/cmdx/configuration.rb +119 -3
  61. data/lib/cmdx/context.rb +36 -0
  62. data/lib/cmdx/deprecator.rb +6 -3
  63. data/lib/cmdx/errors.rb +22 -0
  64. data/lib/cmdx/executor.rb +136 -7
  65. data/lib/cmdx/faults.rb +14 -0
  66. data/lib/cmdx/identifier.rb +2 -0
  67. data/lib/cmdx/locale.rb +3 -0
  68. data/lib/cmdx/log_formatters/json.rb +2 -0
  69. data/lib/cmdx/log_formatters/key_value.rb +2 -0
  70. data/lib/cmdx/log_formatters/line.rb +2 -0
  71. data/lib/cmdx/log_formatters/logstash.rb +2 -0
  72. data/lib/cmdx/log_formatters/raw.rb +2 -0
  73. data/lib/cmdx/middleware_registry.rb +20 -0
  74. data/lib/cmdx/middlewares/correlate.rb +11 -0
  75. data/lib/cmdx/middlewares/runtime.rb +4 -0
  76. data/lib/cmdx/middlewares/timeout.rb +4 -0
  77. data/lib/cmdx/pipeline.rb +24 -5
  78. data/lib/cmdx/railtie.rb +13 -0
  79. data/lib/cmdx/result.rb +133 -2
  80. data/lib/cmdx/task.rb +103 -8
  81. data/lib/cmdx/utils/call.rb +2 -0
  82. data/lib/cmdx/utils/condition.rb +3 -0
  83. data/lib/cmdx/utils/format.rb +5 -0
  84. data/lib/cmdx/validator_registry.rb +18 -0
  85. data/lib/cmdx/validators/exclusion.rb +2 -0
  86. data/lib/cmdx/validators/format.rb +2 -0
  87. data/lib/cmdx/validators/inclusion.rb +2 -0
  88. data/lib/cmdx/validators/length.rb +14 -0
  89. data/lib/cmdx/validators/numeric.rb +14 -0
  90. data/lib/cmdx/validators/presence.rb +2 -0
  91. data/lib/cmdx/version.rb +4 -1
  92. data/lib/cmdx/workflow.rb +10 -0
  93. data/lib/cmdx.rb +9 -0
  94. data/lib/generators/cmdx/locale_generator.rb +0 -1
  95. data/lib/generators/cmdx/templates/install.rb +9 -0
  96. data/mkdocs.yml +122 -0
  97. data/src/cmdx-dark-logo.png +0 -0
  98. data/src/cmdx-favicon.svg +1 -0
  99. data/src/cmdx-light-logo.png +0 -0
  100. data/src/cmdx-logo.svg +1 -0
  101. metadata +14 -3
  102. data/lib/cmdx/freezer.rb +0 -51
  103. data/src/cmdx-logo.png +0 -0
@@ -1,16 +1,6 @@
1
1
  # Outcomes - States
2
2
 
3
- States represent the execution lifecycle condition of task execution, tracking
4
- the progress of tasks through their complete execution journey. States provide
5
- insight into where a task is in its lifecycle and enable lifecycle-based
6
- decision making and monitoring.
7
-
8
- ## Table of Contents
9
-
10
- - [Definitions](#definitions)
11
- - [Transitions](#transitions)
12
- - [Predicates](#predicates)
13
- - [Handlers](#handlers)
3
+ States track where a task is in its execution lifecycle—from creation through completion or interruption.
14
4
 
15
5
  ## Definitions
16
6
 
@@ -34,8 +24,9 @@ State-Status combinations:
34
24
 
35
25
  ## Transitions
36
26
 
37
- > [!CAUTION]
38
- > States are automatically managed during task execution and should **never** be modified manually. State transitions are handled internally by the CMDx framework.
27
+ !!! danger "Caution"
28
+
29
+ States are managed automatically—never modify them manually.
39
30
 
40
31
  ```ruby
41
32
  # Valid state transition flow
@@ -62,19 +53,14 @@ result.executed? #=> true (complete OR interrupted)
62
53
 
63
54
  ## Handlers
64
55
 
65
- Use state-based handlers for lifecycle event handling. The `on_executed` handler is particularly useful for cleanup operations that should run regardless of success, skipped, or failure.
56
+ Handle lifecycle events with state-based handlers. Use `handle_executed` for cleanup that runs regardless of outcome:
66
57
 
67
58
  ```ruby
68
59
  result = ProcessVideoUpload.execute
69
60
 
70
61
  # Individual state handlers
71
62
  result
72
- .on_complete { |result| send_upload_notification(result) }
73
- .on_interrupted { |result| cleanup_temp_files(result) }
74
- .on_executed { |result| log_upload_metrics(result) }
63
+ .handle_complete { |result| send_upload_notification(result) }
64
+ .handle_interrupted { |result| cleanup_temp_files(result) }
65
+ .handle_executed { |result| log_upload_metrics(result) }
75
66
  ```
76
-
77
- ---
78
-
79
- - **Prev:** [Outcomes - Result](result.md)
80
- - **Next:** [Outcomes - Statuses](statuses.md)
@@ -1,13 +1,6 @@
1
1
  # Outcomes - Statuses
2
2
 
3
- Statuses represent the business outcome of task execution logic, indicating how the task's business logic concluded. Statuses differ from execution states by focusing on the business outcome rather than the technical execution lifecycle. Understanding statuses is crucial for implementing proper business logic branching and error handling.
4
-
5
- ## Table of Contents
6
-
7
- - [Definitions](#definitions)
8
- - [Transitions](#transitions)
9
- - [Predicates](#predicates)
10
- - [Handlers](#handlers)
3
+ Statuses represent the business outcome—did the task succeed, skip, or fail? This differs from state, which tracks the execution lifecycle.
11
4
 
12
5
  ## Definitions
13
6
 
@@ -19,8 +12,9 @@ Statuses represent the business outcome of task execution logic, indicating how
19
12
 
20
13
  ## Transitions
21
14
 
22
- > [!IMPORTANT]
23
- > Status transitions are unidirectional and final. Once a task is marked as skipped or failed, it cannot return to success status. Design your business logic accordingly.
15
+ !!! warning "Important"
16
+
17
+ Status transitions are final and unidirectional. Once skipped or failed, tasks can't return to success.
24
18
 
25
19
  ```ruby
26
20
  # Valid status transitions
@@ -53,24 +47,19 @@ result.bad? #=> true if skipped OR failed (not success)
53
47
 
54
48
  ## Handlers
55
49
 
56
- Use status-based handlers for business logic branching. The `on_good` and `on_bad` handlers are particularly useful for handling success/skip vs failed outcomes respectively.
50
+ Branch business logic with status-based handlers. Use `handle_good` and `handle_bad` for success/skip vs failed outcomes:
57
51
 
58
52
  ```ruby
59
53
  result = ProcessNotification.execute
60
54
 
61
55
  # Individual status handlers
62
56
  result
63
- .on_success { |result| mark_notification_sent(result) }
64
- .on_skipped { |result| log_notification_skipped(result) }
65
- .on_failed { |result| queue_retry_notification(result) }
57
+ .handle_success { |result| mark_notification_sent(result) }
58
+ .handle_skipped { |result| log_notification_skipped(result) }
59
+ .handle_failed { |result| queue_retry_notification(result) }
66
60
 
67
61
  # Outcome-based handlers
68
62
  result
69
- .on_good { |result| update_message_stats(result) }
70
- .on_bad { |result| track_delivery_failure(result) }
63
+ .handle_good { |result| update_message_stats(result) }
64
+ .handle_bad { |result| track_delivery_failure(result) }
71
65
  ```
72
-
73
- ---
74
-
75
- - **Prev:** [Outcomes - States](states.md)
76
- - **Next:** [Attributes - Definitions](../attributes/definitions.md)
@@ -0,0 +1,42 @@
1
+ :root > * {
2
+ /* Primary color shades */
3
+ --md-primary-fg-color: #fe1817;
4
+ --md-primary-fg-color--light: #fe1817;
5
+ --md-primary-fg-color--dark: #fe1817;
6
+
7
+ /* Accent color shades */
8
+ --md-accent-fg-color: hsla(#{hex2hsl(#fe1817)}, 1);
9
+ --md-accent-fg-color--transparent: hsla(#{hex2hsl(#fe1817)}, 0.1);
10
+ }
11
+
12
+ /* Atom One Light Pro syntax highlighting */
13
+ [data-md-color-scheme="default"] {
14
+ --md-code-hl-color: #2c3036;
15
+ --md-code-hl-keyword-color: #a626a4;
16
+ --md-code-hl-string-color: #50a14f;
17
+ --md-code-hl-name-color: #e4564a;
18
+ --md-code-hl-function-color: #4078f2;
19
+ --md-code-hl-number-color: #ca7601;
20
+ --md-code-hl-constant-color: #c18401;
21
+ --md-code-hl-comment-color: #9ca0a4;
22
+ --md-code-hl-operator-color: #0184bc;
23
+ --md-code-hl-punctuation-color:#383a42;
24
+ --md-code-hl-variable-color: #e4564a;
25
+ --md-code-hl-generic-color: #e4564a;
26
+ }
27
+
28
+ /* Atom One Dark Pro syntax highlighting */
29
+ [data-md-color-scheme="slate"] {
30
+ --md-code-hl-color: #e5e5e6;
31
+ --md-code-hl-keyword-color: #c678dd;
32
+ --md-code-hl-string-color: #98c379;
33
+ --md-code-hl-name-color: #e06c75;
34
+ --md-code-hl-function-color: #61afef;
35
+ --md-code-hl-number-color: #d19a66;
36
+ --md-code-hl-constant-color: #d19a66;
37
+ --md-code-hl-comment-color: #7f848e;
38
+ --md-code-hl-operator-color: #56b6c2;
39
+ --md-code-hl-punctuation-color:#abb2bf;
40
+ --md-code-hl-variable-color: #e06c75;
41
+ --md-code-hl-generic-color: #e06c75;
42
+ }
@@ -1,16 +1,6 @@
1
1
  # Tips and Tricks
2
2
 
3
- This guide covers advanced patterns and optimization techniques for getting the most out of CMDx in production applications.
4
-
5
- ## Table of Contents
6
-
7
- - [Project Organization](#project-organization)
8
- - [Directory Structure](#directory-structure)
9
- - [Naming Conventions](#naming-conventions)
10
- - [Story Telling](#story-telling)
11
- - [Style Guide](#style-guide)
12
- - [Attribute Options](#attribute-options)
13
- - [ActiveRecord Query Tagging](#activerecord-query-tagging)
3
+ Best practices, patterns, and techniques to build maintainable CMDx applications.
14
4
 
15
5
  ## Project Organization
16
6
 
@@ -54,7 +44,7 @@ class TokenGeneration < CMDx::Task; end # ❌ Avoid
54
44
 
55
45
  ### Story Telling
56
46
 
57
- Consider using descriptive methods to express the task’s flow, rather than concentrating all logic inside the `work` method.
47
+ Break down complex logic into descriptive methods that read like a narrative:
58
48
 
59
49
  ```ruby
60
50
  class ProcessOrder < CMDx::Task
@@ -86,7 +76,7 @@ end
86
76
 
87
77
  ### Style Guide
88
78
 
89
- Follow a style pattern for consistent task design:
79
+ Follow this order for consistent, readable tasks:
90
80
 
91
81
  ```ruby
92
82
  class ExportReport < CMDx::Task
@@ -129,7 +119,7 @@ end
129
119
 
130
120
  ## Attribute Options
131
121
 
132
- Use Rails `with_options` to reduce duplication and improve readability:
122
+ Use `with_options` to reduce duplication:
133
123
 
134
124
  ```ruby
135
125
  class ConfigureCompany < CMDx::Task
@@ -155,36 +145,7 @@ class ConfigureCompany < CMDx::Task
155
145
  end
156
146
  ```
157
147
 
158
- ## ActiveRecord Query Tagging
159
-
160
- Automatically tag SQL queries for better debugging:
161
-
162
- ```ruby
163
- # config/application.rb
164
- config.active_record.query_log_tags_enabled = true
165
- config.active_record.query_log_tags << :cmdx_task_class
166
- config.active_record.query_log_tags << :cmdx_chain_id
167
-
168
- # app/tasks/application_task.rb
169
- class ApplicationTask < CMDx::Task
170
- before_execution :set_execution_context
171
-
172
- private
173
-
174
- def set_execution_context
175
- # NOTE: This could easily be made into a middleware
176
- ActiveSupport::ExecutionContext.set(
177
- cmdx_task_class: self.class.name,
178
- cmdx_chain_id: chain.id
179
- )
180
- end
181
- end
182
-
183
- # SQL queries will now include comments like:
184
- # /*cmdx_task_class:ExportReportTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM reports WHERE id = 1
185
- ```
186
-
187
- ---
148
+ ## Advanced Examples
188
149
 
189
- - **Prev:** [Workflows](workflows.md)
190
- - **Next:** [Getting Started](getting_started.md)
150
+ - [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
151
+ - [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
data/docs/workflows.md CHANGED
@@ -1,26 +1,14 @@
1
1
  # Workflows
2
2
 
3
- Workflow orchestrates sequential execution of multiple tasks in a linear pipeline. Workflows provide a declarative DSL for composing complex business logic from individual task components, with support for conditional execution, context propagation, and configurable halt behavior.
4
-
5
- ## Table of Contents
6
-
7
- - [Declarations](#declarations)
8
- - [Task](#task)
9
- - [Group](#group)
10
- - [Conditionals](#conditionals)
11
- - [Halt Behavior](#halt-behavior)
12
- - [Task Configuration](#task-configuration)
13
- - [Group Configuration](#group-configuration)
14
- - [Nested Workflows](#nested-workflows)
15
- - [Parallel Execution](#parallel-execution)
16
- - [Task Generator](#task-generator)
3
+ Compose multiple tasks into powerful, sequential pipelines. Workflows provide a declarative way to build complex business processes with conditional execution, shared context, and flexible error handling.
17
4
 
18
5
  ## Declarations
19
6
 
20
- Tasks execute sequentially in declaration order (FIFO). The workflow context propagates to each task, allowing access to data from previous executions.
7
+ Tasks run in declaration order (FIFO), sharing a common context across the pipeline.
21
8
 
22
- > [!IMPORTANT]
23
- > Do **NOT** define a `work` method in workflow tasks. The included module automatically provides the execution logic.
9
+ !!! warning
10
+
11
+ Don't define a `work` method in workflows—the module handles execution automatically.
24
12
 
25
13
  ### Task
26
14
 
@@ -35,15 +23,17 @@ class OnboardingWorkflow < CMDx::Task
35
23
  end
36
24
  ```
37
25
 
38
- > [!TIP]
39
- > Execute tasks in parallel via the [cmdx-parallel](https://github.com/drexed/cmdx-parallel) gem.
26
+ !!! tip
27
+
28
+ Execute tasks in parallel via the [cmdx-parallel](https://github.com/drexed/cmdx-parallel) gem.
40
29
 
41
30
  ### Group
42
31
 
43
- Group related tasks for better organization and shared configuration:
32
+ Group related tasks to share configuration:
44
33
 
45
- > [!IMPORTANT]
46
- > Settings and conditionals for a group apply to all tasks within that group.
34
+ !!! warning "Important"
35
+
36
+ Settings and conditionals apply to all tasks in the group.
47
37
 
48
38
  ```ruby
49
39
  class ContentModerationWorkflow < CMDx::Task
@@ -78,10 +68,10 @@ class OnboardingWorkflow < CMDx::Task
78
68
  task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
79
69
 
80
70
  # Proc
81
- task SendWelcomeEmail, if: ->(workflow) { Rails.env.production? && workflow.class.name.include?("Premium") }
71
+ task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
82
72
 
83
73
  # Lambda
84
- task SendWelcomeEmail, if: proc { |workflow| workflow.context.features_enabled? }
74
+ task SendWelcomeEmail, if: proc { context.features_enabled? }
85
75
 
86
76
  # Class or Module
87
77
  task SendWelcomeEmail, unless: ContentAccessCheck
@@ -95,7 +85,7 @@ class OnboardingWorkflow < CMDx::Task
95
85
  private
96
86
 
97
87
  def email_configured?
98
- context.user.email_address.present?
88
+ context.user.email_address == true
99
89
  end
100
90
 
101
91
  def email_disabled?
@@ -106,9 +96,7 @@ end
106
96
 
107
97
  ## Halt Behavior
108
98
 
109
- By default skipped tasks are considered no-op executions and does not stop workflow execution.
110
- This is configurable via global and task level breakpoint settings. Task and group configurations
111
- can be used together within a workflow.
99
+ By default, skipped tasks don't stop the workflow—they're treated as no-ops. Configure breakpoints globally or per-task to customize this behavior.
112
100
 
113
101
  ```ruby
114
102
  class AnalyticsWorkflow < CMDx::Task
@@ -164,7 +152,7 @@ end
164
152
 
165
153
  ## Nested Workflows
166
154
 
167
- Workflows can task other workflows for hierarchical composition:
155
+ Build hierarchical workflows by composing workflows within workflows:
168
156
 
169
157
  ```ruby
170
158
  class EmailPreparationWorkflow < CMDx::Task
@@ -191,10 +179,11 @@ end
191
179
 
192
180
  ## Parallel Execution
193
181
 
194
- Parallel task execution leverages the [Parallel](https://github.com/grosser/parallel) gem, which automatically detects the number of available processors to maximize concurrent task execution.
182
+ Run tasks concurrently using the [Parallel](https://github.com/grosser/parallel) gem. It automatically uses all available processors for maximum throughput.
183
+
184
+ !!! warning
195
185
 
196
- > [!IMPORTANT]
197
- > Context cannot be modified during parallel execution. Ensure that all required data is preloaded into the context before parallelization begins.
186
+ Context is read-only during parallel execution. Load all required data beforehand.
198
187
 
199
188
  ```ruby
200
189
  class SendWelcomeNotifications < CMDx::Task
@@ -232,10 +221,6 @@ class SendNotifications < CMDx::Task
232
221
  end
233
222
  ```
234
223
 
235
- > [!TIP]
236
- > Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
237
-
238
- ---
224
+ !!! tip
239
225
 
240
- - **Prev:** [Deprecation](deprecation.md)
241
- - **Next:** [Tips and Tricks](tips_and_tricks.md)
226
+ Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
@@ -0,0 +1,46 @@
1
+ # Active Record Query Tagging
2
+
3
+ Add a comment to every query indicating some context to help you track down where that query came from, eg:
4
+
5
+ ```sh
6
+ /*cmdx_task_class:ExportReportTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM reports WHERE id = 1
7
+ ```
8
+
9
+ ### Setup
10
+
11
+ ```ruby
12
+ # config/application.rb
13
+ config.active_record.query_log_tags_enabled = true
14
+ config.active_record.query_log_tags += [
15
+ :cmdx_correlation_id,
16
+ :cmdx_chain_id,
17
+ :cmdx_task_class,
18
+ :cmdx_task_id
19
+ ]
20
+
21
+ # lib/cmdx_query_tagging_middleware.rb
22
+ class CmdxQueryTaggingMiddleware
23
+ def self.call(task, **options, &)
24
+ ActiveSupport::ExecutionContext.set(
25
+ cmdx_correlation_id: task.result.metadata[:correlation_id],
26
+ cmdx_chain_id: task.chain.id,
27
+ cmdx_task_class: task.class.name,
28
+ cmdx_task_id: task.id,
29
+ &
30
+ )
31
+ end
32
+ end
33
+ ```
34
+
35
+ ### Usage
36
+
37
+ ```ruby
38
+ class MyTask < CMDx::Task
39
+ register :middleware, CmdxQueryTaggingMiddleware
40
+
41
+ def work
42
+ # Do work...
43
+ end
44
+
45
+ end
46
+ ```
@@ -0,0 +1,39 @@
1
+ # Paper Trail Whatdunnit
2
+
3
+ Tag paper trail version records with which service made a change with a custom `whatdunnit` attribute.
4
+
5
+ <https://github.com/paper-trail-gem/paper_trail?tab=readme-ov-file#4c-storing-metadata>
6
+
7
+ ### Setup
8
+
9
+ ```ruby
10
+ # lib/cmdx_paper_trail_middleware.rb
11
+ class CmdxPaperTrailMiddleware
12
+ def self.call(task, **options, &)
13
+ # This makes sure to reset the whatdunnit value to the previous
14
+ # value for nested task calls
15
+
16
+ begin
17
+ PaperTrail.request.controller_info ||= {}
18
+ old_whatdunnit = PaperTrail.request.controller_info[:whatdunnit]
19
+ PaperTrail.request.controller_info[:whatdunnit] = task.class.name
20
+ yield
21
+ ensure
22
+ PaperTrail.request.controller_info[:whatdunnit] = old_whatdunnit
23
+ end
24
+ end
25
+ end
26
+ ```
27
+
28
+ ### Usage
29
+
30
+ ```ruby
31
+ class MyTask < CMDx::Task
32
+ register :middleware, CmdxPaperTrailMiddleware
33
+
34
+ def work
35
+ # Do work...
36
+ end
37
+
38
+ end
39
+ ```
@@ -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,11 +121,13 @@ 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"
68
129
  elsif (names.size > 1) && options.key?(:as)
69
- raise ArgumentError, ":as option only supports one attribute per definition"
130
+ raise ArgumentError, "the :as option only supports one attribute per definition"
70
131
  end
71
132
 
72
133
  names.filter_map { |name| new(name, **options, &) }
@@ -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,12 +290,15 @@ 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
- raise <<~MESSAGE.gsub!(/[[:space:]]+/, " ").strip!
217
- #{task.class.name}##{method_name} already defined.
218
- Use :as, :prefix, or :suffix option to avoid
219
- conflicts with existing methods
297
+ raise <<~MESSAGE
298
+ The method #{method_name.inspect} is already defined on the #{task.class.name} task.
299
+ This may be due conflicts with one of the task's user defined or internal methods/attributes.
300
+
301
+ Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
220
302
  MESSAGE
221
303
  end
222
304