cmdx 1.8.0 → 1.9.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 (61) 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/.irbrc +14 -2
  6. data/CHANGELOG.md +58 -45
  7. data/LLM.md +150 -53
  8. data/README.md +23 -85
  9. data/docs/.DS_Store +0 -0
  10. data/docs/assets/favicon.ico +0 -0
  11. data/docs/assets/favicon.svg +1 -0
  12. data/docs/attributes/coercions.md +12 -24
  13. data/docs/attributes/defaults.md +3 -16
  14. data/docs/attributes/definitions.md +16 -30
  15. data/docs/attributes/naming.md +3 -13
  16. data/docs/attributes/transformations.md +63 -0
  17. data/docs/attributes/validations.md +14 -33
  18. data/docs/basics/chain.md +14 -23
  19. data/docs/basics/context.md +13 -22
  20. data/docs/basics/execution.md +8 -26
  21. data/docs/basics/setup.md +8 -19
  22. data/docs/callbacks.md +19 -32
  23. data/docs/deprecation.md +8 -25
  24. data/docs/getting_started.md +101 -77
  25. data/docs/index.md +120 -0
  26. data/docs/internationalization.md +6 -18
  27. data/docs/interruptions/exceptions.md +10 -16
  28. data/docs/interruptions/faults.md +8 -25
  29. data/docs/interruptions/halt.md +12 -27
  30. data/docs/logging.md +7 -17
  31. data/docs/middlewares.md +13 -29
  32. data/docs/outcomes/result.md +21 -38
  33. data/docs/outcomes/states.md +8 -22
  34. data/docs/outcomes/statuses.md +10 -21
  35. data/docs/stylesheets/extra.css +42 -0
  36. data/docs/tips_and_tricks.md +7 -46
  37. data/docs/workflows.md +23 -38
  38. data/examples/active_record_query_tagging.md +46 -0
  39. data/examples/paper_trail_whatdunnit.md +39 -0
  40. data/lib/cmdx/attribute.rb +6 -5
  41. data/lib/cmdx/attribute_value.rb +31 -10
  42. data/lib/cmdx/callback_registry.rb +12 -2
  43. data/lib/cmdx/coercions/hash.rb +2 -0
  44. data/lib/cmdx/configuration.rb +10 -2
  45. data/lib/cmdx/deprecator.rb +3 -3
  46. data/lib/cmdx/executor.rb +93 -7
  47. data/lib/cmdx/pipeline.rb +4 -4
  48. data/lib/cmdx/railtie.rb +9 -0
  49. data/lib/cmdx/result.rb +10 -1
  50. data/lib/cmdx/task.rb +12 -7
  51. data/lib/cmdx/version.rb +1 -1
  52. data/lib/cmdx.rb +1 -0
  53. data/lib/generators/cmdx/templates/install.rb +9 -0
  54. data/mkdocs.yml +122 -0
  55. data/src/cmdx-dark-logo.png +0 -0
  56. data/src/cmdx-favicon.svg +1 -0
  57. data/src/cmdx-light-logo.png +0 -0
  58. data/src/cmdx-logo.svg +1 -0
  59. metadata +14 -3
  60. data/lib/cmdx/freezer.rb +0 -51
  61. 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
