cmdx 0.5.0 → 1.0.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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +19 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +95 -28
  7. data/README.md +73 -25
  8. data/docs/ai_prompts.md +319 -0
  9. data/docs/basics/call.md +234 -14
  10. data/docs/basics/chain.md +280 -0
  11. data/docs/basics/context.md +241 -33
  12. data/docs/basics/setup.md +85 -12
  13. data/docs/callbacks.md +283 -0
  14. data/docs/configuration.md +155 -30
  15. data/docs/getting_started.md +145 -22
  16. data/docs/internationalization.md +148 -0
  17. data/docs/interruptions/exceptions.md +198 -11
  18. data/docs/interruptions/faults.md +196 -44
  19. data/docs/interruptions/halt.md +188 -35
  20. data/docs/logging.md +204 -53
  21. data/docs/middlewares.md +745 -0
  22. data/docs/outcomes/result.md +305 -10
  23. data/docs/outcomes/states.md +212 -31
  24. data/docs/outcomes/statuses.md +284 -30
  25. data/docs/parameters/coercions.md +411 -29
  26. data/docs/parameters/defaults.md +258 -25
  27. data/docs/parameters/definitions.md +247 -72
  28. data/docs/parameters/namespacing.md +259 -27
  29. data/docs/parameters/validations.md +173 -168
  30. data/docs/testing.md +560 -0
  31. data/docs/tips_and_tricks.md +103 -42
  32. data/docs/workflows.md +329 -0
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +69 -0
  35. data/lib/cmdx/callback_registry.rb +106 -0
  36. data/lib/cmdx/chain.rb +190 -0
  37. data/lib/cmdx/chain_inspector.rb +149 -0
  38. data/lib/cmdx/chain_serializer.rb +175 -0
  39. data/lib/cmdx/coercions/array.rb +37 -0
  40. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  41. data/lib/cmdx/coercions/boolean.rb +41 -1
  42. data/lib/cmdx/coercions/complex.rb +31 -0
  43. data/lib/cmdx/coercions/date.rb +39 -0
  44. data/lib/cmdx/coercions/date_time.rb +39 -0
  45. data/lib/cmdx/coercions/float.rb +31 -0
  46. data/lib/cmdx/coercions/hash.rb +42 -0
  47. data/lib/cmdx/coercions/integer.rb +32 -0
  48. data/lib/cmdx/coercions/rational.rb +31 -0
  49. data/lib/cmdx/coercions/string.rb +31 -0
  50. data/lib/cmdx/coercions/time.rb +39 -0
  51. data/lib/cmdx/coercions/virtual.rb +31 -0
  52. data/lib/cmdx/configuration.rb +217 -9
  53. data/lib/cmdx/context.rb +173 -2
  54. data/lib/cmdx/core_ext/hash.rb +72 -0
  55. data/lib/cmdx/core_ext/module.rb +94 -0
  56. data/lib/cmdx/core_ext/object.rb +105 -0
  57. data/lib/cmdx/correlator.rb +217 -0
  58. data/lib/cmdx/error.rb +210 -8
  59. data/lib/cmdx/errors.rb +256 -1
  60. data/lib/cmdx/fault.rb +177 -2
  61. data/lib/cmdx/faults.rb +158 -2
  62. data/lib/cmdx/immutator.rb +121 -2
  63. data/lib/cmdx/lazy_struct.rb +261 -18
  64. data/lib/cmdx/log_formatters/json.rb +46 -0
  65. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  66. data/lib/cmdx/log_formatters/line.rb +54 -0
  67. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  68. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  69. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  70. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  71. data/lib/cmdx/log_formatters/raw.rb +54 -0
  72. data/lib/cmdx/logger.rb +85 -0
  73. data/lib/cmdx/logger_ansi.rb +93 -7
  74. data/lib/cmdx/logger_serializer.rb +116 -0
  75. data/lib/cmdx/middleware.rb +74 -0
  76. data/lib/cmdx/middleware_registry.rb +106 -0
  77. data/lib/cmdx/middlewares/correlate.rb +266 -0
  78. data/lib/cmdx/middlewares/timeout.rb +232 -0
  79. data/lib/cmdx/parameter.rb +228 -1
  80. data/lib/cmdx/parameter_inspector.rb +61 -0
  81. data/lib/cmdx/parameter_registry.rb +125 -0
  82. data/lib/cmdx/parameter_serializer.rb +83 -0
  83. data/lib/cmdx/parameter_validator.rb +62 -0
  84. data/lib/cmdx/parameter_value.rb +109 -1
  85. data/lib/cmdx/parameters_inspector.rb +59 -0
  86. data/lib/cmdx/parameters_serializer.rb +102 -0
  87. data/lib/cmdx/railtie.rb +123 -3
  88. data/lib/cmdx/result.rb +367 -25
  89. data/lib/cmdx/result_ansi.rb +105 -9
  90. data/lib/cmdx/result_inspector.rb +76 -0
  91. data/lib/cmdx/result_logger.rb +90 -3
  92. data/lib/cmdx/result_serializer.rb +137 -0
  93. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  94. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  95. data/lib/cmdx/task.rb +405 -37
  96. data/lib/cmdx/task_serializer.rb +74 -2
  97. data/lib/cmdx/utils/ansi_color.rb +95 -0
  98. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  99. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  100. data/lib/cmdx/utils/name_affix.rb +78 -0
  101. data/lib/cmdx/validators/custom.rb +82 -0
  102. data/lib/cmdx/validators/exclusion.rb +94 -0
  103. data/lib/cmdx/validators/format.rb +102 -8
  104. data/lib/cmdx/validators/inclusion.rb +104 -0
  105. data/lib/cmdx/validators/length.rb +128 -0
  106. data/lib/cmdx/validators/numeric.rb +128 -0
  107. data/lib/cmdx/validators/presence.rb +93 -7
  108. data/lib/cmdx/version.rb +7 -1
  109. data/lib/cmdx/workflow.rb +394 -0
  110. data/lib/cmdx.rb +25 -64
  111. data/lib/generators/cmdx/install_generator.rb +37 -1
  112. data/lib/generators/cmdx/task_generator.rb +69 -1
  113. data/lib/generators/cmdx/templates/install.rb +43 -15
  114. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  115. data/lib/locales/ar.yml +36 -0
  116. data/lib/locales/cs.yml +36 -0
  117. data/lib/locales/da.yml +36 -0
  118. data/lib/locales/de.yml +36 -0
  119. data/lib/locales/el.yml +36 -0
  120. data/lib/locales/en.yml +20 -20
  121. data/lib/locales/es.yml +20 -20
  122. data/lib/locales/fi.yml +36 -0
  123. data/lib/locales/fr.yml +36 -0
  124. data/lib/locales/he.yml +36 -0
  125. data/lib/locales/hi.yml +36 -0
  126. data/lib/locales/it.yml +36 -0
  127. data/lib/locales/ja.yml +36 -0
  128. data/lib/locales/ko.yml +36 -0
  129. data/lib/locales/nl.yml +36 -0
  130. data/lib/locales/no.yml +36 -0
  131. data/lib/locales/pl.yml +36 -0
  132. data/lib/locales/pt.yml +36 -0
  133. data/lib/locales/ru.yml +36 -0
  134. data/lib/locales/sv.yml +36 -0
  135. data/lib/locales/th.yml +36 -0
  136. data/lib/locales/tr.yml +36 -0
  137. data/lib/locales/vi.yml +36 -0
  138. data/lib/locales/zh.yml +36 -0
  139. metadata +77 -15
  140. data/docs/basics/run.md +0 -34
  141. data/docs/batch.md +0 -53
  142. data/docs/example.md +0 -82
  143. data/docs/hooks.md +0 -62
  144. data/lib/cmdx/batch.rb +0 -43
  145. data/lib/cmdx/parameters.rb +0 -35
  146. data/lib/cmdx/run.rb +0 -39
  147. data/lib/cmdx/run_inspector.rb +0 -26
  148. data/lib/cmdx/run_serializer.rb +0 -20
  149. data/lib/cmdx/task_hook.rb +0 -18
  150. data/lib/generators/cmdx/batch_generator.rb +0 -30
  151. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -1,67 +1,275 @@
