cmdx 1.1.2 → 1.5.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.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/prompts/docs.md +4 -1
- data/.cursor/prompts/llms.md +20 -0
- data/.cursor/prompts/rspec.md +4 -1
- data/.cursor/prompts/yardoc.md +3 -2
- data/.cursor/rules/cursor-instructions.mdc +55 -1
- data/.irbrc +6 -0
- data/.rubocop.yml +29 -18
- data/CHANGELOG.md +11 -132
- data/LLM.md +3317 -0
- data/README.md +68 -44
- data/docs/attributes/coercions.md +162 -0
- data/docs/attributes/defaults.md +90 -0
- data/docs/attributes/definitions.md +281 -0
- data/docs/attributes/naming.md +78 -0
- data/docs/attributes/validations.md +309 -0
- data/docs/basics/chain.md +56 -249
- data/docs/basics/context.md +56 -289
- data/docs/basics/execution.md +114 -0
- data/docs/basics/setup.md +37 -334
- data/docs/callbacks.md +89 -467
- data/docs/deprecation.md +91 -174
- data/docs/getting_started.md +212 -202
- data/docs/internationalization.md +11 -647
- data/docs/interruptions/exceptions.md +23 -198
- data/docs/interruptions/faults.md +71 -151
- data/docs/interruptions/halt.md +109 -186
- data/docs/logging.md +44 -256
- data/docs/middlewares.md +113 -426
- data/docs/outcomes/result.md +81 -228
- data/docs/outcomes/states.md +33 -221
- data/docs/outcomes/statuses.md +21 -311
- data/docs/tips_and_tricks.md +120 -70
- data/docs/workflows.md +99 -283
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/attribute.rb +229 -0
- data/lib/cmdx/attribute_registry.rb +94 -0
- data/lib/cmdx/attribute_value.rb +193 -0
- data/lib/cmdx/callback_registry.rb +69 -77
- data/lib/cmdx/chain.rb +56 -73
- data/lib/cmdx/coercion_registry.rb +52 -68
- data/lib/cmdx/coercions/array.rb +19 -18
- data/lib/cmdx/coercions/big_decimal.rb +20 -24
- data/lib/cmdx/coercions/boolean.rb +26 -25
- data/lib/cmdx/coercions/complex.rb +21 -22
- data/lib/cmdx/coercions/date.rb +25 -23
- data/lib/cmdx/coercions/date_time.rb +24 -25
- data/lib/cmdx/coercions/float.rb +25 -22
- data/lib/cmdx/coercions/hash.rb +31 -32
- data/lib/cmdx/coercions/integer.rb +30 -24
- data/lib/cmdx/coercions/rational.rb +29 -24
- data/lib/cmdx/coercions/string.rb +19 -22
- data/lib/cmdx/coercions/symbol.rb +37 -0
- data/lib/cmdx/coercions/time.rb +26 -25
- data/lib/cmdx/configuration.rb +49 -108
- data/lib/cmdx/context.rb +222 -44
- data/lib/cmdx/deprecator.rb +61 -0
- data/lib/cmdx/errors.rb +42 -252
- data/lib/cmdx/exceptions.rb +39 -0
- data/lib/cmdx/faults.rb +78 -39
- data/lib/cmdx/freezer.rb +51 -0
- data/lib/cmdx/identifier.rb +30 -0
- data/lib/cmdx/locale.rb +52 -0
- data/lib/cmdx/log_formatters/json.rb +21 -22
- data/lib/cmdx/log_formatters/key_value.rb +20 -22
- data/lib/cmdx/log_formatters/line.rb +15 -22
- data/lib/cmdx/log_formatters/logstash.rb +22 -23
- data/lib/cmdx/log_formatters/raw.rb +16 -22
- data/lib/cmdx/middleware_registry.rb +70 -74
- data/lib/cmdx/middlewares/correlate.rb +90 -54
- data/lib/cmdx/middlewares/runtime.rb +58 -0
- data/lib/cmdx/middlewares/timeout.rb +48 -68
- data/lib/cmdx/railtie.rb +12 -45
- data/lib/cmdx/result.rb +229 -314
- data/lib/cmdx/task.rb +194 -366
- data/lib/cmdx/utils/call.rb +49 -0
- data/lib/cmdx/utils/condition.rb +71 -0
- data/lib/cmdx/utils/format.rb +61 -0
- data/lib/cmdx/validator_registry.rb +63 -72
- data/lib/cmdx/validators/exclusion.rb +38 -67
- data/lib/cmdx/validators/format.rb +48 -49
- data/lib/cmdx/validators/inclusion.rb +43 -74
- data/lib/cmdx/validators/length.rb +101 -162
- data/lib/cmdx/validators/numeric.rb +95 -170
- data/lib/cmdx/validators/presence.rb +37 -50
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx/worker.rb +178 -0
- data/lib/cmdx/workflow.rb +85 -81
- data/lib/cmdx.rb +19 -13
- data/lib/generators/cmdx/install_generator.rb +14 -13
- data/lib/generators/cmdx/task_generator.rb +25 -50
- data/lib/generators/cmdx/templates/install.rb +11 -46
- data/lib/generators/cmdx/templates/task.rb.tt +3 -2
- data/lib/locales/en.yml +18 -4
- data/src/cmdx-logo.png +0 -0
- metadata +32 -116
- data/docs/ai_prompts.md +0 -393
- data/docs/basics/call.md +0 -317
- data/docs/configuration.md +0 -344
- data/docs/parameters/coercions.md +0 -396
- data/docs/parameters/defaults.md +0 -335
- data/docs/parameters/definitions.md +0 -446
- data/docs/parameters/namespacing.md +0 -378
- data/docs/parameters/validations.md +0 -405
- data/docs/testing.md +0 -553
- data/lib/cmdx/callback.rb +0 -53
- data/lib/cmdx/chain_inspector.rb +0 -56
- data/lib/cmdx/chain_serializer.rb +0 -63
- data/lib/cmdx/coercion.rb +0 -57
- data/lib/cmdx/coercions/virtual.rb +0 -29
- data/lib/cmdx/core_ext/hash.rb +0 -83
- data/lib/cmdx/core_ext/module.rb +0 -98
- data/lib/cmdx/core_ext/object.rb +0 -125
- data/lib/cmdx/correlator.rb +0 -122
- data/lib/cmdx/error.rb +0 -67
- data/lib/cmdx/fault.rb +0 -140
- data/lib/cmdx/immutator.rb +0 -52
- data/lib/cmdx/lazy_struct.rb +0 -246
- data/lib/cmdx/log_formatters/pretty_json.rb +0 -40
- data/lib/cmdx/log_formatters/pretty_key_value.rb +0 -38
- data/lib/cmdx/log_formatters/pretty_line.rb +0 -41
- data/lib/cmdx/logger.rb +0 -49
- data/lib/cmdx/logger_ansi.rb +0 -68
- data/lib/cmdx/logger_serializer.rb +0 -116
- data/lib/cmdx/middleware.rb +0 -70
- data/lib/cmdx/parameter.rb +0 -312
- data/lib/cmdx/parameter_evaluator.rb +0 -231
- data/lib/cmdx/parameter_inspector.rb +0 -66
- data/lib/cmdx/parameter_registry.rb +0 -106
- data/lib/cmdx/parameter_serializer.rb +0 -59
- data/lib/cmdx/result_ansi.rb +0 -71
- data/lib/cmdx/result_inspector.rb +0 -71
- data/lib/cmdx/result_logger.rb +0 -59
- data/lib/cmdx/result_serializer.rb +0 -104
- data/lib/cmdx/rspec/matchers.rb +0 -28
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +0 -42
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +0 -94
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +0 -57
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +0 -87
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +0 -51
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +0 -58
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +0 -59
- data/lib/cmdx/rspec/result_matchers/have_context.rb +0 -86
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +0 -54
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +0 -52
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +0 -114
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +0 -66
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +0 -64
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +0 -78
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +0 -76
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +0 -62
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +0 -85
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +0 -68
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +0 -92
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +0 -46
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +0 -181
- data/lib/cmdx/task_deprecator.rb +0 -58
- data/lib/cmdx/task_processor.rb +0 -246
- data/lib/cmdx/task_serializer.rb +0 -57
- data/lib/cmdx/utils/ansi_color.rb +0 -73
- data/lib/cmdx/utils/log_timestamp.rb +0 -36
- data/lib/cmdx/utils/monotonic_runtime.rb +0 -34
- data/lib/cmdx/utils/name_affix.rb +0 -52
- data/lib/cmdx/validator.rb +0 -57
- data/lib/generators/cmdx/templates/workflow.rb.tt +0 -7
- data/lib/generators/cmdx/workflow_generator.rb +0 -84
- data/lib/locales/ar.yml +0 -35
- data/lib/locales/cs.yml +0 -35
- data/lib/locales/da.yml +0 -35
- data/lib/locales/de.yml +0 -35
- data/lib/locales/el.yml +0 -35
- data/lib/locales/es.yml +0 -35
- data/lib/locales/fi.yml +0 -35
- data/lib/locales/fr.yml +0 -35
- data/lib/locales/he.yml +0 -35
- data/lib/locales/hi.yml +0 -35
- data/lib/locales/it.yml +0 -35
- data/lib/locales/ja.yml +0 -35
- data/lib/locales/ko.yml +0 -35
- data/lib/locales/nl.yml +0 -35
- data/lib/locales/no.yml +0 -35
- data/lib/locales/pl.yml +0 -35
- data/lib/locales/pt.yml +0 -35
- data/lib/locales/ru.yml +0 -35
- data/lib/locales/sv.yml +0 -35
- data/lib/locales/th.yml +0 -35
- data/lib/locales/tr.yml +0 -35
- data/lib/locales/vi.yml +0 -35
- data/lib/locales/zh.yml +0 -35
data/docs/callbacks.md
CHANGED
@@ -1,546 +1,168 @@
|
|
1
1
|
# Callbacks
|
2
2
|
|
3
|
-
Callbacks provide precise control over task execution lifecycle, running custom logic at specific transition points. Callback callables have access to the same context and result information as the `
|
4
|
-
|
5
|
-
## Table of Contents
|
6
|
-
|
7
|
-
- [TLDR](#tldr)
|
8
|
-
- [Callback Declaration](#callback-declaration)
|
9
|
-
- [Callback Classes](#callback-classes)
|
10
|
-
- [Available Callbacks](#available-callbacks)
|
11
|
-
- [Validation Callbacks](#validation-callbacks)
|
12
|
-
- [Execution Callbacks](#execution-callbacks)
|
13
|
-
- [State Callbacks](#state-callbacks)
|
14
|
-
- [Status Callbacks](#status-callbacks)
|
15
|
-
- [Outcome Callbacks](#outcome-callbacks)
|
16
|
-
- [Execution Order](#execution-order)
|
17
|
-
- [Conditional Execution](#conditional-execution)
|
18
|
-
- [Error Handling](#error-handling)
|
19
|
-
- [Callback Inheritance](#callback-inheritance)
|
20
|
-
|
21
|
-
## TLDR
|
22
|
-
|
23
|
-
```ruby
|
24
|
-
# Method name callbacks
|
25
|
-
after_validation :verify_order_data
|
26
|
-
on_success :send_notification
|
27
|
-
|
28
|
-
# Proc/lambda callbacks
|
29
|
-
on_complete -> { send_telemetry_data }
|
30
|
-
|
31
|
-
# Callback class instances
|
32
|
-
before_execution LoggingCallback.new(:debug)
|
33
|
-
|
34
|
-
# Conditional execution
|
35
|
-
on_failed :alert_support, if: :critical_order?
|
36
|
-
after_execution :cleanup, unless: :preserve_data?
|
37
|
-
|
38
|
-
# Multiple callbacks for same event
|
39
|
-
on_success :increment_counter, :send_notification
|
40
|
-
```
|
3
|
+
Callbacks provide precise control over task execution lifecycle, running custom logic at specific transition points. Callback callables have access to the same context and result information as the `execute` method, enabling rich integration patterns.
|
41
4
|
|
42
5
|
> [!IMPORTANT]
|
43
|
-
> Callbacks execute in
|
6
|
+
> Callbacks execute in the order they are declared within each hook type. Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
|
44
7
|
|
45
|
-
##
|
8
|
+
## Table of Contents
|
46
9
|
|
47
|
-
|
48
|
-
|
10
|
+
- [Available Callbacks](#available-callbacks)
|
11
|
+
- [Declarations](#declarations)
|
12
|
+
- [Symbol References](#symbol-references)
|
13
|
+
- [Proc or Lambda](#proc-or-lambda)
|
14
|
+
- [Class or Module](#class-or-module)
|
15
|
+
- [Conditional Execution](#conditional-execution)
|
16
|
+
- [Callback Removal](#callback-removal)
|
49
17
|
|
50
|
-
|
18
|
+
## Available Callbacks
|
51
19
|
|
52
|
-
|
53
|
-
|--------|-------------|---------|
|
54
|
-
| Method name | References instance method | `on_success :send_email` |
|
55
|
-
| Proc/Lambda | Inline callable | `on_failed -> { alert_team }` |
|
56
|
-
| Callback class | Reusable class instance | `before_execution LoggerCallback.new` |
|
57
|
-
| Block | Inline block | `on_success { increment_counter }` |
|
20
|
+
Callbacks execute in precise lifecycle order. Here is the complete execution sequence:
|
58
21
|
|
59
22
|
```ruby
|
60
|
-
|
61
|
-
|
62
|
-
before_validation :load_order
|
63
|
-
after_validation :verify_inventory
|
64
|
-
|
65
|
-
# Proc/lambda
|
66
|
-
on_executing -> { context.start_time = Time.current }
|
67
|
-
on_complete lambda { Metrics.increment('orders.processed') }
|
68
|
-
|
69
|
-
# Callback class
|
70
|
-
before_execution AuditCallback.new(action: :process_order)
|
71
|
-
on_success NotificationCallback.new(channels: [:email, :slack])
|
72
|
-
|
73
|
-
# Block
|
74
|
-
on_failed do
|
75
|
-
ErrorReporter.notify(
|
76
|
-
error: result.metadata[:error],
|
77
|
-
order_id: context.order_id,
|
78
|
-
user_id: context.user_id
|
79
|
-
)
|
80
|
-
end
|
23
|
+
1. before_validation # Pre-validation setup
|
24
|
+
2. before_execution # Setup and preparation
|
81
25
|
|
82
|
-
|
83
|
-
on_success :update_inventory, :send_confirmation, :log_success
|
84
|
-
|
85
|
-
def call
|
86
|
-
context.order = Order.find(context.order_id)
|
87
|
-
context.order.process!
|
88
|
-
end
|
89
|
-
|
90
|
-
private
|
26
|
+
# --- Task#work executed ---
|
91
27
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
def verify_inventory
|
97
|
-
raise "Insufficient inventory" unless context.order.items_available?
|
98
|
-
end
|
99
|
-
end
|
28
|
+
3. on_[complete|interrupted] # Based on execution state
|
29
|
+
4. on_executed # Task finished (any outcome)
|
30
|
+
5. on_[success|skipped|failed] # Based on execution status
|
31
|
+
6. on_[good|bad] # Based on outcome classification
|
100
32
|
```
|
101
33
|
|
102
|
-
##
|
34
|
+
## Declarations
|
103
35
|
|
104
|
-
|
105
|
-
> Create reusable Callback classes for complex logic or cross-cutting concerns. Callback classes inherit from `CMDx::Callback` and implement `call(task, type)`.
|
36
|
+
### Symbol References
|
106
37
|
|
107
|
-
|
108
|
-
class AuditCallback < CMDx::Callback
|
109
|
-
def initialize(action:, level: :info)
|
110
|
-
@action = action
|
111
|
-
@level = level
|
112
|
-
end
|
113
|
-
|
114
|
-
def call(task, type)
|
115
|
-
AuditLogger.log(
|
116
|
-
level: @level,
|
117
|
-
action: @action,
|
118
|
-
task: task.class.name,
|
119
|
-
callback_type: type,
|
120
|
-
user_id: task.context.current_user&.id,
|
121
|
-
timestamp: Time.current
|
122
|
-
)
|
123
|
-
end
|
124
|
-
end
|
38
|
+
Reference instance methods by symbol for simple callback logic:
|
125
39
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
@template = template
|
130
|
-
end
|
40
|
+
```ruby
|
41
|
+
class ProcessBooking < CMDx::Task
|
42
|
+
before_execution :find_reservation
|
131
43
|
|
132
|
-
|
133
|
-
|
44
|
+
# Batch declarations (works for any type)
|
45
|
+
on_complete :notify_guest, :update_availability
|
134
46
|
|
135
|
-
|
136
|
-
|
137
|
-
channel: channel,
|
138
|
-
template: @template || default_template(type),
|
139
|
-
data: extract_notification_data(task)
|
140
|
-
)
|
141
|
-
end
|
47
|
+
def work
|
48
|
+
# Your logic here...
|
142
49
|
end
|
143
50
|
|
144
51
|
private
|
145
52
|
|
146
|
-
def
|
147
|
-
|
53
|
+
def find_reservation
|
54
|
+
@reservation ||= Reservation.find(context.reservation_id)
|
148
55
|
end
|
149
56
|
|
150
|
-
def
|
151
|
-
|
57
|
+
def notify_guest
|
58
|
+
GuestNotifier.call(context.guest, result)
|
152
59
|
end
|
153
60
|
|
154
|
-
def
|
155
|
-
|
156
|
-
task_name: task.class.name,
|
157
|
-
status: task.result.status,
|
158
|
-
runtime: task.result.runtime,
|
159
|
-
context: task.context.to_h.except(:sensitive_data)
|
160
|
-
}
|
61
|
+
def update_availability
|
62
|
+
AvailabilityService.update(context.room_ids, result)
|
161
63
|
end
|
162
64
|
end
|
163
65
|
```
|
164
66
|
|
165
|
-
|
166
|
-
|
167
|
-
### Validation Callbacks
|
67
|
+
### Proc or Lambda
|
168
68
|
|
169
|
-
|
170
|
-
|
171
|
-
| Callback | Timing | Description |
|
172
|
-
|----------|--------|-------------|
|
173
|
-
| `before_validation` | Before validation | Setup validation context |
|
174
|
-
| `after_validation` | After successful validation | Post-validation logic |
|
69
|
+
Use anonymous functions for inline callback logic:
|
175
70
|
|
176
71
|
```ruby
|
177
|
-
class
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
required :email, type: :string
|
182
|
-
required :plan, type: :string
|
183
|
-
|
184
|
-
def call
|
185
|
-
User.create!(email: email, plan: plan)
|
186
|
-
end
|
72
|
+
class ProcessBooking < CMDx::Task
|
73
|
+
# Proc
|
74
|
+
on_interrupted proc { |task| ReservationSystem.pause! }
|
187
75
|
|
188
|
-
|
189
|
-
|
190
|
-
def normalize_email
|
191
|
-
context.email = email.downcase.strip
|
192
|
-
end
|
193
|
-
|
194
|
-
def check_user_limits
|
195
|
-
current_users = User.where(plan: plan).count
|
196
|
-
plan_limit = Plan.find_by(name: plan).user_limit
|
197
|
-
|
198
|
-
if current_users >= plan_limit
|
199
|
-
throw(:skip, reason: "Plan user limit reached")
|
200
|
-
end
|
201
|
-
end
|
76
|
+
# Lambda
|
77
|
+
on_complete -> { ReservationSystem.resume! }
|
202
78
|
end
|
203
79
|
```
|
204
80
|
|
205
|
-
###
|
81
|
+
### Class or Module
|
206
82
|
|
207
|
-
|
208
|
-
|
209
|
-
| Callback | Timing | Description |
|
210
|
-
|----------|--------|-------------|
|
211
|
-
| `before_execution` | Before `call` method | Setup and preparation |
|
212
|
-
| `after_execution` | After `call` completes | Cleanup and finalization |
|
83
|
+
Implement reusable callback logic in dedicated classes:
|
213
84
|
|
214
85
|
```ruby
|
215
|
-
class
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
private
|
224
|
-
|
225
|
-
def acquire_payment_lock
|
226
|
-
context.lock_key = "payment:#{context.payment_id}"
|
227
|
-
Redis.current.set(context.lock_key, "locked", ex: 300)
|
228
|
-
end
|
229
|
-
|
230
|
-
def release_payment_lock
|
231
|
-
Redis.current.del(context.lock_key) if context.lock_key
|
86
|
+
class BookingConfirmationCallback
|
87
|
+
def call(task)
|
88
|
+
if task.result.success?
|
89
|
+
MessagingApi.send_confirmation(task.context.guest)
|
90
|
+
else
|
91
|
+
MessagingApi.send_issue_alert(task.context.manager)
|
92
|
+
end
|
232
93
|
end
|
233
94
|
end
|
234
|
-
```
|
235
95
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
| Callback | Condition | Description |
|
241
|
-
|----------|-----------|-------------|
|
242
|
-
| `on_executing` | Task begins running | Track execution start |
|
243
|
-
| `on_complete` | Task completes successfully | Handle successful completion |
|
244
|
-
| `on_interrupted` | Task is halted (skip/failure) | Handle interruptions |
|
245
|
-
| `on_executed` | Task finishes (any outcome) | Post-execution logic |
|
246
|
-
|
247
|
-
### Status Callbacks
|
248
|
-
|
249
|
-
Execute based on execution status:
|
250
|
-
|
251
|
-
| Callback | Status | Description |
|
252
|
-
|----------|--------|-------------|
|
253
|
-
| `on_success` | Task succeeds | Handle success |
|
254
|
-
| `on_skipped` | Task is skipped | Handle skips |
|
255
|
-
| `on_failed` | Task fails | Handle failures |
|
256
|
-
|
257
|
-
### Outcome Callbacks
|
258
|
-
|
259
|
-
Execute based on outcome classification:
|
260
|
-
|
261
|
-
| Callback | Outcomes | Description |
|
262
|
-
|----------|----------|-------------|
|
263
|
-
| `on_good` | Success or skipped | Positive outcomes |
|
264
|
-
| `on_bad` | Failed | Negative outcomes |
|
265
|
-
|
266
|
-
```ruby
|
267
|
-
class EmailCampaignTask < CMDx::Task
|
268
|
-
on_executing -> { Metrics.increment('campaigns.started') }
|
269
|
-
on_complete :track_completion
|
270
|
-
on_interrupted :handle_interruption
|
271
|
-
|
272
|
-
on_success :schedule_followup
|
273
|
-
on_skipped :log_skip_reason
|
274
|
-
on_failed :alert_marketing_team
|
275
|
-
|
276
|
-
on_good -> { Metrics.increment('campaigns.positive_outcome') }
|
277
|
-
on_bad :create_incident_ticket
|
278
|
-
|
279
|
-
def call
|
280
|
-
EmailService.send_campaign(context.campaign_data)
|
281
|
-
end
|
282
|
-
|
283
|
-
private
|
284
|
-
|
285
|
-
def track_completion
|
286
|
-
Campaign.find(context.campaign_id).update!(
|
287
|
-
sent_at: Time.current,
|
288
|
-
recipient_count: context.recipients.size
|
289
|
-
)
|
290
|
-
end
|
96
|
+
class ProcessBooking < CMDx::Task
|
97
|
+
# Class or Module
|
98
|
+
on_success BookingConfirmationCallback
|
291
99
|
|
292
|
-
|
293
|
-
|
294
|
-
end
|
100
|
+
# Instance
|
101
|
+
on_interrupted BookingConfirmationCallback.new
|
295
102
|
end
|
296
103
|
```
|
297
104
|
|
298
|
-
|
299
|
-
|
300
|
-
> [!IMPORTANT]
|
301
|
-
> Callbacks execute in precise lifecycle order. Multiple callbacks of the same type execute in declaration order (FIFO: first in, first out).
|
302
|
-
|
303
|
-
```ruby
|
304
|
-
1. before_execution # Setup and preparation
|
305
|
-
2. on_executing # Task begins running
|
306
|
-
3. before_validation # Pre-validation setup
|
307
|
-
4. after_validation # Post-validation logic
|
308
|
-
5. [call method] # Your business logic
|
309
|
-
6. on_[complete|interrupted] # Based on execution state
|
310
|
-
7. on_executed # Task finished (any outcome)
|
311
|
-
8. on_[success|skipped|failed] # Based on execution status
|
312
|
-
9. on_[good|bad] # Based on outcome classification
|
313
|
-
10. after_execution # Cleanup and finalization
|
314
|
-
```
|
315
|
-
|
316
|
-
## Conditional Execution
|
317
|
-
|
318
|
-
> [!TIP]
|
319
|
-
> Use `:if` and `:unless` options for conditional callback execution. Conditions can be method names, procs, or strings.
|
105
|
+
### Conditional Execution
|
320
106
|
|
321
|
-
|
322
|
-
|--------|-------------|---------|
|
323
|
-
| `:if` | Execute if condition is truthy | `if: :production_env?` |
|
324
|
-
| `:unless` | Execute if condition is falsy | `unless: :maintenance_mode?` |
|
107
|
+
Control callback execution with conditional logic:
|
325
108
|
|
326
109
|
```ruby
|
327
|
-
class
|
328
|
-
|
329
|
-
|
330
|
-
on_failed :retry_payment, unless: :max_retries_reached?
|
331
|
-
|
332
|
-
# Proc conditions
|
333
|
-
after_execution :log_metrics, if: -> { Rails.env.production? }
|
334
|
-
on_success :expensive_operation, unless: -> { SystemStatus.overloaded? }
|
335
|
-
|
336
|
-
# String conditions (evaluated as methods)
|
337
|
-
on_complete :update_analytics, if: "tracking_enabled?"
|
338
|
-
|
339
|
-
# Multiple conditions
|
340
|
-
on_failed :escalate_to_support, if: :critical_order?, unless: :business_hours?
|
341
|
-
|
342
|
-
# Complex conditional logic
|
343
|
-
on_success :trigger_automation, if: :automation_conditions_met?
|
344
|
-
|
345
|
-
def call
|
346
|
-
Order.process!(context.order_data)
|
347
|
-
end
|
348
|
-
|
349
|
-
private
|
350
|
-
|
351
|
-
def email_enabled?
|
352
|
-
context.user.email_notifications? && !context.user.email.blank?
|
353
|
-
end
|
354
|
-
|
355
|
-
def max_retries_reached?
|
356
|
-
context.retry_count >= 3
|
357
|
-
end
|
358
|
-
|
359
|
-
def critical_order?
|
360
|
-
context.order_value > 10_000 || context.priority == :high
|
361
|
-
end
|
362
|
-
|
363
|
-
def business_hours?
|
364
|
-
Time.current.hour.between?(9, 17) && Time.current.weekday?
|
365
|
-
end
|
366
|
-
|
367
|
-
def automation_conditions_met?
|
368
|
-
context.order_type == :subscription &&
|
369
|
-
context.user.plan.automation_enabled? &&
|
370
|
-
!SystemStatus.maintenance_mode?
|
110
|
+
class MessagingPermissionCheck
|
111
|
+
def call(task)
|
112
|
+
task.context.guest.can?(:receive_messages)
|
371
113
|
end
|
372
114
|
end
|
373
|
-
```
|
374
115
|
|
375
|
-
|
116
|
+
class ProcessBooking < CMDx::Task
|
117
|
+
# If and/or Unless
|
118
|
+
before_execution :notify_guest, if: :messaging_enabled?, unless: :messaging_blocked?
|
376
119
|
|
377
|
-
|
378
|
-
|
120
|
+
# Proc
|
121
|
+
on_failure :increment_failure, if: ->(task) { Rails.env.production? && task.class.name.include?("Legacy") }
|
379
122
|
|
380
|
-
|
123
|
+
# Lambda
|
124
|
+
on_success :ping_housekeeping, if: proc { |task| task.context.rooms_need_cleaning? }
|
381
125
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
126
|
+
# Class or Module
|
127
|
+
on_complete :send_confirmation, unless: MessagingPermissionCheck
|
128
|
+
|
129
|
+
# Instance
|
130
|
+
on_complete :send_confirmation, if: MessagingPermissionCheck.new
|
387
131
|
|
388
|
-
def
|
389
|
-
|
132
|
+
def work
|
133
|
+
# Your logic here...
|
390
134
|
end
|
391
135
|
|
392
136
|
private
|
393
137
|
|
394
|
-
def
|
395
|
-
|
396
|
-
context.processor = ProcessorService.initialize_secure_processor
|
138
|
+
def messaging_enabled?
|
139
|
+
context.guest.messaging_preference.present?
|
397
140
|
end
|
398
141
|
|
399
|
-
def
|
400
|
-
|
401
|
-
NotificationService.send(context.notification_data)
|
402
|
-
rescue NotificationService::Error => e
|
403
|
-
Rails.logger.warn "Notification failed: #{e.message}"
|
404
|
-
# Don't re-raise - allow other callbacks to continue
|
405
|
-
end
|
406
|
-
|
407
|
-
def cleanup_resources
|
408
|
-
# Cleanup callback - always handle errors
|
409
|
-
context.processor&.cleanup
|
410
|
-
rescue => e
|
411
|
-
Rails.logger.error "Cleanup failed: #{e.message}"
|
412
|
-
# Log but don't re-raise
|
142
|
+
def messaging_blocked?
|
143
|
+
context.guest.communication_status == :blocked
|
413
144
|
end
|
414
145
|
end
|
415
146
|
```
|
416
147
|
|
417
|
-
|
418
|
-
|
419
|
-
```ruby
|
420
|
-
class ResilientCallback < CMDx::Callback
|
421
|
-
def initialize(callback_proc, isolate: false)
|
422
|
-
@callback_proc = callback_proc
|
423
|
-
@isolate = isolate
|
424
|
-
end
|
148
|
+
## Callback Removal
|
425
149
|
|
426
|
-
|
427
|
-
if @isolate
|
428
|
-
begin
|
429
|
-
@callback_proc.call(task, type)
|
430
|
-
rescue => e
|
431
|
-
Rails.logger.warn "Isolated callback failed: #{e.message}"
|
432
|
-
end
|
433
|
-
else
|
434
|
-
@callback_proc.call(task, type)
|
435
|
-
end
|
436
|
-
end
|
437
|
-
end
|
150
|
+
Remove callbacks at runtime for dynamic behavior control:
|
438
151
|
|
439
|
-
|
440
|
-
|
441
|
-
before_execution :validate_payment_method
|
442
|
-
|
443
|
-
# Isolated non-critical callback
|
444
|
-
on_success ResilientCallback.new(
|
445
|
-
-> (task, type) { AnalyticsService.track_order(task.context.order_id) },
|
446
|
-
isolate: true
|
447
|
-
)
|
448
|
-
|
449
|
-
def call
|
450
|
-
Order.process!(context.order_data)
|
451
|
-
end
|
452
|
-
end
|
453
|
-
```
|
454
|
-
|
455
|
-
## Callback Inheritance
|
456
|
-
|
457
|
-
> [!NOTE]
|
458
|
-
> Callbacks are inherited from parent classes, enabling application-wide patterns. Child classes can add additional callbacks or override inherited behavior.
|
152
|
+
> [!IMPORTANT]
|
153
|
+
> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
|
459
154
|
|
460
155
|
```ruby
|
461
|
-
class
|
462
|
-
#
|
463
|
-
before_execution :
|
464
|
-
after_execution :log_task_end
|
465
|
-
|
466
|
-
# Global error handling
|
467
|
-
on_failed :report_failure
|
468
|
-
|
469
|
-
# Global metrics
|
470
|
-
on_success :track_success_metrics
|
471
|
-
on_executed :track_execution_metrics
|
472
|
-
|
473
|
-
private
|
474
|
-
|
475
|
-
def log_task_start
|
476
|
-
Rails.logger.info "Starting #{self.class.name} with context: #{context.to_h.except(:sensitive_data)}"
|
477
|
-
end
|
478
|
-
|
479
|
-
def log_task_end
|
480
|
-
Rails.logger.info "Finished #{self.class.name} in #{result.runtime}ms with status: #{result.status}"
|
481
|
-
end
|
482
|
-
|
483
|
-
def report_failure
|
484
|
-
ErrorReporter.notify(
|
485
|
-
task: self.class.name,
|
486
|
-
error: result.metadata[:reason],
|
487
|
-
context: context.to_h.except(:sensitive_data),
|
488
|
-
backtrace: result.metadata[:backtrace]
|
489
|
-
)
|
490
|
-
end
|
156
|
+
class ProcessBooking < CMDx::Task
|
157
|
+
# Symbol
|
158
|
+
deregister :callback, :before_execution, :notify_guest
|
491
159
|
|
492
|
-
|
493
|
-
|
494
|
-
end
|
495
|
-
|
496
|
-
def track_execution_metrics
|
497
|
-
Metrics.histogram("task.#{self.class.name.underscore}.runtime", result.runtime)
|
498
|
-
end
|
499
|
-
end
|
500
|
-
|
501
|
-
class ProcessPaymentTask < ApplicationTask
|
502
|
-
# Inherits all ApplicationTask callbacks
|
503
|
-
# Plus payment-specific callbacks
|
504
|
-
|
505
|
-
before_validation :load_payment_method
|
506
|
-
on_success :send_receipt
|
507
|
-
on_failed :refund_payment, if: :payment_captured?
|
508
|
-
|
509
|
-
def call
|
510
|
-
# Inherits global logging, error handling, and metrics
|
511
|
-
# Plus payment-specific behavior
|
512
|
-
PaymentProcessor.charge(context.payment_data)
|
513
|
-
end
|
514
|
-
|
515
|
-
private
|
516
|
-
|
517
|
-
def load_payment_method
|
518
|
-
context.payment_method = PaymentMethod.find(context.payment_method_id)
|
519
|
-
end
|
520
|
-
|
521
|
-
def send_receipt
|
522
|
-
ReceiptService.send(
|
523
|
-
user: context.user,
|
524
|
-
payment: context.payment,
|
525
|
-
template: :payment_success
|
526
|
-
)
|
527
|
-
end
|
528
|
-
|
529
|
-
def payment_captured?
|
530
|
-
context.payment&.status == :captured
|
531
|
-
end
|
532
|
-
|
533
|
-
def refund_payment
|
534
|
-
RefundService.process(
|
535
|
-
payment: context.payment,
|
536
|
-
reason: :task_failure,
|
537
|
-
amount: context.payment.amount
|
538
|
-
)
|
539
|
-
end
|
160
|
+
# Class or Module (no instances)
|
161
|
+
deregister :callback, :on_complete, BookingConfirmationCallback
|
540
162
|
end
|
541
163
|
```
|
542
164
|
|
543
165
|
---
|
544
166
|
|
545
|
-
- **Prev:** [
|
167
|
+
- **Prev:** [Attributes - Defaults](attributes/defaults.md)
|
546
168
|
- **Next:** [Middlewares](middlewares.md)
|