+ ```
@@ -66,7 +66,7 @@ module CMDx
66
66
  if names.none?
67
67
  raise ArgumentError, "no attributes given"
68
68
  elsif (names.size > 1) && options.key?(:as)
69
- raise ArgumentError, ":as option only supports one attribute per definition"
69
+ raise ArgumentError, "the :as option only supports one attribute per definition"
70
70
  end
71
71
 
72
72
  names.filter_map { |name| new(name, **options, &) }
@@ -213,10 +213,11 @@ module CMDx
213
213
  # @raise [RuntimeError] When the method name is already defined on the task
214
214
  def define_and_verify
215
215
  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
216
+ raise <<~MESSAGE
217
+ The method #{method_name.inspect} is already defined on the #{task.class.name} task.
218
+ This may be due conflicts with one of the task's user defined or internal methods/attributes.
219
+
220
+ Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
220
221
  MESSAGE
221
222
  end
222
223
 
@@ -51,9 +51,10 @@ module CMDx
51
51
  return if errors.for?(method_name)
52
52
 
53
53
  coerced_value = coerce_value(derived_value)
54
+ transformed_value = transform_value(coerced_value)
54
55
  return if errors.for?(method_name)
55
56
 
56
- attributes[method_name] = coerced_value
57
+ attributes[method_name] = transformed_value
57
58
  end
58
59
 
59
60
  # Validates the current attribute value against configured validators.
@@ -113,7 +114,7 @@ module CMDx
113
114
  #
114
115
  # @example
115
116
  # # Default can be symbol, proc, or direct value
116
- # default_value # => "default_value"
117
+ # -> { rand(100) } # => 23
117
118
  def default_value
118
119
  default = options[:default]
119
120
 
@@ -138,7 +139,7 @@ module CMDx
138
139
  #
139
140
  # @example
140
141
  # # Derives from hash key, method call, or proc execution
141
- # derive_value({user_id: 42}) # => 42
142
+ # context.user_id # => 42
142
143
  def derive_value(source_value)
143
144
  derived_value =
144
145
  case source_value
@@ -154,9 +155,29 @@ module CMDx
154
155
  nil
155
156
  end
156
157
 
158
+ # Transforms the derived value using the transform option.
159
+ #
160
+ # @param derived_value [Object] The value to transform
161
+ #
162
+ # @return [Object, nil] The transformed value or nil if transformation failed
163
+ #
164
+ # @example
165
+ # :downcase # => "hello"
166
+ def transform_value(derived_value)
167
+ transform = options[:transform]
168
+
169
+ if transform.is_a?(Symbol) && derived_value.respond_to?(transform, true)
170
+ derived_value.send(transform)
171
+ elsif transform.respond_to?(:call)
172
+ transform.call(derived_value)
173
+ else
174
+ derived_value
175
+ end
176
+ end
177
+
157
178
  # Coerces the derived value to the expected type(s) using the coercion registry.
158
179
  #
159
- # @param derived_value [Object] The value to coerce
180
+ # @param transformed_value [Object] The value to coerce
160
181
  #
161
182
  # @return [Object, nil] The coerced value or nil if coercion failed
162
183
  #
@@ -165,14 +186,14 @@ module CMDx
165
186
  # @example
166
187
  # # Coerces "42" to Integer, "true" to Boolean, etc.
167
188
  # coerce_value("42") # => 42
168
- def coerce_value(derived_value)
169
- return derived_value if attribute.types.empty?
189
+ def coerce_value(transformed_value)
190
+ return transformed_value if types.empty?
170
191
 
171
192
  registry = task.class.settings[:coercions]
172
- last_idx = attribute.types.size - 1
193
+ last_idx = types.size - 1
173
194
 
174
- attribute.types.find.with_index do |type, i|
175
- break registry.coerce(type, task, derived_value, options)
195
+ types.find.with_index do |type, i|
196
+ break registry.coerce(type, task, transformed_value, options)
176
197
  rescue CoercionError => e
177
198
  next if i != last_idx
178
199
 
@@ -180,7 +201,7 @@ module CMDx
180
201
  if last_idx.zero?
181
202
  e.message
182
203
  else
183
- tl = attribute.types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
204
+ tl = types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
184
205
  Locale.t("cmdx.coercions.into_any", types: tl)
185
206
  end
186
207
 
@@ -95,9 +95,19 @@ module CMDx
95
95
  raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type)
96
96
 
97
97
  Array(registry[type]).each do |callables, options|
98
- next unless Utils::Condition.evaluate(task, options, task)
98
+ next unless Utils::Condition.evaluate(task, options)
99
99
 
100
- Array(callables).each { |callable| Utils::Call.invoke(task, callable, task) }
100
+ Array(callables).each do |callable|
101
+ if callable.is_a?(Symbol)
102
+ task.send(callable)
103
+ elsif callable.is_a?(Proc)
104
+ task.instance_exec(&callable)
105
+ elsif callable.respond_to?(:call)
106
+ callable.call(task)
107
+ else
108
+ raise "cannot invoke #{callable}"
109
+ end
110
+ end
101
111
  end
102
112
  end
103
113
 
@@ -39,6 +39,8 @@ module CMDx
39
39
  ::Hash[*value]
40
40
  elsif value.is_a?(::String) && value.start_with?("{")
41
41
  JSON.parse(value)
42
+ elsif value.respond_to?(:to_h)
43
+ value.to_h
42
44
  else
43
45
  raise_coercion_error!
44
46
  end
@@ -3,13 +3,14 @@
3
3
  module CMDx
4
4
 
5
5
  # Configuration class that manages global settings for CMDx including middlewares,
6
- # callbacks, coercions, validators, breakpoints, and logging.
6
+ # callbacks, coercions, validators, breakpoints, backtraces, and logging.
7
7
  class Configuration
8
8
 
9
9
  DEFAULT_BREAKPOINTS = %w[failed].freeze
10
10
 
11
11
  attr_accessor :middlewares, :callbacks, :coercions, :validators,
12
- :task_breakpoints, :workflow_breakpoints, :logger
12
+ :task_breakpoints, :workflow_breakpoints, :logger,
13
+ :backtrace, :backtrace_cleaner, :exception_handler
13
14
 
14
15
  # Initializes a new Configuration instance with default values.
15
16
  #
@@ -31,6 +32,10 @@ module CMDx
31
32
  @task_breakpoints = DEFAULT_BREAKPOINTS
32
33
  @workflow_breakpoints = DEFAULT_BREAKPOINTS
33
34
 
35
+ @backtrace = false
36
+ @backtrace_cleaner = nil
37
+ @exception_handler = nil
38
+
34
39
  @logger = Logger.new(
35
40
  $stdout,
36
41
  progname: "cmdx",
@@ -55,6 +60,9 @@ module CMDx
55
60
  validators: @validators,
56
61
  task_breakpoints: @task_breakpoints,
57
62
  workflow_breakpoints: @workflow_breakpoints,
63
+ backtrace: @backtrace,
64
+ backtrace_cleaner: @backtrace_cleaner,
65
+ exception_handler: @exception_handler,
58
66
  logger: @logger
59
67
  }
60
68
  end
@@ -44,15 +44,15 @@ module CMDx
44
44
  # settings(deprecate: :warn)
45
45
  # end
46
46
  #
47
- # MyTask.new # => [MyTask] DEPRECATED: migrate to replacement or discontinue use
47
+ # MyTask.new # => [MyTask] DEPRECATED: migrate to a replacement or discontinue use
48
48
  def restrict(task)
49
49
  type = EVAL.call(task, task.class.settings[:deprecate])
50
50
 
51
51
  case type
52
52
  when NilClass, FalseClass # Do nothing
53
53
  when TrueClass, /raise/ then raise DeprecationError, "#{task.class.name} usage prohibited"
54
- when /log/ then task.logger.warn { "DEPRECATED: migrate to replacement or discontinue use" }
55
- when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to replacement or discontinue use", category: :deprecated)
54
+ when /log/ then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
55
+ when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
56
56
  else raise "unknown deprecation type #{type.inspect}"
57
57
  end
58
58
  end