1
1
  # Basics - Context
2
2
 
3
- The task `context` provides a form of storage to the task objects themselves.
3
+ The task `context` provides flexible data storage and sharing for task objects.
4
+ Built on `LazyStruct`, context enables dynamic attribute access, parameter
5
+ validation, and seamless data flow between related tasks.
4
6
 
5
- ## Loading
7
+ ## Table of Contents
6
8
 
7
- Loading a task with data is as simple as passing objects in a key value format.
8
- The context object is a custom version of an [OpenStruct](https://github.com/ruby/ostruct)
9
- called a `LazyStruct` with no limitations for what can be stored.
9
+ - [TLDR](#tldr)
10
+ - [Loading Parameters](#loading-parameters)
11
+ - [Accessing Data](#accessing-data)
12
+ - [Modifying Context](#modifying-context)
13
+ - [Context Features](#context-features)
14
+ - [Data Sharing Between Tasks](#data-sharing-between-tasks)
15
+ - [Result Object Context Passing](#result-object-context-passing)
16
+ - [Context Inspection](#context-inspection)
17
+
18
+ ## TLDR
19
+
20
+ - **Dynamic attributes** - Access data with `context.user_id` or `context[:user_id]`
21
+ - **Automatic loading** - Parameters become context attributes automatically
22
+ - **Modification** - Assign data with `context.user = User.find(id)`
23
+ - **Sharing** - Pass context between tasks with `SomeTask.call(context)`
24
+ - **Nil safety** - Missing attributes return `nil` instead of raising errors
25
+
26
+ ## Loading Parameters
27
+
28
+ Context is automatically populated when calling tasks with parameters. All
29
+ parameters become accessible as dynamic attributes within the task.
10
30
 
11
31
  ```ruby
12
- ProcessOrderTask.call(email: "bob@bigcorp.com", order: order)
32
+ ProcessUserOrderTask.call(
33
+ user: User.first,
34
+ order_id: 456,
35
+ send_notification: true
36
+ )
13
37
  ```
14
38
 
15
- ## Access
39
+ ## Accessing Data
16
40
 
17
- Tasks with a loaded context can be accessed within a task itself. Read, set and
18
- alter the context attributes anywhere within the task object.
41
+ Context provides multiple ways to access stored data with automatic key
42
+ normalization to symbols:
19
43
 
20
44
  ```ruby
21
- class ProcessOrderTask < CMDx::Task
45
+ class ProcessUserOrderTask < CMDx::Task
22
46
 
23
47
  def call
24
- # Reading from context storage:
25
- context.email #=> "bob@bigcorp.com"
26
- ctx.order #=> <Order #a1b2c3d>
27
- context.idk #=> nil
28
-
29
- # Writing to context storage:
30
- context.first_name = "Bob"
31
- ctx.middle_name ||= "Biggie"
32
- context.merge!(last_name: "Boomer")
48
+ # Method-style access (preferred)
49
+ context.user_id #=> 123
50
+ context.send_notification #=> true
51
+
52
+ # Hash-style access
53
+ context[:order_id] #=> 456
54
+ context["user_id"] #=> 123
55
+
56
+ # Safe access with defaults
57
+ context.fetch!(:priority, "normal") #=> "high"
58
+
59
+ # Deep access for nested data
60
+ context.dig(:metadata, :source) #=> "mobile"
61
+
62
+ # Alias for shorter code
63
+ ctx.user_id #=> 123 (ctx is alias for context)
33
64
  end
34
65
 
35
66
  end
36
67
  ```
37
68
 
38
- [Learn more](https://github.com/drexed/cmdx/blob/main/lib/cmdx/lazy_struct.rb)
39
- about the `LazyStruct` public API for interacting with the context.
69
+ ## Modifying Context
70
+
71
+ Context supports dynamic modification during task execution:
72
+
73
+ ```ruby
74
+ class ProcessUserOrderTask < CMDx::Task
75
+
76
+ def call
77
+ # Direct assignment
78
+ context.user = User.find(user_id)
79
+ context.order = Order.find(order_id)
80
+ context.processed_at = Time.now
81
+
82
+ # Hash-style assignment
83
+ context[:status] = "processing"
84
+ context["result_code"] = "SUCCESS"
85
+
86
+ # Conditional assignment
87
+ context.notification_sent ||= false
88
+
89
+ # Batch updates
90
+ context.merge!(
91
+ status: "completed",
92
+ processed_by: current_user.id,
93
+ completion_time: Time.now
94
+ )
95
+
96
+ # Removing data
97
+ context.delete!(:temporary_data)
98
+ end
99
+
100
+ end
101
+ ```
102
+
103
+ > [!TIP]
104
+ > Use context for both input parameters and intermediate results. This creates
105
+ > a natural data flow through your task execution pipeline.
106
+
107
+ ## Context Features
108
+
109
+ ### Key Normalization
110
+
111
+ All keys are automatically converted to symbols for consistent access:
112
+
113
+ ```ruby
114
+ SomeTask.call("user_id" => 123, :order_id => 456)
115
+
116
+ # Both accessible as symbols
117
+ context.user_id #=> 123
118
+ context.order_id #=> 456
119
+ ```
120
+
121
+ ### Nil Safety
122
+
123
+ Accessing undefined attributes returns `nil` instead of raising errors:
124
+
125
+ ```ruby
126
+ context.undefined_attribute #=> nil
127
+ context[:missing_key] #=> nil
128
+ ```
40
129
 
41
130
  > [!NOTE]
42
- > Attributes that are **NOT** loaded into the context will return `nil`.
131
+ > Context attributes that are **NOT** loaded will return `nil` rather than
132
+ > raising an error. This allows for graceful handling of optional parameters.
43
133
 
44
- ## Passing
134
+ ### Type Flexibility
45
135
 
46
- Context objects can be passed to other tasks which allows you to build small tasks
47
- that passes data around as part of a higher level task.
136
+ Context accepts any data type without restrictions:
48
137
 
49
138
  ```ruby
50
- # Within task:
51
- class ProcessOrderTask < CMDx::Task
139
+ context.string_value = "Order processed"
140
+ context.numeric_value = 42
141
+ context.array_value = [1, 2, 3]
142
+ context.hash_value = { total: 99.99, currency: "USD" }
143
+ context.object_value = User.find(123)
144
+ context.proc_value = -> { "dynamic value" }
145
+ ```
146
+
147
+ ## Data Sharing Between Tasks
148
+
149
+ Context objects can be passed between tasks, enabling data flow in complex
150
+ workflows:
151
+
152
+ ### Within Task Composition
153
+
154
+ ```ruby
155
+ class ProcessUserOrderTask < CMDx::Task
52
156
 
53
157
  def call
54
- SendEmailConfirmationTask.call(context)
158
+ # Subtasks inherit and modify the same context
159
+ validation_result = ValidateUserOrderTask.call(context)
160
+ throw!(validation_result) unless validation_result.success?
161
+
162
+ payment_result = ProcessOrderPaymentTask.call(context)
163
+ throw!(payment_result) unless payment_result.success?
164
+
165
+ # Context now contains data from all subtasks
166
+ context.order_validated #=> true (from ValidateUserOrderTask)
167
+ context.payment_processed #=> true (from ProcessOrderPaymentTask)
55
168
  end
56
169
 
57
170
  end
171
+ ```
172
+
173
+ ### After Task Completion
174
+
175
+ ```ruby
176
+ # Chain task results using context
177
+ validation_result = ValidateUserOrderTask.call(user_id: 123, order_id: 456)
178
+
179
+ if validation_result.success?
180
+ # Pass accumulated context to next task
181
+ process_result = ProcessUserOrderTask.call(validation_result)
182
+
183
+ # Continue chain with enriched context
184
+ notification_result = SendOrderNotificationTask.call(process_result.context)
185
+ end
186
+ ```
187
+
188
+ ### Workflow Processing
189
+
190
+ ```ruby
191
+ # Context maintains continuity across workflow operations
192
+ initial_context = {
193
+ user_id: 123,
194
+ action: "bulk_order_processing"
195
+ }
196
+
197
+ results = [
198
+ ValidateOrderDataTask.call(initial_context),
199
+ ProcessOrderPaymentTask.call(initial_context),
200
+ UpdateInventoryTask.call(initial_context)
201
+ ]
202
+
203
+ # All tasks share and modify the same context data
204
+ results.first.context.validation_completed #=> true
205
+ results.last.context.inventory_updated #=> true
206
+ ```
207
+
208
+ ## Result Object Context Passing
209
+
210
+ CMDx supports automatic context extraction when Result objects are passed to task
211
+ `new` or `call` methods. This enables seamless task chaining where the output of
212
+ one task becomes the input for the next, creating powerful workflow compositions.
213
+
214
+ ```ruby
215
+ # Chain tasks by passing Result objects
216
+ extraction_result = ExtractDataTask.call(source_id: 123)
217
+ processing_result = ProcessDataTask.call(extraction_result)
218
+
219
+ # Context flows automatically between tasks
220
+ processing_result.context.source_id #=> 123 (from first task)
221
+ processing_result.context.extracted_data #=> [data...] (from first task)
222
+ processing_result.context.extraction_time #=> 2024-01-01 10:00:00 (from first task)
223
+ processing_result.context.processed_data #=> [processed...] (from second task)
224
+ processing_result.context.processing_time #=> 2024-01-01 10:00:05 (from second task)
225
+ ```
226
+
227
+ ### Error Handling in Chains
228
+
229
+ Result object chaining works seamlessly with both `call` and `call!` methods:
230
+
231
+ ```ruby
232
+ # Non-raising version (returns failed results)
233
+ extraction_result = ExtractDataTask.call(source_id: 123)
234
+ if extraction_result.failed?
235
+ # Handle failure, but can still pass context to error handler
236
+ error_result = HandleErrorTask.call(extraction_result)
237
+ end
58
238
 
59
- # After call:
60
- result = ProcessOrderTask.call(email: "bob@bigcorp.com", order: order)
61
- NotifyPartnerWarehousesTask.call(result.ctx)
239
+ # Raising version (propagates exceptions)
240
+ begin
241
+ extraction_result = ExtractDataTask.call!(source_id: 123)
242
+ processing_result = ProcessDataTask.call!(extraction_result)
243
+ rescue CMDx::Failed => e
244
+ # Handle any failure in the chain
245
+ error_result = HandleErrorTask.call(e.result)
246
+ end
62
247
  ```
63
248
 
249
+ > [!TIP]
250
+ > Result object chaining is particularly powerful when combined with [Workflows](../workflows.md)
251
+ > processing, where multiple tasks can operate on shared context while maintaining
252
+ > individual result tracking.
253
+
254
+ ## Context Inspection
255
+
256
+ Context provides inspection methods for debugging and logging:
257
+
258
+ ```ruby
259
+ # Hash representation
260
+ context.to_h #=> { user_id: 123, order_id: 456, ... }
261
+
262
+ # Human-readable inspection
263
+ context.inspect #=> "#<CMDx::Context :user_id=123 :order_id=456>"
264
+
265
+ # Iteration
266
+ context.each_pair { |key, value| puts "#{key}: #{value}" }
267
+ ```
268
+
269
+ [Learn more](../../lib/cmdx/lazy_struct.rb)
270
+ about the `LazyStruct` public API that powers context functionality.
271
+
64
272
  ---
65
273
 
66
- - **Prev:** [Basics - Call](https://github.com/drexed/cmdx/blob/main/docs/basics/call.md)
67
- - **Next:** [Basics - Run](https://github.com/drexed/cmdx/blob/main/docs/basics/run.md)
274
+ - **Prev:** [Basics - Call](call.md)
275
+ - **Next:** [Basics - Chain](chain.md)
data/docs/basics/setup.md CHANGED
@@ -1,32 +1,105 @@
1
1
  # Basics - Setup
2
2
 
3
- A task represents a unit of work to execute. While `CMDx` offers a plethora
4
- of features, a `call` method is the only thing required to execute a task.
3
+ A task represents a unit of work to execute. Tasks are the core building blocks
4
+ of CMDx, encapsulating business logic within a structured, reusable object. While
5
+ CMDx offers extensive features like parameter validation, callbacks, and state tracking,
6
+ only a `call` method is required to create a functional task.
7
+
8
+ ## Table of Contents
9
+
10
+ - [TLDR](#tldr)
11
+ - [Basic Task Structure](#basic-task-structure)
12
+ - [Task Execution](#task-execution)
13
+ - [Inheritance and Application Tasks](#inheritance-and-application-tasks)
14
+ - [Generator](#generator)
15
+ - [Task Lifecycle](#task-lifecycle)
16
+
17
+ ## TLDR
18
+
19
+ - **Tasks** - Ruby classes that inherit from `CMDx::Task` with a `call` method
20
+ - **Structure** - Only `call` method required, everything else is optional
21
+ - **Execution** - Run with `MyTask.call(params)` to get a `CMDx::Result`
22
+ - **Generator** - Use `rails g cmdx:task MyTask` to create task templates
23
+ - **Single-use** - Tasks are frozen after execution, create new instances for each run
24
+
25
+ ## Basic Task Structure
5
26
 
6
27
  ```ruby
7
- class ProcessOrderTask < CMDx::Task
28
+ class ProcessUserOrderTask < CMDx::Task
8
29
 
9
30
  def call
10
- # Do work...
31
+ # Your business logic here
32
+ context.order = Order.find(context.order_id)
33
+ context.order.process!
11
34
  end
12
35
 
13
36
  private
14
37
 
15
- # Business logic...
38
+ # Support methods and business logic
16
39
 
17
40
  end
18
41
  ```
19
42
 
20
- > [!TIP]
21
- > While complexity designed into a task is up to the engineer, it's
22
- > suggested that tasks be small and composed into higher level tasks.
43
+ ## Task Execution
44
+
45
+ ```ruby
46
+ # Execute a task
47
+ result = ProcessUserOrderTask.call(order_id: 123)
48
+
49
+ # Access the result
50
+ result.success? #=> true
51
+ result.context.order #=> <Order id: 123>
52
+ ```
53
+
54
+ ## Inheritance and Application Tasks
55
+
56
+ In Rails applications, tasks typically inherit from an `ApplicationTask` base class:
57
+
58
+ ```ruby
59
+ # app/tasks/application_task.rb
60
+ class ApplicationTask < CMDx::Task
61
+ # Shared configuration and functionality
62
+ end
63
+
64
+ # app/tasks/process_user_order_task.rb
65
+ class ProcessUserOrderTask < ApplicationTask
66
+ def call
67
+ # Implementation
68
+ end
69
+ end
70
+ ```
23
71
 
24
72
  ## Generator
25
73
 
26
- Run `rails g cmdx:task [NAME]` to create a task template file under `app/cmds`.
27
- Tasks will inherit from `ApplicationTask` if available or fall back to `CMDx::Task`.
74
+ Rails applications can use the built-in generator to create task templates:
75
+
76
+ ```bash
77
+ rails g cmdx:task ProcessUserOrder
78
+ ```
79
+
80
+ This creates `app/tasks/process_user_order_task.rb` with:
81
+ - Proper inheritance from `ApplicationTask` (if available) or `CMDx::Task`
82
+ - Basic structure with parameter definitions
83
+ - Template implementation
84
+
85
+ > [!TIP]
86
+ > Use the generator to maintain consistent task structure and naming conventions across your application.
87
+
88
+ ## Task Lifecycle
89
+
90
+ Every task follows a predictable lifecycle:
91
+
92
+ 1. **Instantiation** - Task object created with context
93
+ 2. **Validation** - Parameters validated against definitions
94
+ 3. **Execution** - The `call` method runs business logic
95
+ 4. **Completion** - Result finalized with state and status
96
+ 5. **Freezing** - Task becomes immutable after execution
97
+
98
+ > [!IMPORTANT]
99
+ > Tasks are single-use objects. Once executed, they are frozen and cannot
100
+ > be called again. Create a new task instance for each execution.
28
101
 
29
102
  ---
30
103
 
31
- - **Prev:** [Configuration](https://github.com/drexed/cmdx/blob/main/docs/configuration.md)
32
- - **Next:** [Basics - Call](https://github.com/drexed/cmdx/blob/main/docs/basics/call.md)
104
+ - **Prev:** [Configuration](../configuration.md)
105
+ - **Next:** [Basics - Call](call.md)
data/docs/callbacks.md ADDED
@@ -0,0 +1,283 @@
1
+ # Callbacks
2
+
3
+ Callbacks (callbacks) provide precise control over task execution lifecycle, running custom logic at
4
+ specific transition points. Callback callables have access to the same context and result information
5
+ as the `call` method, enabling rich integration patterns.
6
+
7
+ ## Table of Contents
8
+
9
+ - [TLDR](#tldr)
10
+ - [Overview](#overview)
11
+ - [Callback Declaration](#callback-declaration)
12
+ - [Callback Classes](#callback-classes)
13
+ - [Available Callbacks](#available-callbacks)
14
+ - [Validation Callbacks](#validation-callbacks)
15
+ - [Execution Callbacks](#execution-callbacks)
16
+ - [State Callbacks](#state-callbacks)
17
+ - [Status Callbacks](#status-callbacks)
18
+ - [Outcome Callbacks](#outcome-callbacks)
19
+ - [Execution Order](#execution-order)
20
+ - [Conditional Execution](#conditional-execution)
21
+ - [Callback Inheritance](#callback-inheritance)
22
+
23
+ ## TLDR
24
+
25
+ - **Purpose** - Execute custom logic at specific points in task lifecycle
26
+ - **Declaration** - Use method names, procs, class instances, or blocks
27
+ - **Callback types** - Validation, execution, state, status, and outcome callbacks
28
+ - **Execution order** - Runs in precise lifecycle order (before_execution → validation → call → status → after_execution)
29
+ - **Conditional** - Support `:if` and `:unless` options for conditional execution
30
+ - **Inheritance** - Callbacks are inherited, perfect for global patterns
31
+
32
+ > [!TIP]
33
+ > Callbacks are inheritable, making them perfect for setting up global logic execution patterns like tracking markers, account plan checks, or logging standards.
34
+
35
+ ## Overview
36
+
37
+ Callbacks can be declared in multiple ways: method names, procs/lambdas, Callback class instances, or blocks.
38
+
39
+ ```ruby
40
+ class ProcessOrderTask < CMDx::Task
41
+ # Method name declaration
42
+ after_validation :verify_order_data
43
+
44
+ # Proc/lambda declaration
45
+ on_complete -> { send_telemetry_data }
46
+
47
+ # Callback class declaration
48
+ before_execution LoggingCallback.new(:debug)
49
+ on_success NotificationCallback.new([:email, :slack])
50
+
51
+ # Multiple callbacks for same event
52
+ on_success :increment_counter, :send_notification
53
+
54
+ # Conditional execution
55
+ on_failed :alert_support, if: :critical_order?
56
+ after_execution :cleanup_resources, unless: :preserve_data?
57
+
58
+ # Block declaration
59
+ before_execution do
60
+ context.processing_start = Time.now
61
+ end
62
+
63
+ def call
64
+ context.order = Order.find(order_id)
65
+ context.order.process!
66
+ end
67
+
68
+ private
69
+
70
+ def critical_order?
71
+ context.order.value > 10_000
72
+ end
73
+
74
+ def preserve_data?
75
+ Rails.env.development?
76
+ end
77
+ end
78
+ ```
79
+
80
+ ## Callback Classes
81
+
82
+ For complex callback logic or reusable patterns, you can create Callback classes similar to Middleware classes. Callback classes inherit from `CMDx::Callback` and implement the `call(task, callback_type)` method.
83
+
84
+ ### Creating Callback Classes
85
+
86
+ ```ruby
87
+ class NotificationCallback < CMDx::Callback
88
+ def initialize(channels)
89
+ @channels = Array(channels)
90
+ end
91
+
92
+ def call(task, callback_type)
93
+ return unless callback_type == :on_success
94
+
95
+ @channels.each do |channel|
96
+ NotificationService.send(channel, "Task #{task.class.name} completed")
97
+ end
98
+ end
99
+ end
100
+ ```
101
+
102
+ ### Registering Callback Classes
103
+
104
+ Callback classes can be registered using the `register` class method (recommended) or by directly calling the CallbackRegistry:
105
+
106
+ ```ruby
107
+ class ProcessOrderTask < CMDx::Task
108
+ # Recommended: Use the register class method
109
+ register :before_execution, LoggingCallback.new(:debug)
110
+ register :on_success, NotificationCallback.new([:email, :slack])
111
+ register :on_failure, :alert_admin, if: :critical?
112
+
113
+ # Alternative: Direct CallbackRegistry access (less common)
114
+ # cmd_callbacks.register(:after_execution, CleanupCallback.new)
115
+
116
+ # Traditional callback definitions still work alongside Callback classes
117
+ before_validation :validate_order_data
118
+ on_success :update_metrics
119
+
120
+ def call
121
+ context.order = Order.find(order_id)
122
+ context.order.process!
123
+ end
124
+
125
+ private
126
+
127
+ def critical?
128
+ context.order.value > 10_000
129
+ end
130
+ end
131
+ ```
132
+
133
+ ## Available Callbacks
134
+
135
+ ### Validation Callbacks
136
+
137
+ Execute around parameter validation:
138
+
139
+ - `before_validation` - Before parameter validation
140
+ - `after_validation` - After successful parameter validation
141
+
142
+ ### Execution Callbacks
143
+
144
+ Execute around task logic:
145
+
146
+ - `before_execution` - Before task logic begins
147
+ - `after_execution` - After task logic completes (success or failure)
148
+
149
+ ### State Callbacks
150
+
151
+ Execute based on execution state:
152
+
153
+ - `on_executing` - Task begins running
154
+ - `on_complete` - Task completes successfully
155
+ - `on_interrupted` - Task is halted (skip/failure)
156
+ - `on_executed` - Task finishes (complete or interrupted)
157
+
158
+ ### Status Callbacks
159
+
160
+ Execute based on execution status:
161
+
162
+ - `on_success` - Task succeeds
163
+ - `on_skipped` - Task is skipped
164
+ - `on_failed` - Task fails
165
+
166
+ ### Outcome Callbacks
167
+
168
+ Execute based on outcome classification:
169
+
170
+ - `on_good` - Positive outcomes (success or skipped)
171
+ - `on_bad` - Negative outcomes (skipped or failed)
172
+
173
+ ## Execution Order
174
+
175
+ Callbacks execute in precise order during task lifecycle:
176
+
177
+ > [!IMPORTANT]
178
+ > Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
179
+
180
+ ```ruby
181
+ 1. before_execution # Setup and preparation
182
+ 2. on_executing # Task begins running
183
+ 3. before_validation # Pre-validation setup
184
+ 4. after_validation # Post-validation logic
185
+ 5. [call method] # Your business logic
186
+ 6. on_[complete|interrupted] # Based on execution state
187
+ 7. on_executed # Task finished (any outcome)
188
+ 8. on_[success|skipped|failed] # Based on execution status
189
+ 9. on_[good|bad] # Based on outcome classification
190
+ 10. after_execution # Cleanup and finalization
191
+ ```
192
+
193
+ > [!IMPORTANT]
194
+ > Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
195
+
196
+ ## Conditional Execution
197
+
198
+ Callbacks support conditional execution through `:if` and `:unless` options:
199
+
200
+ | Option | Description |
201
+ | --------- | ----------- |
202
+ | `:if` | Execute callback only if condition is truthy |
203
+ | `:unless` | Execute callback only if condition is falsy |
204
+
205
+ ```ruby
206
+ class ProcessPaymentTask < CMDx::Task
207
+ # Method name condition
208
+ on_success :send_receipt, if: :email_enabled?
209
+
210
+ # Proc condition
211
+ on_failure :retry_payment, if: -> { retry_count < 3 }
212
+
213
+ # String condition (evaluated as method)
214
+ after_execution :log_metrics, unless: "Rails.env.test?"
215
+
216
+ # Multiple conditions
217
+ on_complete :expensive_operation, if: :production_env?, unless: :maintenance_mode?
218
+
219
+ private
220
+
221
+ def email_enabled?
222
+ context.user.email_notifications?
223
+ end
224
+
225
+ def production_env?
226
+ Rails.env.production?
227
+ end
228
+
229
+ def maintenance_mode?
230
+ SystemStatus.maintenance_mode?
231
+ end
232
+ end
233
+ ```
234
+
235
+ ## Callback Inheritance
236
+
237
+ Callbacks are inherited from parent classes, enabling application-wide patterns:
238
+
239
+ ```ruby
240
+ class ApplicationTask < CMDx::Task
241
+ before_execution :log_task_start # All tasks get execution logging
242
+ after_execution :log_task_end # All tasks get completion logging
243
+ on_failed :report_failure # All tasks get error reporting
244
+ on_success :track_success_metrics # All tasks get success tracking
245
+
246
+ private
247
+
248
+ def log_task_start
249
+ Rails.logger.info "Starting #{self.class.name}"
250
+ end
251
+
252
+ def log_task_end
253
+ Rails.logger.info "Finished #{self.class.name} in #{result.runtime}s"
254
+ end
255
+
256
+ def report_failure
257
+ ErrorReporter.notify(result.metadata)
258
+ end
259
+
260
+ def track_success_metrics
261
+ Metrics.increment("task.#{self.class.name.underscore}.success")
262
+ end
263
+ end
264
+
265
+ class ProcessOrderTask < ApplicationTask
266
+ before_validation :load_order # Specific to order processing
267
+ on_success :send_confirmation # Domain-specific success action
268
+ on_failed :refund_payment, if: :payment_captured? # Order-specific failure handling
269
+
270
+ def call
271
+ # Inherits all ApplicationTask callbacks plus order-specific ones
272
+ context.order.process!
273
+ end
274
+ end
275
+ ```
276
+
277
+ > [!TIP]
278
+ > Callbacks are inherited by subclasses, making them ideal for setting up global lifecycle patterns across all tasks in your application.
279
+
280
+ ---
281
+
282
+ - **Prev:** [Parameters - Defaults](parameters/defaults.md)
283
+ - **Prev:** [Middlewares](middlewares.md)