cmdx 1.9.1 → 1.10.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.
data/LLM.md DELETED
@@ -1,3674 +0,0 @@
1
- # CMDx Documentation
2
-
3
- This file contains all the CMDx documentation consolidated from the docs directory.
4
-
5
- ---
6
-
7
- url: https://github.com/drexed/cmdx/blob/main/docs/getting_started.md
8
- ---
9
-
10
- # Getting Started
11
-
12
- CMDx is a Ruby framework for building maintainable, observable business logic through composable command objects. Design robust workflows with automatic attribute validation, structured error handling, comprehensive logging, and intelligent execution flow control.
13
-
14
- **Common Challenges:**
15
-
16
- - Inconsistent patterns across implementations
17
- - Minimal or no logging, making debugging painful
18
- - Fragile designs that erode developer confidence
19
-
20
- **CMDx Solutions:**
21
-
22
- - Establishes a consistent, standardized design
23
- - Provides flow control and error handling
24
- - Supports composable, reusable workflows
25
- - Includes detailed logging for observability
26
- - Defines input attributes with fallback defaults
27
- - Adds validations and type coercions
28
- - Plus many other developer-friendly tools
29
-
30
- ## Compose, Execute, React, Observe pattern
31
-
32
- CMDx encourages breaking business logic into composable tasks. Each task can be combined into larger workflows, executed with standardized flow control, and fully observed through logging, validations, and context.
33
-
34
- - **Compose** → Define small, contract-driven tasks with typed attributes, validations, and natural workflow composition.
35
- - **Execute** → Run tasks with clear outcomes, intentional halts, and pluggable behaviors via middlewares and callbacks.
36
- - **React** → Adapt to outcomes by chaining follow-up tasks, handling faults, or shaping future flows.
37
- - **Observe** → Capture immutable results, structured logs, and full execution chains for reliable tracing and insight.
38
-
39
- ## Installation
40
-
41
- Add CMDx to your Gemfile:
42
-
43
- ```ruby
44
- gem 'cmdx'
45
- ```
46
-
47
- For Rails applications, generate the configuration:
48
-
49
- ```bash
50
- rails generate cmdx:install
51
- ```
52
-
53
- This creates `config/initializers/cmdx.rb` file.
54
-
55
- ## Configuration Hierarchy
56
-
57
- CMDx follows a two-tier configuration hierarchy:
58
-
59
- 1. **Global Configuration**: Framework-wide defaults
60
- 2. **Task Settings**: Class-level overrides via `settings`
61
-
62
- > [!IMPORTANT]
63
- > Task-level settings take precedence over global configuration. Settings are inherited from superclasses and can be overridden in subclasses.
64
-
65
- ## Global Configuration
66
-
67
- Global configuration settings apply to all tasks inherited from `CMDx::Task`.
68
- Globally these settings are initialized with sensible defaults.
69
-
70
- ### Breakpoints
71
-
72
- Raise `CMDx::Fault` when a task called with `execute!` returns a matching status.
73
-
74
- ```ruby
75
- CMDx.configure do |config|
76
- # String or Array[String]
77
- config.task_breakpoints = "failed"
78
- end
79
- ```
80
-
81
- Workflow breakpoints stops execution and of workflow pipeline on the first task that returns a matching status and throws its `CMDx::Fault`.
82
-
83
- ```ruby
84
- CMDx.configure do |config|
85
- # String or Array[String]
86
- config.workflow_breakpoints = ["skipped", "failed"]
87
- end
88
- ```
89
-
90
- ### Backtraces
91
-
92
- Enable backtraces to be logged on any non-fault exceptions for improved debugging context. Run them through a cleaner to remove unwanted stack trace noise.
93
-
94
- > [!NOTE]
95
- > The `backtrace_cleaner` is set to `Rails.backtrace_cleaner.clean` in a Rails env by default.
96
-
97
- ```ruby
98
- CMDx.configure do |config|
99
- # Truthy
100
- config.backtrace = true
101
-
102
- # Via callable (must respond to `call(backtrace)`)
103
- config.backtrace_cleaner = AdvanceCleaner.new
104
-
105
- # Via proc or lambda
106
- config.backtrace_cleaner = ->(backtrace) { backtrace[0..5] }
107
- end
108
- ```
109
-
110
- ### Exception Handlers
111
-
112
- Use exception handlers are called on non-fault standard error based exceptions.
113
-
114
- > [!TIP]
115
- > Use exception handlers to send errors to your APM of choice.
116
-
117
- ```ruby
118
- CMDx.configure do |config|
119
- # Via callable (must respond to `call(task, exception)`)
120
- config.exception_handler = NewRelicReporter
121
-
122
- # Via proc or lambda
123
- config.exception_handler = proc do |task, exception|
124
- APMService.report(exception, extra_data: { task: task.name, id: task.id })
125
- end
126
- end
127
- ```
128
-
129
- ### Logging
130
-
131
- ```ruby
132
- CMDx.configure do |config|
133
- config.logger = CustomLogger.new($stdout)
134
- end
135
- ```
136
-
137
- ### Middlewares
138
-
139
- See the [Middelwares](#https://github.com/drexed/cmdx/blob/main/docs/middlewares.md#declarations) docs for task level configurations.
140
-
141
- ```ruby
142
- CMDx.configure do |config|
143
- # Via callable (must respond to `call(task, options)`)
144
- config.middlewares.register CMDx::Middlewares::Timeout
145
-
146
- # Via proc or lambda
147
- config.middlewares.register proc { |task, options|
148
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
149
- result = yield
150
- end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
151
- Rails.logger.debug { "task completed in #{((end_time - start_time) * 1000).round(2)}ms" }
152
- result
153
- }
154
-
155
- # With options
156
- config.middlewares.register AuditTrailMiddleware, service_name: "document_processor"
157
-
158
- # Remove middleware
159
- config.middlewares.deregister CMDx::Middlewares::Timeout
160
- end
161
- ```
162
-
163
- > [!NOTE]
164
- > Middlewares are executed in registration order. Each middleware wraps the next, creating an execution chain around task logic.
165
-
166
- ### Callbacks
167
-
168
- See the [Callbacks](#https://github.com/drexed/cmdx/blob/main/docs/callbacks.md#declarations) docs for task level configurations.
169
-
170
- ```ruby
171
- CMDx.configure do |config|
172
- # Via method
173
- config.callbacks.register :before_execution, :initialize_user_session
174
-
175
- # Via callable (must respond to `call(task)`)
176
- config.callbacks.register :on_success, LogUserActivity
177
-
178
- # Via proc or lambda
179
- config.callbacks.register :on_complete, proc { |task|
180
- execution_time = task.metadata[:runtime]
181
- Metrics.timer("task.execution_time", execution_time, tags: ["task:#{task.class.name.underscore}"])
182
- }
183
-
184
- # With options
185
- config.callbacks.register :on_failure, :send_alert_notification, if: :critical_task?
186
-
187
- # Remove callback
188
- config.callbacks.deregister :on_success, LogUserActivity
189
- end
190
- ```
191
-
192
- ### Coercions
193
-
194
- See the [Attributes - Coercions](#https://github.com/drexed/cmdx/blob/main/docs/attributes/coercions.md#declarations) docs for task level configurations.
195
-
196
- ```ruby
197
- CMDx.configure do |config|
198
- # Via callable (must respond to `call(value, options)`)
199
- config.coercions.register :currency, CurrencyCoercion
200
-
201
- # Via method (must match signature `def coordinates_coercion(value, options)`)
202
- config.coercions.register :coordinates, :coordinates_coercion
203
-
204
- # Via proc or lambda
205
- config.coercions.register :tag_list, proc { |value, options|
206
- delimiter = options[:delimiter] || ','
207
- max_tags = options[:max_tags] || 50
208
-
209
- tags = value.to_s.split(delimiter).map(&:strip).reject(&:empty?)
210
- tags.first(max_tags)
211
- }
212
-
213
- # Remove coercion
214
- config.coercions.deregister :currency
215
- end
216
- ```
217
-
218
- ### Validators
219
-
220
- See the [Attributes - Validations](#https://github.com/drexed/cmdx/blob/main/docs/attributes/validations.md#declarations) docs for task level configurations.
221
-
222
- ```ruby
223
- CMDx.configure do |config|
224
- # Via callable (must respond to `call(value, options)`)
225
- config.validators.register :username, UsernameValidator
226
-
227
- # Via method (must match signature `def url_validator(value, options)`)
228
- config.validators.register :url, :url_validator
229
-
230
- # Via proc or lambda
231
- config.validators.register :access_token, proc { |value, options|
232
- expected_prefix = options[:prefix] || "tok_"
233
- minimum_length = options[:min_length] || 40
234
-
235
- value.start_with?(expected_prefix) && value.length >= minimum_length
236
- }
237
-
238
- # Remove validator
239
- config.validators.deregister :username
240
- end
241
- ```
242
-
243
- ## Task Configuration
244
-
245
- ### Settings
246
-
247
- Override global configuration for specific tasks using `settings`:
248
-
249
- ```ruby
250
- class GenerateInvoice < CMDx::Task
251
- settings(
252
- # Global configuration overrides
253
- task_breakpoints: ["failed"], # Breakpoint override
254
- workflow_breakpoints: [], # Breakpoint override
255
- backtrace: true, # Toggle backtrace
256
- backtrace_cleaner: ->(bt) { bt[0..5] }, # Backtrace cleaner
257
- logger: CustomLogger.new($stdout), # Custom logger
258
-
259
- # Task configuration settings
260
- breakpoints: ["failed"], # Contextual pointer for :task_breakpoints and :workflow_breakpoints
261
- log_level: :info, # Log level override
262
- log_formatter: CMDx::LogFormatters::Json.new # Log formatter override
263
- tags: ["billing", "financial"], # Logging tags
264
- deprecated: true, # Task deprecations
265
- retries: 3, # Non-fault exception retries
266
- retry_on: [External::ApiError], # List of exceptions to retry on
267
- retry_jitter: 1 # Space between retry iteration, eg: current retry num + 1
268
- )
269
-
270
- def work
271
- # Your logic here...
272
- end
273
- end
274
- ```
275
-
276
- > [!IMPORTANT]
277
- > Retries reuse the same context when executing its work. By default all `StandardErrors` will be retried if no `retry_on` option is passed.
278
-
279
- ### Registrations
280
-
281
- Register middlewares, callbacks, coercions, and validators on a specific task.
282
- Deregister options that should not be available.
283
-
284
- ```ruby
285
- class SendCampaignEmail < CMDx::Task
286
- # Middlewares
287
- register :middleware, CMDx::Middlewares::Timeout
288
- deregister :middleware, AuditTrailMiddleware
289
-
290
- # Callbacks
291
- register :callback, :on_complete, proc { |task|
292
- runtime = task.metadata[:runtime]
293
- Analytics.track("email_campaign.sent", runtime, tags: ["task:#{task.class.name}"])
294
- }
295
- deregister :callback, :before_execution, :initialize_user_session
296
-
297
- # Coercions
298
- register :coercion, :currency, CurrencyCoercion
299
- deregister :coercion, :coordinates
300
-
301
- # Validators
302
- register :validator, :username, :username_validator
303
- deregister :validator, :url
304
-
305
- def work
306
- # Your logic here...
307
- end
308
- end
309
- ```
310
-
311
- ## Configuration Management
312
-
313
- ### Access
314
-
315
- ```ruby
316
- # Global configuration access
317
- CMDx.configuration.logger #=> <Logger instance>
318
- CMDx.configuration.task_breakpoints #=> ["failed"]
319
- CMDx.configuration.middlewares.registry #=> [<Middleware>, ...]
320
-
321
- # Task configuration access
322
- class ProcessUpload < CMDx::Task
323
- settings(tags: ["files", "storage"])
324
-
325
- def work
326
- self.class.settings[:logger] #=> Global configuration value
327
- self.class.settings[:tags] #=> Task configuration value => ["files", "storage"]
328
- end
329
- end
330
- ```
331
-
332
- ### Resetting
333
-
334
- > [!WARNING]
335
- > Resetting configuration affects the entire application. Use primarily in test environments or during application initialization.
336
-
337
- ```ruby
338
- # Reset to framework defaults
339
- CMDx.reset_configuration!
340
-
341
- # Verify reset
342
- CMDx.configuration.task_breakpoints #=> ["failed"] (default)
343
- CMDx.configuration.middlewares.registry #=> Empty registry
344
-
345
- # Commonly used in test setup (RSpec example)
346
- RSpec.configure do |config|
347
- config.before(:each) do
348
- CMDx.reset_configuration!
349
- end
350
- end
351
- ```
352
-
353
- ## Task Generator
354
-
355
- Generate new CMDx tasks quickly using the built-in generator:
356
-
357
- ```bash
358
- rails generate cmdx:task ModerateBlogPost
359
- ```
360
-
361
- This creates a new task file with the basic structure:
362
-
363
- ```ruby
364
- # app/tasks/moderate_blog_post.rb
365
- class ModerateBlogPost < CMDx::Task
366
- def work
367
- # Your logic here...
368
- end
369
- end
370
- ```
371
-
372
- > [!TIP]
373
- > Use **present tense verbs + noun** for task names, eg: `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument`
374
-
375
- ## Type safety
376
-
377
- CMDx includes built-in RBS (Ruby Type Signature) inline annotations throughout the codebase, providing type information for static analysis and editor support.
378
-
379
- - **Type checking** — Catch type errors before runtime using tools like Steep or TypeProf
380
- - **Better IDE support** — Enhanced autocomplete, navigation, and inline documentation
381
- - **Self-documenting code** — Clear method signatures and return types
382
- - **Refactoring confidence** — Type-aware refactoring reduces bugs
383
-
384
- ---
385
-
386
- url: https://github.com/drexed/cmdx/blob/main/docs/basics/setup.md
387
- ---
388
-
389
- # Basics - Setup
390
-
391
- Tasks are the core building blocks of CMDx, encapsulating business logic within structured, reusable objects. Each task represents a unit of work with automatic attribute validation, error handling, and execution tracking.
392
-
393
- ## Structure
394
-
395
- Tasks inherit from `CMDx::Task` and require only a `work` method:
396
-
397
- ```ruby
398
- class ValidateDocument < CMDx::Task
399
- def work
400
- # Your logic here...
401
- end
402
- end
403
- ```
404
-
405
- An exception will be raised if a work method is not defined.
406
-
407
- ```ruby
408
- class IncompleteTask < CMDx::Task
409
- # No `work` method defined
410
- end
411
-
412
- IncompleteTask.execute #=> raises CMDx::UndefinedMethodError
413
- ```
414
-
415
- ## Inheritance
416
-
417
- All configuration options are inheritable by any child classes.
418
- Create a base class to share common configuration across tasks:
419
-
420
- ```ruby
421
- class ApplicationTask < CMDx::Task
422
- register :middleware, SecurityMiddleware
423
-
424
- before_execution :initialize_request_tracking
425
-
426
- attribute :session_id
427
-
428
- private
429
-
430
- def initialize_request_tracking
431
- context.tracking_id ||= SecureRandom.uuid
432
- end
433
- end
434
-
435
- class SyncInventory < ApplicationTask
436
- def work
437
- # Your logic here...
438
- end
439
- end
440
- ```
441
-
442
- ## Lifecycle
443
-
444
- Tasks follow a predictable call pattern with specific states and statuses:
445
-
446
- > [!CAUTION]
447
- > Tasks are single-use objects. Once executed, they are frozen and cannot be executed again.
448
-
449
- | Stage | State | Status | Description |
450
- |-------|-------|--------|-------------|
451
- | **Instantiation** | `initialized` | `success` | Task created with context |
452
- | **Validation** | `executing` | `success`/`failed` | Attributes validated |
453
- | **Execution** | `executing` | `success`/`failed`/`skipped` | `work` method runs |
454
- | **Completion** | `executed` | `success`/`failed`/`skipped` | Result finalized |
455
- | **Freezing** | `executed` | `success`/`failed`/`skipped` | Task becomes immutable |
456
-
457
- ---
458
-
459
- url: https://github.com/drexed/cmdx/blob/main/docs/basics/execution.md
460
- ---
461
-
462
- # Basics - Execution
463
-
464
- Task execution in CMDx provides two distinct methods that handle success and halt scenarios differently. Understanding when to use each method is crucial for proper error handling and control flow in your application workflows.
465
-
466
- ## Methods Overview
467
-
468
- Tasks are single-use objects. Once executed, they are frozen and cannot be executed again.
469
- Create a new instance for subsequent executions.
470
-
471
- | Method | Returns | Exceptions | Use Case |
472
- |--------|---------|------------|----------|
473
- | `execute` | Always returns `CMDx::Result` | Never raises | Predictable result handling |
474
- | `execute!` | Returns `CMDx::Result` on success | Raises `CMDx::Fault` when skipped or failed | Exception-based control flow |
475
-
476
- ## Non-bang Execution
477
-
478
- The `execute` method always returns a `CMDx::Result` object regardless of execution outcome.
479
- This is the preferred method for most use cases.
480
-
481
- Any unhandled exceptions will be caught and returned as a task failure.
482
-
483
- ```ruby
484
- result = CreateAccount.execute(email: "user@example.com")
485
-
486
- # Check execution state
487
- result.success? #=> true/false
488
- result.failed? #=> true/false
489
- result.skipped? #=> true/false
490
-
491
- # Access result data
492
- result.context.email #=> "user@example.com"
493
- result.state #=> "complete"
494
- result.status #=> "success"
495
- ```
496
-
497
- ## Bang Execution
498
-
499
- The bang `execute!` method raises a `CMDx::Fault` based exception when tasks fail or are skipped, and returns a `CMDx::Result` object only on success.
500
-
501
- It raises any unhandled non-fault exceptions caused during execution.
502
-
503
- | Exception | Raised When |
504
- |-----------|-------------|
505
- | `CMDx::FailFault` | Task execution fails |
506
- | `CMDx::SkipFault` | Task execution is skipped |
507
-
508
- > [!IMPORTANT]
509
- > `execute!` behavior depends on the `task_breakpoints` or `workflow_breakpoints` configuration. By default, it raises exceptions only on failures.
510
-
511
- ```ruby
512
- begin
513
- result = CreateAccount.execute!(email: "user@example.com")
514
- SendWelcomeEmail.execute(result.context)
515
- rescue CMDx::Fault => e
516
- ScheduleAccountRetryJob.perform_later(e.result.context.email)
517
- rescue CMDx::SkipFault => e
518
- Rails.logger.info("Account creation skipped: #{e.result.reason}")
519
- rescue Exception => e
520
- ErrorTracker.capture(unhandled_exception: e)
521
- end
522
- ```
523
-
524
- ## Direct Instantiation
525
-
526
- Tasks can be instantiated directly for advanced use cases, testing, and custom execution patterns:
527
-
528
- ```ruby
529
- # Direct instantiation
530
- task = CreateAccount.new(email: "user@example.com", send_welcome: true)
531
-
532
- # Access properties before execution
533
- task.id #=> "abc123..." (unique task ID)
534
- task.context.email #=> "user@example.com"
535
- task.context.send_welcome #=> true
536
- task.result.state #=> "initialized"
537
- result.status #=> "success"
538
-
539
- # Manual execution
540
- task.execute
541
- # or
542
- task.execute!
543
-
544
- task.result.success? #=> true/false
545
- ```
546
-
547
- ## Result Details
548
-
549
- The `Result` object provides comprehensive execution information:
550
-
551
- ```ruby
552
- result = CreateAccount.execute(email: "user@example.com")
553
-
554
- # Execution metadata
555
- result.id #=> "abc123..." (unique execution ID)
556
- result.task #=> CreateAccount instance (frozen)
557
- result.chain #=> Task execution chain
558
-
559
- # Context and metadata
560
- result.context #=> Context with all task data
561
- result.metadata #=> Hash with execution metadata
562
-
563
- ---
564
-
565
- url: https://github.com/drexed/cmdx/blob/main/docs/basics/context.md
566
- ---
567
-
568
- # Basics - Context
569
-
570
- Task context provides flexible data storage, access, and sharing within task execution. It serves as the primary data container for all task inputs, intermediate results, and outputs.
571
-
572
- ## Assigning Data
573
-
574
- Context is automatically populated with all inputs passed to a task. All keys are normalized to symbols for consistent access:
575
-
576
- ```ruby
577
- # Direct execution
578
- CalculateShipping.execute(weight: 2.5, destination: "CA")
579
-
580
- # Instance creation
581
- CalculateShipping.new(weight: 2.5, "destination" => "CA")
582
- ```
583
-
584
- > [!IMPORTANT]
585
- > String keys are automatically converted to symbols. Use symbols for consistency in your code.
586
-
587
- ## Accessing Data
588
-
589
- Context provides multiple access patterns with automatic nil safety:
590
-
591
- ```ruby
592
- class CalculateShipping < CMDx::Task
593
- def work
594
- # Method style access (preferred)
595
- weight = context.weight
596
- destination = context.destination
597
-
598
- # Hash style access
599
- service_type = context[:service_type]
600
- options = context["options"]
601
-
602
- # Safe access with defaults
603
- rush_delivery = context.fetch!(:rush_delivery, false)
604
- carrier = context.dig(:options, :carrier)
605
-
606
- # Shorter alias
607
- cost = ctx.weight * ctx.rate_per_pound # ctx aliases context
608
- end
609
- end
610
- ```
611
-
612
- > [!IMPORTANT]
613
- > Accessing undefined context attributes returns `nil` instead of raising errors, enabling graceful handling of optional attributes.
614
-
615
- ## Modifying Context
616
-
617
- Context supports dynamic modification during task execution:
618
-
619
- ```ruby
620
- class CalculateShipping < CMDx::Task
621
- def work
622
- # Direct assignment
623
- context.carrier = Carrier.find_by(code: context.carrier_code)
624
- context.package = Package.new(weight: context.weight)
625
- context.calculated_at = Time.now
626
-
627
- # Hash-style assignment
628
- context[:status] = "calculating"
629
- context["tracking_number"] = "SHIP#{SecureRandom.hex(6)}"
630
-
631
- # Conditional assignment
632
- context.insurance_included ||= false
633
-
634
- # Batch updates
635
- context.merge!(
636
- status: "completed",
637
- shipping_cost: calculate_cost,
638
- estimated_delivery: Time.now + 3.days
639
- )
640
-
641
- # Remove sensitive data
642
- context.delete!(:credit_card_token)
643
- end
644
-
645
- private
646
-
647
- def calculate_cost
648
- base_rate = context.weight * context.rate_per_pound
649
- base_rate + (base_rate * context.tax_percentage)
650
- end
651
- end
652
- ```
653
-
654
- > [!TIP]
655
- > Use context for both input values and intermediate results. This creates natural data flow through your task execution pipeline.
656
-
657
- ## Data Sharing
658
-
659
- Context enables seamless data flow between related tasks in complex workflows:
660
-
661
- ```ruby
662
- # During execution
663
- class CalculateShipping < CMDx::Task
664
- def work
665
- # Validate shipping data
666
- validation_result = ValidateAddress.execute(context)
667
-
668
- # Via context
669
- CalculateInsurance.execute(context)
670
-
671
- # Via result
672
- NotifyShippingCalculated.execute(validation_result)
673
-
674
- # Context now contains accumulated data from all tasks
675
- context.address_validated #=> true (from validation)
676
- context.insurance_calculated #=> true (from insurance)
677
- context.notification_sent #=> true (from notification)
678
- end
679
- end
680
-
681
- # After execution
682
- result = CalculateShipping.execute(destination: "New York, NY")
683
-
684
- CreateShippingLabel.execute(result)
685
- ```
686
-
687
- ---
688
-
689
- url: https://github.com/drexed/cmdx/blob/main/docs/basics/chain.md
690
- ---
691
-
692
- # Basics - Chain
693
-
694
- Chains automatically group related task executions within a thread, providing unified tracking, correlation, and execution context management. Each thread maintains its own chain through thread-local storage, eliminating the need for manual coordination.
695
-
696
- ## Management
697
-
698
- Each thread maintains its own chain context through thread-local storage, providing automatic isolation without manual coordination.
699
-
700
- > [!WARNING]
701
- > Chain operations are thread-local. Never share chain references across threads as this can lead to race conditions and data corruption.
702
-
703
- ```ruby
704
- # Thread A
705
- Thread.new do
706
- result = ImportDataset.execute(file_path: "/data/batch1.csv")
707
- result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
708
- end
709
-
710
- # Thread B (completely separate chain)
711
- Thread.new do
712
- result = ImportDataset.execute(file_path: "/data/batch2.csv")
713
- result.chain.id #=> "z3a42b95-c821-7892-b156-dd7c921fe2a3"
714
- end
715
-
716
- # Access current thread's chain
717
- CMDx::Chain.current #=> Returns current chain or nil
718
- CMDx::Chain.clear #=> Clears current thread's chain
719
- ```
720
-
721
- ## Links
722
-
723
- Every task execution automatically creates or joins the current thread's chain:
724
-
725
- > [!IMPORTANT]
726
- > Chain creation is automatic and transparent. You don't need to manually manage chain lifecycle.
727
-
728
- ```ruby
729
- class ImportDataset < CMDx::Task
730
- def work
731
- # First task creates new chain
732
- result1 = ValidateHeaders.execute(file_path: context.file_path)
733
- result1.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
734
- result1.chain.results.size #=> 1
735
-
736
- # Second task joins existing chain
737
- result2 = SendNotification.execute(to: "admin@company.com")
738
- result2.chain.id == result1.chain.id #=> true
739
- result2.chain.results.size #=> 2
740
-
741
- # Both results reference the same chain
742
- result1.chain.results == result2.chain.results #=> true
743
- end
744
- end
745
- ```
746
-
747
- ## Inheritance
748
-
749
- When tasks call subtasks within the same thread, all executions automatically inherit the current chain, creating a unified execution trail.
750
-
751
- ```ruby
752
- class ImportDataset < CMDx::Task
753
- def work
754
- context.dataset = Dataset.find(context.dataset_id)
755
-
756
- # Subtasks automatically inherit current chain
757
- ValidateSchema.execute
758
- TransformData.execute!(context)
759
- SaveToDatabase.execute(dataset_id: context.dataset_id)
760
- end
761
- end
762
-
763
- result = ImportDataset.execute(dataset_id: 456)
764
- chain = result.chain
765
-
766
- # All tasks share the same chain
767
- chain.results.size #=> 4 (main task + 3 subtasks)
768
- chain.results.map { |r| r.task.class }
769
- #=> [ImportDataset, ValidateSchema, TransformData, SaveToDatabase]
770
- ```
771
-
772
- ## Structure
773
-
774
- Chains provide comprehensive execution information with state delegation:
775
-
776
- > [!IMPORTANT]
777
- > Chain state always reflects the first (outer-most) task result, not individual subtask outcomes. Subtasks maintain their own success/failure states.
778
-
779
- ```ruby
780
- result = ImportDataset.execute(dataset_id: 456)
781
- chain = result.chain
782
-
783
- # Chain identification
784
- chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5"
785
- chain.results #=> Array of all results in execution order
786
-
787
- # State delegation (from first/outer-most result)
788
- chain.state #=> "complete"
789
- chain.status #=> "success"
790
- chain.outcome #=> "success"
791
-
792
- # Access individual results
793
- chain.results.each_with_index do |result, index|
794
- puts "#{index}: #{result.task.class} - #{result.status}"
795
- end
796
- ```
797
-
798
- ---
799
-
800
- url: https://github.com/drexed/cmdx/blob/main/docs/interruptions/halt.md
801
- ---
802
-
803
- # Interruptions - Halt
804
-
805
- Halting stops task execution with explicit intent signaling. Tasks provide two primary halt methods that control execution flow and result in different outcomes.
806
-
807
- ## Skipping
808
-
809
- `skip!` communicates that the task is to be intentionally bypassed. This represents a controlled, intentional interruption where the task determines that execution is not necessary or appropriate.
810
-
811
- > [!IMPORTANT]
812
- > Skipping is a no-op, not a failure or error and are considered successful outcomes.
813
-
814
- ```ruby
815
- class ProcessInventory < CMDx::Task
816
- def work
817
- # Without a reason
818
- skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
819
-
820
- # With a reason
821
- skip!("Warehouse closed") unless Time.now.hour.between?(8, 18)
822
-
823
- inventory = Inventory.find(context.inventory_id)
824
-
825
- if inventory.already_counted?
826
- skip!("Inventory already counted today")
827
- else
828
- inventory.count!
829
- end
830
- end
831
- end
832
-
833
- result = ProcessInventory.execute(inventory_id: 456)
834
-
835
- # Executed
836
- result.status #=> "skipped"
837
-
838
- # Without a reason
839
- result.reason #=> "Unspecified"
840
-
841
- # With a reason
842
- result.reason #=> "Warehouse closed"
843
- ```
844
-
845
- ## Failing
846
-
847
- `fail!` communicates that the task encountered an impediment that prevents successful completion. This represents controlled failure where the task explicitly determines that execution cannot continue.
848
-
849
- ```ruby
850
- class ProcessRefund < CMDx::Task
851
- def work
852
- # Without a reason
853
- fail! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name)
854
-
855
- refund = Refund.find(context.refund_id)
856
-
857
- # With a reason
858
- if refund.expired?
859
- fail!("Refund period has expired")
860
- elsif !refund.amount.positive?
861
- fail!("Refund amount must be positive")
862
- else
863
- refund.process!
864
- end
865
- end
866
- end
867
-
868
- result = ProcessRefund.execute(refund_id: 789)
869
-
870
- # Executed
871
- result.status #=> "failed"
872
-
873
- # Without a reason
874
- result.reason #=> "Unspecified"
875
-
876
- # With a reason
877
- result.reason #=> "Refund period has expired"
878
- ```
879
-
880
- ## Metadata Enrichment
881
-
882
- Both halt methods accept metadata to provide additional context about the interruption. Metadata is stored as a hash and becomes available through the result object.
883
-
884
- ```ruby
885
- class ProcessRenewal < CMDx::Task
886
- def work
887
- license = License.find(context.license_id)
888
-
889
- if license.already_renewed?
890
- # Without metadata
891
- skip!("License already renewed")
892
- end
893
-
894
- unless license.renewal_eligible?
895
- # With metadata
896
- fail!(
897
- "License not eligible for renewal",
898
- error_code: "LICENSE.NOT_ELIGIBLE",
899
- retry_after: Time.current + 30.days
900
- )
901
- end
902
-
903
- process_renewal
904
- end
905
- end
906
-
907
- result = ProcessRenewal.execute(license_id: 567)
908
-
909
- # Without metadata
910
- result.metadata #=> {}
911
-
912
- # With metadata
913
- result.metadata #=> {
914
- # error_code: "LICENSE.NOT_ELIGIBLE",
915
- # retry_after: <Time 30 days from now>
916
- # }
917
- ```
918
-
919
- ## State Transitions
920
-
921
- Halt methods trigger specific state and status transitions:
922
-
923
- | Method | State | Status | Outcome |
924
- |--------|-------|--------|---------|
925
- | `skip!` | `interrupted` | `skipped` | `good? = true`, `bad? = true` |
926
- | `fail!` | `interrupted` | `failed` | `good? = false`, `bad? = true` |
927
-
928
- ```ruby
929
- result = ProcessRenewal.execute(license_id: 567)
930
-
931
- # State information
932
- result.state #=> "interrupted"
933
- result.status #=> "skipped" or "failed"
934
- result.interrupted? #=> true
935
- result.complete? #=> false
936
-
937
- # Outcome categorization
938
- result.good? #=> true for skipped, false for failed
939
- result.bad? #=> true for both skipped and failed
940
- ```
941
-
942
- ## Execution Behavior
943
-
944
- Halt methods behave differently depending on the call method used:
945
-
946
- ### Non-bang execution
947
-
948
- Returns result object without raising exceptions:
949
-
950
- ```ruby
951
- result = ProcessRefund.execute(refund_id: 789)
952
-
953
- case result.status
954
- when "success"
955
- puts "Refund processed: $#{result.context.refund.amount}"
956
- when "skipped"
957
- puts "Refund skipped: #{result.reason}"
958
- when "failed"
959
- puts "Refund failed: #{result.reason}"
960
- handle_refund_error(result.metadata[:error_code])
961
- end
962
- ```
963
-
964
- ### Bang execution
965
-
966
- Raises exceptions for halt conditions based on `task_breakpoints` configuration:
967
-
968
- ```ruby
969
- begin
970
- result = ProcessRefund.execute!(refund_id: 789)
971
- puts "Success: Refund processed"
972
- rescue CMDx::SkipFault => e
973
- puts "Skipped: #{e.message}"
974
- rescue CMDx::FailFault => e
975
- puts "Failed: #{e.message}"
976
- handle_refund_failure(e.result.metadata[:error_code])
977
- end
978
- ```
979
-
980
- ## Best Practices
981
-
982
- Always try to provide a `reason` when using halt methods. This provides clear context for debugging and creates meaningful exception messages.
983
-
984
- ```ruby
985
- # Good: Clear, specific reason
986
- skip!("Document processing paused for compliance review")
987
- fail!("File format not supported by processor", code: "FORMAT_UNSUPPORTED")
988
-
989
- # Acceptable: Generic, non-specific reason
990
- skip!("Paused")
991
- fail!("Unsupported")
992
-
993
- # Bad: Default, cannot determine reason
994
- skip! #=> "Unspecified"
995
- fail! #=> "Unspecified"
996
- ```
997
-
998
- ## Manual Errors
999
-
1000
- There are rare cases where you need to manually assign errors.
1001
-
1002
- > [!IMPORTANT]
1003
- > Keep in mind you will still need to initiate a fault if a stoppage of work is required.
1004
-
1005
- ```ruby
1006
- class ProcessRenewal < CMDx::Task
1007
- def work
1008
- if document.nonrenewable?
1009
- errors.add(:document, "not renewable")
1010
- fail!("document could not be renewed")
1011
- else
1012
- document.renew!
1013
- end
1014
- end
1015
- end
1016
- ```
1017
-
1018
- ---
1019
-
1020
- url: https://github.com/drexed/cmdx/blob/main/docs/interruptions/faults.md
1021
- ---
1022
-
1023
- # Interruptions - Faults
1024
-
1025
- Faults are exception mechanisms that halt task execution via `skip!` and `fail!` methods. When tasks execute with the `execute!` method, fault exceptions matching the task's interruption status are raised, enabling sophisticated exception handling and control flow patterns.
1026
-
1027
- ## Fault Types
1028
-
1029
- | Type | Triggered By | Use Case |
1030
- |------|--------------|----------|
1031
- | `CMDx::Fault` | Base class | Catch-all for any interruption |
1032
- | `CMDx::SkipFault` | `skip!` method | Optional processing, early returns |
1033
- | `CMDx::FailFault` | `fail!` method | Validation errors, processing failures |
1034
-
1035
- > [!IMPORTANT]
1036
- > All fault exceptions inherit from `CMDx::Fault` and provide access to the complete task execution context including result, task, context, and chain information.
1037
-
1038
- ## Fault Handling
1039
-
1040
- ```ruby
1041
- begin
1042
- ProcessTicket.execute!(ticket_id: 456)
1043
- rescue CMDx::SkipFault => e
1044
- logger.info "Ticket processing skipped: #{e.message}"
1045
- schedule_retry(e.context.ticket_id)
1046
- rescue CMDx::FailFault => e
1047
- logger.error "Ticket processing failed: #{e.message}"
1048
- notify_admin(e.context.assigned_agent, e.result.metadata[:error_code])
1049
- rescue CMDx::Fault => e
1050
- logger.warn "Ticket processing interrupted: #{e.message}"
1051
- rollback_changes
1052
- end
1053
- ```
1054
-
1055
- ## Data Access
1056
-
1057
- Faults provide comprehensive access to execution context, eg:
1058
-
1059
- ```ruby
1060
- begin
1061
- LicenseActivation.execute!(license_key: key, machine_id: machine)
1062
- rescue CMDx::Fault => e
1063
- # Result information
1064
- e.result.state #=> "interrupted"
1065
- e.result.status #=> "failed" or "skipped"
1066
- e.result.reason #=> "License key already activated"
1067
-
1068
- # Task information
1069
- e.task.class #=> <LicenseActivation>
1070
- e.task.id #=> "abc123..."
1071
-
1072
- # Context data
1073
- e.context.license_key #=> "ABC-123-DEF"
1074
- e.context.machine_id #=> "[FILTERED]"
1075
-
1076
- # Chain information
1077
- e.chain.id #=> "def456..."
1078
- e.chain.size #=> 3
1079
- end
1080
- ```
1081
-
1082
- ## Advanced Matching
1083
-
1084
- ### Task-Specific Matching
1085
-
1086
- Use `for?` to handle faults only from specific task classes, enabling targeted exception handling in complex workflows.
1087
-
1088
- ```ruby
1089
- begin
1090
- DocumentWorkflow.execute!(document_data: data)
1091
- rescue CMDx::FailFault.for?(FormatValidator, ContentProcessor) => e
1092
- # Handle only document-related failures
1093
- retry_with_alternate_parser(e.context)
1094
- rescue CMDx::SkipFault.for?(VirusScanner, ContentFilter) => e
1095
- # Handle security-related skips
1096
- quarantine_for_review(e.context.document_id)
1097
- end
1098
- ```
1099
-
1100
- ### Custom Logic Matching
1101
-
1102
- ```ruby
1103
- begin
1104
- ReportGenerator.execute!(report: report_data)
1105
- rescue CMDx::Fault.matches? { |f| f.context.data_size > 10_000 } => e
1106
- escalate_large_dataset_failure(e)
1107
- rescue CMDx::FailFault.matches? { |f| f.result.metadata[:attempt_count] > 3 } => e
1108
- abandon_report_generation(e)
1109
- rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "memory" } => e
1110
- increase_memory_and_retry(e)
1111
- end
1112
- ```
1113
-
1114
- ## Fault Propagation
1115
-
1116
- Use `throw!` to propagate failures while preserving fault context and maintaining the error chain for debugging.
1117
-
1118
- ### Basic Propagation
1119
-
1120
- ```ruby
1121
- class ReportGenerator < CMDx::Task
1122
- def work
1123
- # Throw if skipped or failed
1124
- validation_result = DataValidator.execute(context)
1125
- throw!(validation_result)
1126
-
1127
- # Only throw if skipped
1128
- check_permissions = CheckPermissions.execute(context)
1129
- throw!(check_permissions) if check_permissions.skipped?
1130
-
1131
- # Only throw if failed
1132
- data_result = DataProcessor.execute(context)
1133
- throw!(data_result) if data_result.failed?
1134
-
1135
- # Continue processing
1136
- generate_report
1137
- end
1138
- end
1139
- ```
1140
-
1141
- ### Additional Metadata
1142
-
1143
- ```ruby
1144
- class BatchProcessor < CMDx::Task
1145
- def work
1146
- step_result = FileValidation.execute(context)
1147
-
1148
- if step_result.failed?
1149
- throw!(step_result, {
1150
- batch_stage: "validation",
1151
- can_retry: true,
1152
- next_step: "file_repair"
1153
- })
1154
- end
1155
-
1156
- continue_batch
1157
- end
1158
- end
1159
- ```
1160
-
1161
- ## Chain Analysis
1162
-
1163
- Results provide methods to analyze fault propagation and identify original failure sources in complex execution chains.
1164
-
1165
- ```ruby
1166
- result = DocumentWorkflow.execute(invalid_data)
1167
-
1168
- if result.failed?
1169
- # Trace the original failure
1170
- original = result.caused_failure
1171
- if original
1172
- puts "Original failure: #{original.task.class.name}"
1173
- puts "Reason: #{original.reason}"
1174
- end
1175
-
1176
- # Find what propagated the failure
1177
- thrower = result.threw_failure
1178
- puts "Propagated by: #{thrower.task.class.name}" if thrower
1179
-
1180
- # Analyze failure type
1181
- case
1182
- when result.caused_failure?
1183
- puts "This task was the original source"
1184
- when result.threw_failure?
1185
- puts "This task propagated a failure"
1186
- when result.thrown_failure?
1187
- puts "This task failed due to propagation"
1188
- end
1189
- end
1190
- ```
1191
-
1192
- ---
1193
-
1194
- url: https://github.com/drexed/cmdx/blob/main/docs/interruptions/exceptions.md
1195
- ---
1196
-
1197
- # Interruptions - Exceptions
1198
-
1199
- CMDx provides robust exception handling that differs between the `execute` and `execute!` methods. Understanding how unhandled exceptions are processed is crucial for building reliable task execution flows and implementing proper error handling strategies.
1200
-
1201
- ## Exception Handling
1202
-
1203
- > [!IMPORTANT]
1204
- > When designing tasks try not to `raise` your own exceptions directly, instead use `skip!` or `fail!` to signal intent clearly.
1205
-
1206
- ### Non-bang execution
1207
-
1208
- The `execute` method captures **all** unhandled exceptions and converts them to failed results, ensuring predictable behavior and consistent result processing.
1209
-
1210
- ```ruby
1211
- class CompressDocument < CMDx::Task
1212
- def work
1213
- document = Document.find(context.document_id)
1214
- document.compress!
1215
- end
1216
- end
1217
-
1218
- result = CompressDocument.execute(document_id: "unknown-doc-id")
1219
- result.state #=> "interrupted"
1220
- result.status #=> "failed"
1221
- result.failed? #=> true
1222
- result.reason #=> "[ActiveRecord::NotFoundError] record not found"
1223
- result.cause #=> <ActiveRecord::NotFoundError>
1224
- ```
1225
-
1226
- > [!NOTE]
1227
- > The `exception_handler` setting only works with non-bang execution as it catches all exceptions preventing them from reaching your apps global error handler.
1228
-
1229
- ### Bang execution
1230
-
1231
- The `execute!` method allows unhandled exceptions to propagate, enabling standard Ruby exception handling while respecting CMDx fault configuration.
1232
-
1233
- ```ruby
1234
- class CompressDocument < CMDx::Task
1235
- def work
1236
- document = Document.find(context.document_id)
1237
- document.compress!
1238
- end
1239
- end
1240
-
1241
- begin
1242
- CompressDocument.execute!(document_id: "unknown-doc-id")
1243
- rescue ActiveRecord::NotFoundError => e
1244
- puts "Handle exception: #{e.message}"
1245
- end
1246
- ```
1247
-
1248
- ---
1249
-
1250
- url: https://github.com/drexed/cmdx/blob/main/docs/outcomes/result.md
1251
- ---
1252
-
1253
- # Outcomes - Result
1254
-
1255
- The result object is the comprehensive return value of task execution, providing complete information about the execution outcome, state, timing, and any data produced during the task lifecycle. Results serve as the primary interface for inspecting task execution outcomes and chaining task operations.
1256
-
1257
- ## Result Attributes
1258
-
1259
- Every result provides access to essential execution information:
1260
-
1261
- > [!IMPORTANT]
1262
- > Result objects are immutable after task execution completes and reflect the final state.
1263
-
1264
- ```ruby
1265
- result = BuildApplication.execute(version: "1.2.3")
1266
-
1267
- # Object data
1268
- result.task #=> <BuildApplication>
1269
- result.context #=> <CMDx::Context>
1270
- result.chain #=> <CMDx::Chain>
1271
-
1272
- # Execution data
1273
- result.state #=> "interrupted"
1274
- result.status #=> "failed"
1275
-
1276
- # Fault data
1277
- result.reason #=> "Build tool not found"
1278
- result.cause #=> <CMDx::FailFault>
1279
- result.metadata #=> { error_code: "BUILD_TOOL.NOT_FOUND" }
1280
- ```
1281
-
1282
- ## Lifecycle Information
1283
-
1284
- Results provide comprehensive methods for checking execution state and status:
1285
-
1286
- ```ruby
1287
- result = BuildApplication.execute(version: "1.2.3")
1288
-
1289
- # State predicates (execution lifecycle)
1290
- result.complete? #=> true (successful completion)
1291
- result.interrupted? #=> false (no interruption)
1292
- result.executed? #=> true (execution finished)
1293
-
1294
- # Status predicates (execution outcome)
1295
- result.success? #=> true (successful execution)
1296
- result.failed? #=> false (no failure)
1297
- result.skipped? #=> false (not skipped)
1298
-
1299
- # Outcome categorization
1300
- result.good? #=> true (success or skipped)
1301
- result.bad? #=> false (skipped or failed)
1302
- ```
1303
-
1304
- ## Outcome Analysis
1305
-
1306
- Results provide unified outcome determination depending on the fault causal chain:
1307
-
1308
- ```ruby
1309
- result = BuildApplication.execute(version: "1.2.3")
1310
-
1311
- result.outcome #=> "success" (state and status)
1312
- ```
1313
-
1314
- ## Chain Analysis
1315
-
1316
- Use these methods to trace the root cause of faults or trace the cause points.
1317
-
1318
- ```ruby
1319
- result = DeploymentWorkflow.execute(app_name: "webapp")
1320
-
1321
- if result.failed?
1322
- # Find the original cause of failure
1323
- if original_failure = result.caused_failure
1324
- puts "Root cause: #{original_failure.task.class.name}"
1325
- puts "Reason: #{original_failure.reason}"
1326
- end
1327
-
1328
- # Find what threw the failure to this result
1329
- if throwing_task = result.threw_failure
1330
- puts "Failure source: #{throwing_task.task.class.name}"
1331
- puts "Reason: #{throwing_task.reason}"
1332
- end
1333
-
1334
- # Failure classification
1335
- result.caused_failure? #=> true if this result was the original cause
1336
- result.threw_failure? #=> true if this result threw a failure
1337
- result.thrown_failure? #=> true if this result received a thrown failure
1338
- end
1339
- ```
1340
-
1341
- ## Index and Position
1342
-
1343
- Results track their position within execution chains:
1344
-
1345
- ```ruby
1346
- result = BuildApplication.execute(version: "1.2.3")
1347
-
1348
- # Position in execution sequence
1349
- result.index #=> 0 (first task in chain)
1350
-
1351
- # Access via chain
1352
- result.chain.results[result.index] == result #=> true
1353
- ```
1354
-
1355
- ## Block Yield
1356
-
1357
- Implement conditional logic using a block expression that yields a result for complete encapsulation.
1358
-
1359
- ```ruby
1360
- BuildApplication.execute(version: "1.2.3") do |result|
1361
- if result.success?
1362
- notify_deployment_ready(result)
1363
- elsif result.failed?
1364
- handle_build_failure(result)
1365
- else
1366
- log_skip_reason(result)
1367
- end
1368
- end
1369
- ```
1370
-
1371
- ## Handlers
1372
-
1373
- Use result handlers for clean, functional-style conditional logic. Handlers return the result object, enabling method chaining and fluent interfaces.
1374
-
1375
- ```ruby
1376
- result = BuildApplication.execute(version: "1.2.3")
1377
-
1378
- # Status-based handlers
1379
- result
1380
- .handle_success { |result| notify_deployment_ready(result) }
1381
- .handle_failed { |result| handle_build_failure(result) }
1382
- .handle_skipped { |result| log_skip_reason(result) }
1383
-
1384
- # State-based handlers
1385
- result
1386
- .handle_complete { |result| update_build_status(result) }
1387
- .handle_interrupted { |result| cleanup_partial_artifacts(result) }
1388
-
1389
- # Outcome-based handlers
1390
- result
1391
- .handle_good { |result| increment_success_counter(result) }
1392
- .handle_bad { |result| alert_operations_team(result) }
1393
- ```
1394
-
1395
- ## Pattern Matching
1396
-
1397
- Results support Ruby's pattern matching through array and hash deconstruction:
1398
-
1399
- > [!IMPORTANT]
1400
- > Pattern matching requires Ruby 3.0+
1401
-
1402
- ### Array Pattern
1403
-
1404
- ```ruby
1405
- result = BuildApplication.execute(version: "1.2.3")
1406
-
1407
- case result
1408
- in ["complete", "success"]
1409
- redirect_to build_success_page
1410
- in ["interrupted", "failed"]
1411
- retry_build_with_backoff(result)
1412
- in ["interrupted", "skipped"]
1413
- log_skip_and_continue
1414
- end
1415
- ```
1416
-
1417
- ### Hash Pattern
1418
-
1419
- ```ruby
1420
- result = BuildApplication.execute(version: "1.2.3")
1421
-
1422
- case result
1423
- in { state: "complete", status: "success" }
1424
- celebrate_build_success
1425
- in { status: "failed", metadata: { retryable: true } }
1426
- schedule_build_retry(result)
1427
- in { bad: true, metadata: { reason: String => reason } }
1428
- escalate_build_error("Build failed: #{reason}")
1429
- end
1430
- ```
1431
-
1432
- ### Pattern Guards
1433
-
1434
- ```ruby
1435
- case result
1436
- in { status: "failed", metadata: { attempts: n } } if n < 3
1437
- retry_build_with_delay(result, n * 2)
1438
- in { status: "failed", metadata: { attempts: n } } if n >= 3
1439
- mark_build_permanently_failed(result)
1440
- in { runtime: time } if time > performance_threshold
1441
- investigate_build_performance(result)
1442
- end
1443
- ```
1444
-
1445
- ---
1446
-
1447
- url: https://github.com/drexed/cmdx/blob/main/docs/outcomes/states.md
1448
- ---
1449
-
1450
- # Outcomes - States
1451
-
1452
- States represent the execution lifecycle condition of task execution, tracking
1453
- the progress of tasks through their complete execution journey. States provide
1454
- insight into where a task is in its lifecycle and enable lifecycle-based
1455
- decision making and monitoring.
1456
-
1457
- ## Definitions
1458
-
1459
- | State | Description |
1460
- | ----- | ----------- |
1461
- | `initialized` | Task created but execution not yet started. Default state for new tasks. |
1462
- | `executing` | Task is actively running its business logic. Transient state during execution. |
1463
- | `complete` | Task finished execution successfully without any interruption or halt. |
1464
- | `interrupted` | Task execution was stopped due to a fault, exception, or explicit halt. |
1465
-
1466
- State-Status combinations:
1467
-
1468
- | State | Status | Meaning |
1469
- | ----- | ------ | ------- |
1470
- | `initialized` | `success` | Task created, not yet executed |
1471
- | `executing` | `success` | Task currently running |
1472
- | `complete` | `success` | Task finished successfully |
1473
- | `complete` | `skipped` | Task finished by skipping execution |
1474
- | `interrupted` | `failed` | Task stopped due to failure |
1475
- | `interrupted` | `skipped` | Task stopped by skip condition |
1476
-
1477
- ## Transitions
1478
-
1479
- > [!CAUTION]
1480
- > States are automatically managed during task execution and should **never** be modified manually. State transitions are handled internally by the CMDx framework.
1481
-
1482
- ```ruby
1483
- # Valid state transition flow
1484
- initialized → executing → complete (successful execution)
1485
- initialized → executing → interrupted (skipped/failed execution)
1486
- ```
1487
-
1488
- ## Predicates
1489
-
1490
- Use state predicates to check the current execution lifecycle:
1491
-
1492
- ```ruby
1493
- result = ProcessVideoUpload.execute
1494
-
1495
- # Individual state checks
1496
- result.initialized? #=> false (after execution)
1497
- result.executing? #=> false (after execution)
1498
- result.complete? #=> true (successful completion)
1499
- result.interrupted? #=> false (no interruption)
1500
-
1501
- # State categorization
1502
- result.executed? #=> true (complete OR interrupted)
1503
- ```
1504
-
1505
- ## Handlers
1506
-
1507
- 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.
1508
-
1509
- ```ruby
1510
- result = ProcessVideoUpload.execute
1511
-
1512
- # Individual state handlers
1513
- result
1514
- .handle_complete { |result| send_upload_notification(result) }
1515
- .handle_interrupted { |result| cleanup_temp_files(result) }
1516
- .handle_executed { |result| log_upload_metrics(result) }
1517
- ```
1518
-
1519
- ---
1520
-
1521
- url: https://github.com/drexed/cmdx/blob/main/docs/outcomes/statuses.md
1522
- ---
1523
-
1524
- # Outcomes - Statuses
1525
-
1526
- 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.
1527
-
1528
- ## Definitions
1529
-
1530
- | Status | Description |
1531
- | ------ | ----------- |
1532
- | `success` | Task execution completed successfully with expected business outcome. Default status for all tasks. |
1533
- | `skipped` | Task intentionally stopped execution because conditions weren't met or continuation was unnecessary. |
1534
- | `failed` | Task stopped execution due to business rule violations, validation errors, or exceptions. |
1535
-
1536
- ## Transitions
1537
-
1538
- > [!IMPORTANT]
1539
- > 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.
1540
-
1541
- ```ruby
1542
- # Valid status transitions
1543
- success → skipped # via skip!
1544
- success → failed # via fail! or exception
1545
-
1546
- # Invalid transitions (will raise errors)
1547
- skipped → success # ❌ Cannot transition
1548
- skipped → failed # ❌ Cannot transition
1549
- failed → success # ❌ Cannot transition
1550
- failed → skipped # ❌ Cannot transition
1551
- ```
1552
-
1553
- ## Predicates
1554
-
1555
- Use status predicates to check execution outcomes:
1556
-
1557
- ```ruby
1558
- result = ProcessNotification.execute
1559
-
1560
- # Individual status checks
1561
- result.success? #=> true/false
1562
- result.skipped? #=> true/false
1563
- result.failed? #=> true/false
1564
-
1565
- # Outcome categorization
1566
- result.good? #=> true if success OR skipped
1567
- result.bad? #=> true if skipped OR failed (not success)
1568
- ```
1569
-
1570
- ## Handlers
1571
-
1572
- 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.
1573
-
1574
- ```ruby
1575
- result = ProcessNotification.execute
1576
-
1577
- # Individual status handlers
1578
- result
1579
- .handle_success { |result| mark_notification_sent(result) }
1580
- .handle_skipped { |result| log_notification_skipped(result) }
1581
- .handle_failed { |result| queue_retry_notification(result) }
1582
-
1583
- # Outcome-based handlers
1584
- result
1585
- .handle_good { |result| update_message_stats(result) }
1586
- .handle_bad { |result| track_delivery_failure(result) }
1587
- ```
1588
-
1589
- ---
1590
-
1591
- url: https://github.com/drexed/cmdx/blob/main/docs/attributes/definitions.md
1592
- ---
1593
-
1594
- # Attributes - Definitions
1595
-
1596
- Attributes define the interface between task callers and implementation, enabling automatic validation, type coercion, and method generation. They provide a contract to verify that task execution arguments match expected requirements and structure.
1597
-
1598
- ## Declarations
1599
-
1600
- > [!TIP]
1601
- > Prefer using the `required` and `optional` alias for `attributes` for brevity and to clearly signal intent.
1602
-
1603
- ### Optional
1604
-
1605
- Optional attributes return `nil` when not provided.
1606
-
1607
- ```ruby
1608
- class ScheduleEvent < CMDx::Task
1609
- attribute :title
1610
- attributes :duration, :location
1611
-
1612
- # Alias for attributes (preferred)
1613
- optional :description
1614
- optional :visibility, :attendees
1615
-
1616
- def work
1617
- title #=> "Team Standup"
1618
- duration #=> 30
1619
- location #=> nil
1620
- description #=> nil
1621
- visibility #=> nil
1622
- attendees #=> ["alice@company.com", "bob@company.com"]
1623
- end
1624
- end
1625
-
1626
- # Attributes passed as keyword arguments
1627
- ScheduleEvent.execute(
1628
- title: "Team Standup",
1629
- duration: 30,
1630
- attendees: ["alice@company.com", "bob@company.com"]
1631
- )
1632
- ```
1633
-
1634
- ### Required
1635
-
1636
- Required attributes must be provided in call arguments or task execution will fail.
1637
-
1638
- ```ruby
1639
- class PublishArticle < CMDx::Task
1640
- attribute :title, required: true
1641
- attributes :content, :author_id, required: true
1642
-
1643
- # Alias for attributes => required: true (preferred)
1644
- required :category
1645
- required :status, :tags
1646
-
1647
- def work
1648
- title #=> "Getting Started with Ruby"
1649
- content #=> "This is a comprehensive guide..."
1650
- author_id #=> 42
1651
- category #=> "programming"
1652
- status #=> :published
1653
- tags #=> ["ruby", "beginner"]
1654
- end
1655
- end
1656
-
1657
- # Attributes passed as keyword arguments
1658
- PublishArticle.execute(
1659
- title: "Getting Started with Ruby",
1660
- content: "This is a comprehensive guide...",
1661
- author_id: 42,
1662
- category: "programming",
1663
- status: :published,
1664
- tags: ["ruby", "beginner"]
1665
- )
1666
- ```
1667
-
1668
- ## Sources
1669
-
1670
- Attributes delegate to accessible objects within the task. The default source is `:context`, but any accessible method or object can serve as an attribute source.
1671
-
1672
- ### Context
1673
-
1674
- ```ruby
1675
- class BackupDatabase < CMDx::Task
1676
- # Default source is :context
1677
- required :database_name
1678
- optional :compression_level
1679
-
1680
- # Explicitly specify context source
1681
- attribute :backup_path, source: :context
1682
-
1683
- def work
1684
- database_name #=> context.database_name
1685
- backup_path #=> context.backup_path
1686
- compression_level #=> context.compression_level
1687
- end
1688
- end
1689
- ```
1690
-
1691
- ### Symbol References
1692
-
1693
- Reference instance methods by symbol for dynamic source values:
1694
-
1695
- ```ruby
1696
- class BackupDatabase < CMDx::Task
1697
- attributes :host, :credentials, source: :database_config
1698
-
1699
- # Access from declared attributes
1700
- attribute :connection_string, source: :credentials
1701
-
1702
- def work
1703
- # Your logic here...
1704
- end
1705
-
1706
- private
1707
-
1708
- def database_config
1709
- @database_config ||= DatabaseConfig.find(context.database_name)
1710
- end
1711
- end
1712
- ```
1713
-
1714
- ### Proc or Lambda
1715
-
1716
- Use anonymous functions for dynamic source values:
1717
-
1718
- ```ruby
1719
- class BackupDatabase < CMDx::Task
1720
- # Proc
1721
- attribute :timestamp, source: proc { Time.current }
1722
-
1723
- # Lambda
1724
- attribute :server, source: -> { Current.server }
1725
- end
1726
- ```
1727
-
1728
- ### Class or Module
1729
-
1730
- For complex source logic, use classes or modules:
1731
-
1732
- ```ruby
1733
- class DatabaseResolver
1734
- def self.call(task)
1735
- Database.find(task.context.database_name)
1736
- end
1737
- end
1738
-
1739
- class BackupDatabase < CMDx::Task
1740
- # Class or Module
1741
- attribute :schema, source: DatabaseResolver
1742
-
1743
- # Instance
1744
- attribute :metadata, source: DatabaseResolver.new
1745
- end
1746
- ```
1747
-
1748
- ## Nesting
1749
-
1750
- Nested attributes enable complex attribute structures where child attributes automatically inherit their parent as the source. This allows validation and access of structured data.
1751
-
1752
- > [!NOTE]
1753
- > All options available to top-level attributes are available to nested attributes, eg: naming, coercions, and validations
1754
-
1755
- ```ruby
1756
- class ConfigureServer < CMDx::Task
1757
- # Required parent with required children
1758
- required :network_config do
1759
- required :hostname, :port, :protocol, :subnet
1760
- optional :load_balancer
1761
- attribute :firewall_rules
1762
- end
1763
-
1764
- # Optional parent with conditional children
1765
- optional :ssl_config do
1766
- required :certificate_path, :private_key # Only required if ssl_config provided
1767
- optional :enable_http2, prefix: true
1768
- end
1769
-
1770
- # Multi-level nesting
1771
- attribute :monitoring do
1772
- required :provider
1773
-
1774
- optional :alerting do
1775
- required :threshold_percentage
1776
- optional :notification_channel
1777
- end
1778
- end
1779
-
1780
- def work
1781
- network_config #=> { hostname: "api.company.com" ... }
1782
- hostname #=> "api.company.com"
1783
- load_balancer #=> nil
1784
- end
1785
- end
1786
-
1787
- ConfigureServer.execute(
1788
- server_id: "srv-001",
1789
- network_config: {
1790
- hostname: "api.company.com",
1791
- port: 443,
1792
- protocol: "https",
1793
- subnet: "10.0.1.0/24",
1794
- firewall_rules: "allow_web_traffic"
1795
- },
1796
- monitoring: {
1797
- provider: "datadog",
1798
- alerting: {
1799
- threshold_percentage: 85.0,
1800
- notification_channel: "slack"
1801
- }
1802
- }
1803
- )
1804
- ```
1805
-
1806
- > [!IMPORTANT]
1807
- > Child attributes are only required when their parent attribute is provided, enabling flexible optional structures.
1808
-
1809
- ## Error Handling
1810
-
1811
- Attribute validation failures result in structured error information with details about each failed attribute.
1812
-
1813
- > [!NOTE]
1814
- > Nested attributes are only ever evaluated when the parent attribute is available and valid.
1815
-
1816
- ```ruby
1817
- class ConfigureServer < CMDx::Task
1818
- required :server_id, :environment
1819
- required :network_config do
1820
- required :hostname, :port
1821
- end
1822
-
1823
- def work
1824
- # Your logic here...
1825
- end
1826
- end
1827
-
1828
- # Missing required top-level attributes
1829
- result = ConfigureServer.execute(server_id: "srv-001")
1830
-
1831
- result.state #=> "interrupted"
1832
- result.status #=> "failed"
1833
- result.reason #=> "Invalid"
1834
- result.metadata #=> {
1835
- # errors: {
1836
- # full_message: "environment is required. network_config is required.",
1837
- # messages: {
1838
- # environment: ["is required"],
1839
- # network_config: ["is required"]
1840
- # }
1841
- # }
1842
- # }
1843
-
1844
- # Missing required nested attributes
1845
- result = ConfigureServer.execute(
1846
- server_id: "srv-001",
1847
- environment: "production",
1848
- network_config: { hostname: "api.company.com" } # Missing port
1849
- )
1850
-
1851
- result.state #=> "interrupted"
1852
- result.status #=> "failed"
1853
- result.reason #=> "Invalid"
1854
- result.metadata #=> {
1855
- # errors: {
1856
- # full_message: "port is required.",
1857
- # messages: {
1858
- # port: ["is required"]
1859
- # }
1860
- # }
1861
- # }
1862
- ```
1863
-
1864
- ---
1865
-
1866
- url: https://github.com/drexed/cmdx/blob/main/docs/attributes/naming.md
1867
- ---
1868
-
1869
- # Attributes - Naming
1870
-
1871
- Attribute naming provides method name customization to prevent conflicts and enable flexible attribute access patterns. When attributes share names with existing methods or when multiple attributes from different sources have the same name, affixing ensures clean method resolution within tasks.
1872
-
1873
- > [!NOTE]
1874
- > Affixing modifies only the generated accessor method names within tasks.
1875
-
1876
- ## Prefix
1877
-
1878
- Adds a prefix to the generated accessor method name.
1879
-
1880
- ```ruby
1881
- class GenerateReport < CMDx::Task
1882
- # Dynamic from attribute source
1883
- attribute :template, prefix: true
1884
-
1885
- # Static
1886
- attribute :format, prefix: "report_"
1887
-
1888
- def work
1889
- context_template #=> "monthly_sales"
1890
- report_format #=> "pdf"
1891
- end
1892
- end
1893
-
1894
- # Attributes passed as original attribute names
1895
- GenerateReport.execute(template: "monthly_sales", format: "pdf")
1896
- ```
1897
-
1898
- ## Suffix
1899
-
1900
- Adds a suffix to the generated accessor method name.
1901
-
1902
- ```ruby
1903
- class DeployApplication < CMDx::Task
1904
- # Dynamic from attribute source
1905
- attribute :branch, suffix: true
1906
-
1907
- # Static
1908
- attribute :version, suffix: "_tag"
1909
-
1910
- def work
1911
- branch_context #=> "main"
1912
- version_tag #=> "v1.2.3"
1913
- end
1914
- end
1915
-
1916
- # Attributes passed as original attribute names
1917
- DeployApplication.execute(branch: "main", version: "v1.2.3")
1918
- ```
1919
-
1920
- ## As
1921
-
1922
- Completely renames the generated accessor method.
1923
-
1924
- ```ruby
1925
- class ScheduleMaintenance < CMDx::Task
1926
- attribute :scheduled_at, as: :when
1927
-
1928
- def work
1929
- when #=> <DateTime>
1930
- end
1931
- end
1932
-
1933
- # Attributes passed as original attribute names
1934
- ScheduleMaintenance.execute(scheduled_at: DateTime.new(2024, 12, 15, 2, 0, 0))
1935
- ```
1936
-
1937
- ---
1938
-
1939
- url: https://github.com/drexed/cmdx/blob/main/docs/attributes/coercions.md
1940
- ---
1941
-
1942
- # Attributes - Coercions
1943
-
1944
- Attribute coercions automatically convert task arguments to expected types, ensuring type safety while providing flexible input handling. Coercions transform raw input values into the specified types, supporting simple conversions like string-to-integer and complex operations like JSON parsing.
1945
-
1946
- Check out the [Getting Started](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#coercions) docs for global configuration.
1947
-
1948
- ## Usage
1949
-
1950
- Define attribute types to enable automatic coercion:
1951
-
1952
- ```ruby
1953
- class ParseMetrics < CMDx::Task
1954
- # Coerce into a symbol
1955
- attribute :measurement_type, type: :symbol
1956
-
1957
- # Coerce into a rational fallback to big decimal
1958
- attribute :value, type: [:rational, :big_decimal]
1959
-
1960
- # Coerce with options
1961
- attribute :recorded_at, type: :date, strptime: "%m-%d-%Y"
1962
-
1963
- def work
1964
- measurement_type #=> :temperature
1965
- recorded_at #=> <Date 2024-01-23>
1966
- value #=> 98.6 (Float)
1967
- end
1968
- end
1969
-
1970
- ParseMetrics.execute(
1971
- measurement_type: "temperature",
1972
- recorded_at: "01-23-2020",
1973
- value: "98.6"
1974
- )
1975
- ```
1976
-
1977
- > [!TIP]
1978
- > Specify multiple coercion types for attributes that could be a variety of value formats. CMDx attempts each type in order until one succeeds.
1979
-
1980
- ## Built-in Coercions
1981
-
1982
- | Type | Options | Description | Examples |
1983
- |------|---------|-------------|----------|
1984
- | `:array` | | Array conversion with JSON support | `"val"` → `["val"]`<br>`"[1,2,3]"` → `[1, 2, 3]` |
1985
- | `:big_decimal` | `:precision` | High-precision decimal | `"123.456"` → `BigDecimal("123.456")` |
1986
- | `:boolean` | | Boolean with text patterns | `"yes"` → `true`, `"no"` → `false` |
1987
- | `:complex` | | Complex numbers | `"1+2i"` → `Complex(1, 2)` |
1988
- | `:date` | `:strptime` | Date objects | `"2024-01-23"` → `Date.new(2024, 1, 23)` |
1989
- | `:datetime` | `:strptime` | DateTime objects | `"2024-01-23 10:30"` → `DateTime.new(2024, 1, 23, 10, 30)` |
1990
- | `:float` | | Floating-point numbers | `"123.45"` → `123.45` |
1991
- | `:hash` | | Hash conversion with JSON support | `'{"a":1}'` → `{"a" => 1}` |
1992
- | `:integer` | | Integer with hex/octal support | `"0xFF"` → `255`, `"077"` → `63` |
1993
- | `:rational` | | Rational numbers | `"1/2"` → `Rational(1, 2)` |
1994
- | `:string` | | String conversion | `123` → `"123"` |
1995
- | `:symbol` | | Symbol conversion | `"abc"` → `:abc` |
1996
- | `:time` | `:strptime` | Time objects | `"10:30:00"` → `Time.new(2024, 1, 23, 10, 30)` |
1997
-
1998
- ## Declarations
1999
-
2000
- > [!IMPORTANT]
2001
- > Coercions must raise a CMDx::CoercionError and its message is used as part of the fault reason and metadata.
2002
-
2003
- ### Proc or Lambda
2004
-
2005
- Use anonymous functions for simple coercion logic:
2006
-
2007
- ```ruby
2008
- class TransformCoordinates < CMDx::Task
2009
- # Proc
2010
- register :callback, :geolocation, proc do |value, options = {}|
2011
- begin
2012
- Geolocation(value)
2013
- rescue StandardError
2014
- raise CMDx::CoercionError, "could not convert into a geolocation"
2015
- end
2016
- end
2017
-
2018
- # Lambda
2019
- register :callback, :geolocation, ->(value, options = {}) {
2020
- begin
2021
- Geolocation(value)
2022
- rescue StandardError
2023
- raise CMDx::CoercionError, "could not convert into a geolocation"
2024
- end
2025
- }
2026
- end
2027
- ```
2028
-
2029
- ### Class or Module
2030
-
2031
- Register custom coercion logic for specialized type handling:
2032
-
2033
- ```ruby
2034
- class GeolocationCoercion
2035
- def self.call(value, options = {})
2036
- Geolocation(value)
2037
- rescue StandardError
2038
- raise CMDx::CoercionError, "could not convert into a geolocation"
2039
- end
2040
- end
2041
-
2042
- class TransformCoordinates < CMDx::Task
2043
- register :coercion, :geolocation, GeolocationCoercion
2044
-
2045
- attribute :latitude, type: :geolocation
2046
- end
2047
- ```
2048
-
2049
- ## Removals
2050
-
2051
- Remove custom coercions when no longer needed:
2052
-
2053
- > [!WARNING]
2054
- > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
2055
-
2056
- ```ruby
2057
- class TransformCoordinates < CMDx::Task
2058
- deregister :coercion, :geolocation
2059
- end
2060
- ```
2061
-
2062
- ## Error Handling
2063
-
2064
- Coercion failures provide detailed error information including attribute paths, attempted types, and specific failure reasons:
2065
-
2066
- ```ruby
2067
- class AnalyzePerformance < CMDx::Task
2068
- attribute :iterations, type: :integer
2069
- attribute :score, type: [:float, :big_decimal]
2070
-
2071
- def work
2072
- # Your logic here...
2073
- end
2074
- end
2075
-
2076
- result = AnalyzePerformance.execute(
2077
- iterations: "not-a-number",
2078
- score: "invalid-float"
2079
- )
2080
-
2081
- result.state #=> "interrupted"
2082
- result.status #=> "failed"
2083
- result.reason #=> "Invalid"
2084
- result.metadata #=> {
2085
- # errors: {
2086
- # full_message: "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal.",
2087
- # messages: {
2088
- # iterations: ["could not coerce into an integer"],
2089
- # score: ["could not coerce into one of: float, big_decimal"]
2090
- # }
2091
- # }
2092
- # }
2093
- ```
2094
-
2095
- ---
2096
-
2097
- url: https://github.com/drexed/cmdx/blob/main/docs/attributes/validations.md
2098
- ---
2099
-
2100
- # Attributes - Validations
2101
-
2102
- Attribute validations ensure task arguments meet specified requirements before execution begins. Validations run after coercions and provide declarative rules for data integrity, supporting both built-in validators and custom validation logic.
2103
-
2104
- Check out the [Getting Started](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#validations) docs for global configuration.
2105
-
2106
- ## Usage
2107
-
2108
- Define validation rules on attributes to enforce data requirements:
2109
-
2110
- ```ruby
2111
- class ProcessSubscription < CMDx::Task
2112
- # Required field with presence validation
2113
- attribute :user_id, presence: true
2114
-
2115
- # String with length constraints
2116
- attribute :preferences, length: { minimum: 10, maximum: 500 }
2117
-
2118
- # Numeric range validation
2119
- attribute :tier_level, inclusion: { in: 1..5 }
2120
-
2121
- # Format validation for email
2122
- attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
2123
-
2124
- def work
2125
- user_id #=> "98765"
2126
- preferences #=> "Send weekly digest emails"
2127
- tier_level #=> 3
2128
- contact_email #=> "user@company.com"
2129
- end
2130
- end
2131
-
2132
- ProcessSubscription.execute(
2133
- user_id: "98765",
2134
- preferences: "Send weekly digest emails",
2135
- tier_level: 3,
2136
- contact_email: "user@company.com"
2137
- )
2138
- ```
2139
-
2140
- > [!TIP]
2141
- > Validations run after coercions, so you can validate the final coerced values rather than raw input.
2142
-
2143
- ## Built-in Validators
2144
-
2145
- ### Common Options
2146
-
2147
- This list of options is available to all validators:
2148
-
2149
- | Option | Description |
2150
- |--------|-------------|
2151
- | `:allow_nil` | Skip validation when value is `nil` |
2152
- | `:if` | Symbol, proc, lambda, or callable determining when to validate |
2153
- | `:unless` | Symbol, proc, lambda, or callable determining when to skip validation |
2154
- | `:message` | Custom error message for validation failures |
2155
-
2156
- ### Exclusion
2157
-
2158
- ```ruby
2159
- class ProcessProduct < CMDx::Task
2160
- attribute :status, exclusion: { in: %w[recalled archived] }
2161
-
2162
- def work
2163
- # Your logic here...
2164
- end
2165
- end
2166
- ```
2167
-
2168
- | Options | Description |
2169
- |---------|-------------|
2170
- | `:in` | The collection of forbidden values or range |
2171
- | `:within` | Alias for :in option |
2172
- | `:of_message` | Custom message for discrete value exclusions |
2173
- | `:in_message` | Custom message for range-based exclusions |
2174
- | `:within_message` | Alias for :in_message option |
2175
-
2176
- ### Format
2177
-
2178
- ```ruby
2179
- class ProcessProduct < CMDx::Task
2180
- attribute :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/
2181
-
2182
- attribute :sku, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ }
2183
-
2184
- def work
2185
- # Your logic here...
2186
- end
2187
- end
2188
- ```
2189
-
2190
- | Options | Description |
2191
- |---------|-------------|
2192
- | `regexp` | Alias for :with option |
2193
- | `:with` | Regex pattern that the value must match |
2194
- | `:without` | Regex pattern that the value must not match |
2195
-
2196
- ### Inclusion
2197
-
2198
- ```ruby
2199
- class ProcessProduct < CMDx::Task
2200
- attribute :availability, inclusion: { in: %w[available limited] }
2201
-
2202
- def work
2203
- # Your logic here...
2204
- end
2205
- end
2206
- ```
2207
-
2208
- | Options | Description |
2209
- |---------|-------------|
2210
- | `:in` | The collection of allowed values or range |
2211
- | `:within` | Alias for :in option |
2212
- | `:of_message` | Custom message for discrete value inclusions |
2213
- | `:in_message` | Custom message for range-based inclusions |
2214
- | `:within_message` | Alias for :in_message option |
2215
-
2216
- ### Length
2217
-
2218
- ```ruby
2219
- class CreateBlogPost < CMDx::Task
2220
- attribute :title, length: { within: 5..100 }
2221
-
2222
- def work
2223
- # Your logic here...
2224
- end
2225
- end
2226
- ```
2227
-
2228
- | Options | Description |
2229
- |---------|-------------|
2230
- | `:within` | Range that the length must fall within (inclusive) |
2231
- | `:not_within` | Range that the length must not fall within |
2232
- | `:in` | Alias for :within |
2233
- | `:not_in` | Range that the length must not fall within |
2234
- | `:min` | Minimum allowed length |
2235
- | `:max` | Maximum allowed length |
2236
- | `:is` | Exact required length |
2237
- | `:is_not` | Length that is not allowed |
2238
- | `:within_message` | Custom message for within/range validations |
2239
- | `:in_message` | Custom message for :in validation |
2240
- | `:not_within_message` | Custom message for not_within validation |
2241
- | `:not_in_message` | Custom message for not_in validation |
2242
- | `:min_message` | Custom message for minimum length validation |
2243
- | `:max_message` | Custom message for maximum length validation |
2244
- | `:is_message` | Custom message for exact length validation |
2245
- | `:is_not_message` | Custom message for is_not validation |
2246
-
2247
- ### Numeric
2248
-
2249
- ```ruby
2250
- class CreateBlogPost < CMDx::Task
2251
- attribute :word_count, numeric: { min: 100 }
2252
-
2253
- def work
2254
- # Your logic here...
2255
- end
2256
- end
2257
- ```
2258
-
2259
- | Options | Description |
2260
- |---------|-------------|
2261
- | `:within` | Range that the value must fall within (inclusive) |
2262
- | `:not_within` | Range that the value must not fall within |
2263
- | `:in` | Alias for :within option |
2264
- | `:not_in` | Alias for :not_within option |
2265
- | `:min` | Minimum allowed value (inclusive, >=) |
2266
- | `:max` | Maximum allowed value (inclusive, <=) |
2267
- | `:is` | Exact value that must match |
2268
- | `:is_not` | Value that must not match |
2269
- | `:within_message` | Custom message for range validations |
2270
- | `:not_within_message` | Custom message for exclusion validations |
2271
- | `:min_message` | Custom message for minimum validation |
2272
- | `:max_message` | Custom message for maximum validation |
2273
- | `:is_message` | Custom message for exact match validation |
2274
- | `:is_not_message` | Custom message for exclusion validation |
2275
-
2276
- ### Presence
2277
-
2278
- ```ruby
2279
- class CreateBlogPost < CMDx::Task
2280
- attribute :content, presence: true
2281
-
2282
- attribute :content, presence: { message: "cannot be blank" }
2283
-
2284
- def work
2285
- # Your logic here...
2286
- end
2287
- end
2288
- ```
2289
-
2290
- | Options | Description |
2291
- |---------|-------------|
2292
- | `true` | Ensures value is not nil, empty string, or whitespace |
2293
-
2294
- ## Declarations
2295
-
2296
- > [!IMPORTANT]
2297
- > Custom validators must raise a `CMDx::ValidationError` and its message is used as part of the fault reason and metadata.
2298
-
2299
- ### Proc or Lambda
2300
-
2301
- Use anonymous functions for simple validation logic:
2302
-
2303
- ```ruby
2304
- class SetupApplication < CMDx::Task
2305
- # Proc
2306
- register :validator, :api_key, proc do |value, options = {}|
2307
- unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
2308
- raise CMDx::ValidationError, "invalid API key format"
2309
- end
2310
- end
2311
-
2312
- # Lambda
2313
- register :validator, :api_key, ->(value, options = {}) {
2314
- unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
2315
- raise CMDx::ValidationError, "invalid API key format"
2316
- end
2317
- }
2318
- end
2319
- ```
2320
-
2321
- ### Class or Module
2322
-
2323
- Register custom validation logic for specialized requirements:
2324
-
2325
- ```ruby
2326
- class ApiKeyValidator
2327
- def self.call(value, options = {})
2328
- unless value.match?(/\A[a-zA-Z0-9]{32}\z/)
2329
- raise CMDx::ValidationError, "invalid API key format"
2330
- end
2331
- end
2332
- end
2333
-
2334
- class SetupApplication < CMDx::Task
2335
- register :validator, :api_key, ApiKeyValidator
2336
-
2337
- attribute :access_key, api_key: true
2338
- end
2339
- ```
2340
-
2341
- ## Removals
2342
-
2343
- Remove custom validators when no longer needed:
2344
-
2345
- > [!WARNING]
2346
- > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
2347
-
2348
- ```ruby
2349
- class SetupApplication < CMDx::Task
2350
- deregister :validator, :api_key
2351
- end
2352
- ```
2353
-
2354
- ## Error Handling
2355
-
2356
- Validation failures provide detailed error information including attribute paths, validation rules, and specific failure reasons:
2357
-
2358
- ```ruby
2359
- class CreateProject < CMDx::Task
2360
- attribute :project_name, presence: true, length: { minimum: 3, maximum: 50 }
2361
- attribute :budget, numeric: { greater_than: 1000, less_than: 1000000 }
2362
- attribute :priority, inclusion: { in: [:low, :medium, :high] }
2363
- attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
2364
-
2365
- def work
2366
- # Your logic here...
2367
- end
2368
- end
2369
-
2370
- result = CreateProject.execute(
2371
- project_name: "AB", # Too short
2372
- budget: 500, # Too low
2373
- priority: :urgent, # Not in allowed list
2374
- contact_email: "invalid-email" # Invalid format
2375
- )
2376
-
2377
- result.state #=> "interrupted"
2378
- result.status #=> "failed"
2379
- result.reason #=> "Invalid"
2380
- result.metadata #=> {
2381
- # errors: {
2382
- # full_message: "project_name is too short (minimum is 3 characters). budget must be greater than 1000. priority is not included in the list. contact_email is invalid.",
2383
- # messages: {
2384
- # project_name: ["is too short (minimum is 3 characters)"],
2385
- # budget: ["must be greater than 1000"],
2386
- # priority: ["is not included in the list"],
2387
- # contact_email: ["is invalid"]
2388
- # }
2389
- # }
2390
- # }
2391
- ```
2392
-
2393
- ---
2394
-
2395
- url: https://github.com/drexed/cmdx/blob/main/docs/attributes/defaults.md
2396
- ---
2397
-
2398
- # Attributes - Defaults
2399
-
2400
- Attribute defaults provide fallback values when arguments are not provided or resolve to `nil`. Defaults ensure tasks have sensible values for optional attributes while maintaining flexibility for callers to override when needed.
2401
-
2402
- ## Declarations
2403
-
2404
- Defaults apply when attributes are not provided or resolve to `nil`. They work seamlessly with coercion, validation, and nested attributes.
2405
-
2406
- ### Static Values
2407
-
2408
- ```ruby
2409
- class OptimizeDatabase < CMDx::Task
2410
- attribute :strategy, default: :incremental
2411
- attribute :level, default: "basic"
2412
- attribute :notify_admin, default: true
2413
- attribute :timeout_minutes, default: 30
2414
- attribute :indexes, default: []
2415
- attribute :options, default: {}
2416
-
2417
- def work
2418
- strategy #=> :incremental
2419
- level #=> "basic"
2420
- notify_admin #=> true
2421
- timeout_minutes #=> 30
2422
- indexes #=> []
2423
- options #=> {}
2424
- end
2425
- end
2426
- ```
2427
-
2428
- ### Symbol References
2429
-
2430
- Reference instance methods by symbol for dynamic default values:
2431
-
2432
- ```ruby
2433
- class ProcessAnalytics < CMDx::Task
2434
- attribute :granularity, default: :default_granularity
2435
-
2436
- def work
2437
- # Your logic here...
2438
- end
2439
-
2440
- private
2441
-
2442
- def default_granularity
2443
- Current.user.premium? ? "hourly" : "daily"
2444
- end
2445
- end
2446
- ```
2447
-
2448
- ### Proc or Lambda
2449
-
2450
- Use anonymous functions for dynamic default values:
2451
-
2452
- ```ruby
2453
- class CacheContent < CMDx::Task
2454
- # Proc
2455
- attribute :expire_hours, default: proc { Current.tenant.cache_duration || 24 }
2456
-
2457
- # Lambda
2458
- attribute :compression, default: -> { Current.tenant.premium? ? "gzip" : "none" }
2459
- end
2460
- ```
2461
-
2462
- ## Coercions and Validations
2463
-
2464
- Defaults are subject to the same coercion and validation rules as provided values, ensuring consistency and catching configuration errors early.
2465
-
2466
- ```ruby
2467
- class ScheduleBackup < CMDx::Task
2468
- # Coercions
2469
- attribute :retention_days, default: "7", type: :integer
2470
-
2471
- # Validations
2472
- optional :frequency, default: "daily", inclusion: { in: %w[hourly daily weekly monthly] }
2473
- end
2474
- ```
2475
-
2476
- ---
2477
-
2478
- url: https://github.com/drexed/cmdx/blob/main/docs/attributes/transformations.md
2479
- ---
2480
-
2481
- # Attributes - Transformations
2482
-
2483
- Transformations allow you to modify attribute values after they are derived and coerced from their source but before any validations. This enables data normalization, formatting, and conditional processing within the attribute pipeline.
2484
-
2485
- ## Declarations
2486
-
2487
- ### Symbol References
2488
-
2489
- Reference instance methods by symbol for dynamic value transformations:
2490
-
2491
- ```ruby
2492
- class ProcessAnalytics < CMDx::Task
2493
- attribute :options, transform: :compact_blank
2494
- end
2495
- ```
2496
-
2497
- ### Proc or Lambda
2498
-
2499
- Use anonymous functions for dynamic value transformations:
2500
-
2501
- ```ruby
2502
- class CacheContent < CMDx::Task
2503
- # Proc
2504
- attribute :expire_hours, transform: proc { |v| v * 2 }
2505
-
2506
- # Lambda
2507
- attribute :compression, transform: ->(v) { v.to_s.upcase.strip[0..2] }
2508
- end
2509
- ```
2510
-
2511
- ### Class or Module
2512
-
2513
- Use any object that responds to `call` for reusable transformation logic:
2514
-
2515
- ```ruby
2516
- class EmailNormalizer
2517
- def call(value)
2518
- value.to_s.downcase.strip
2519
- end
2520
- end
2521
-
2522
- class ProcessContacts < CMDx::Task
2523
- # Class or Module
2524
- attribute :email, transform: EmailNormalizer
2525
-
2526
- # Instance
2527
- attribute :email, transform: EmailNormalizer.new
2528
- end
2529
- ```
2530
-
2531
- ## Validations
2532
-
2533
- Transformed values are subject to the same validation rules as untransformed values, ensuring consistency and catching configuration errors early.
2534
-
2535
- ```ruby
2536
- class ScheduleBackup < CMDx::Task
2537
- # Coercions
2538
- attribute :retention_days, type: :integer, transform: proc { |v| v.clamp(1, 5) }
2539
-
2540
- # Validations
2541
- optional :frequency, transform: :downcase, inclusion: { in: %w[hourly daily weekly monthly] }
2542
- end
2543
- ```
2544
-
2545
- ---
2546
-
2547
- - **Prev:** [Attributes - Defaults](defaults.md)
2548
- - **Next:** [Callbacks](../callbacks.md)
2549
-
2550
- ---
2551
-
2552
- url: https://github.com/drexed/cmdx/blob/main/docs/callbacks.md
2553
- ---
2554
-
2555
- # Callbacks
2556
-
2557
- 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.
2558
-
2559
- Check out the [Getting Started](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#callbacks) docs for global configuration.
2560
-
2561
- > [!IMPORTANT]
2562
- > 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).
2563
-
2564
- ## Available Callbacks
2565
-
2566
- Callbacks execute in precise lifecycle order. Here is the complete execution sequence:
2567
-
2568
- ```ruby
2569
- 1. before_validation # Pre-validation setup
2570
- 2. before_execution # Setup and preparation
2571
-
2572
- # --- Task#work executed ---
2573
-
2574
- 3. on_[complete|interrupted] # Based on execution state
2575
- 4. on_executed # Task finished (any outcome)
2576
- 5. on_[success|skipped|failed] # Based on execution status
2577
- 6. on_[good|bad] # Based on outcome classification
2578
- ```
2579
-
2580
- ## Declarations
2581
-
2582
- ### Symbol References
2583
-
2584
- Reference instance methods by symbol for simple callback logic:
2585
-
2586
- ```ruby
2587
- class ProcessBooking < CMDx::Task
2588
- before_execution :find_reservation
2589
-
2590
- # Batch declarations (works for any type)
2591
- on_complete :notify_guest, :update_availability
2592
-
2593
- def work
2594
- # Your logic here...
2595
- end
2596
-
2597
- private
2598
-
2599
- def find_reservation
2600
- @reservation ||= Reservation.find(context.reservation_id)
2601
- end
2602
-
2603
- def notify_guest
2604
- GuestNotifier.call(context.guest, result)
2605
- end
2606
-
2607
- def update_availability
2608
- AvailabilityService.update(context.room_ids, result)
2609
- end
2610
- end
2611
- ```
2612
-
2613
- ### Proc or Lambda
2614
-
2615
- Use anonymous functions for inline callback logic:
2616
-
2617
- ```ruby
2618
- class ProcessBooking < CMDx::Task
2619
- # Proc
2620
- on_interrupted proc { ReservationSystem.pause! }
2621
-
2622
- # Lambda
2623
- on_complete -> { ReservationSystem.resume! }
2624
- end
2625
- ```
2626
-
2627
- ### Class or Module
2628
-
2629
- Implement reusable callback logic in dedicated classes:
2630
-
2631
- ```ruby
2632
- class BookingConfirmationCallback
2633
- def call(task)
2634
- if task.result.success?
2635
- MessagingApi.send_confirmation(task.context.guest)
2636
- else
2637
- MessagingApi.send_issue_alert(task.context.manager)
2638
- end
2639
- end
2640
- end
2641
-
2642
- class ProcessBooking < CMDx::Task
2643
- # Class or Module
2644
- on_success BookingConfirmationCallback
2645
-
2646
- # Instance
2647
- on_interrupted BookingConfirmationCallback.new
2648
- end
2649
- ```
2650
-
2651
- ### Conditional Execution
2652
-
2653
- Control callback execution with conditional logic:
2654
-
2655
- ```ruby
2656
- class MessagingPermissionCheck
2657
- def call(task)
2658
- task.context.guest.can?(:receive_messages)
2659
- end
2660
- end
2661
-
2662
- class ProcessBooking < CMDx::Task
2663
- # If and/or Unless
2664
- before_execution :notify_guest, if: :messaging_enabled?, unless: :messaging_blocked?
2665
-
2666
- # Proc
2667
- on_failure :increment_failure, if: -> { Rails.env.production? && self.class.name.include?("Legacy") }
2668
-
2669
- # Lambda
2670
- on_success :ping_housekeeping, if: proc { context.rooms_need_cleaning? }
2671
-
2672
- # Class or Module
2673
- on_complete :send_confirmation, unless: MessagingPermissionCheck
2674
-
2675
- # Instance
2676
- on_complete :send_confirmation, if: MessagingPermissionCheck.new
2677
-
2678
- def work
2679
- # Your logic here...
2680
- end
2681
-
2682
- private
2683
-
2684
- def messaging_enabled?
2685
- context.guest.messaging_preference == true
2686
- end
2687
-
2688
- def messaging_blocked?
2689
- context.guest.communication_status == :blocked
2690
- end
2691
- end
2692
- ```
2693
-
2694
- ## Callback Removal
2695
-
2696
- Remove callbacks at runtime for dynamic behavior control:
2697
-
2698
- > [!IMPORTANT]
2699
- > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
2700
-
2701
- ```ruby
2702
- class ProcessBooking < CMDx::Task
2703
- # Symbol
2704
- deregister :callback, :before_execution, :notify_guest
2705
-
2706
- # Class or Module (no instances)
2707
- deregister :callback, :on_complete, BookingConfirmationCallback
2708
- end
2709
- ```
2710
-
2711
- ---
2712
-
2713
- url: https://github.com/drexed/cmdx/blob/main/docs/middlewares.md
2714
- ---
2715
-
2716
- # Middlewares
2717
-
2718
- Middleware provides Rack-style wrappers around task execution for cross-cutting concerns like authentication, logging, caching, and error handling.
2719
-
2720
- Check out the [Getting Started](https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#middlewares) docs for global configuration.
2721
-
2722
- ## Order
2723
-
2724
- Middleware executes in a nested fashion, creating an onion-like execution pattern:
2725
-
2726
- > [!NOTE]
2727
- > Middleware executes in the order they are registered, with the first registered middleware being the outermost wrapper.
2728
-
2729
- ```ruby
2730
- class ProcessCampaign < CMDx::Task
2731
- register :middleware, AuditMiddleware # 1st: outermost wrapper
2732
- register :middleware, AuthorizationMiddleware # 2nd: middle wrapper
2733
- register :middleware, CacheMiddleware # 3rd: innermost wrapper
2734
-
2735
- def work
2736
- # Your logic here...
2737
- end
2738
- end
2739
-
2740
- # Execution flow:
2741
- # 1. AuditMiddleware (before)
2742
- # 2. AuthorizationMiddleware (before)
2743
- # 3. CacheMiddleware (before)
2744
- # 4. [task execution]
2745
- # 5. CacheMiddleware (after)
2746
- # 6. AuthorizationMiddleware (after)
2747
- # 7. AuditMiddleware (after)
2748
- ```
2749
-
2750
- ## Declarations
2751
-
2752
- ### Proc or Lambda
2753
-
2754
- Use anonymous functions for simple middleware logic:
2755
-
2756
- ```ruby
2757
- class ProcessCampaign < CMDx::Task
2758
- # Proc
2759
- register :middleware, proc do |task, options, &block|
2760
- result = block.call
2761
- Analytics.track(result.status)
2762
- result
2763
- end
2764
-
2765
- # Lambda
2766
- register :middleware, ->(task, options, &block) {
2767
- result = block.call
2768
- Analytics.track(result.status)
2769
- result
2770
- }
2771
- end
2772
- ```
2773
-
2774
- ### Class or Module
2775
-
2776
- For complex middleware logic, use classes or modules:
2777
-
2778
- ```ruby
2779
- class TelemetryMiddleware
2780
- def call(task, options)
2781
- result = yield
2782
- Telemetry.record(result.status)
2783
- ensure
2784
- result # Always return result
2785
- end
2786
- end
2787
-
2788
- class ProcessCampaign < CMDx::Task
2789
- # Class or Module
2790
- register :middleware, TelemetryMiddleware
2791
-
2792
- # Instance
2793
- register :middleware, TelemetryMiddleware.new
2794
-
2795
- # With options
2796
- register :middleware, MonitoringMiddleware, service_key: ENV["MONITORING_KEY"]
2797
- register :middleware, MonitoringMiddleware.new(ENV["MONITORING_KEY"])
2798
- end
2799
- ```
2800
-
2801
- ## Removals
2802
-
2803
- Class and Module based declarations can be removed at a global and task level.
2804
-
2805
- > [!WARNING]
2806
- > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls.
2807
-
2808
- ```ruby
2809
- class ProcessCampaign < CMDx::Task
2810
- # Class or Module (no instances)
2811
- deregister :middleware, TelemetryMiddleware
2812
- end
2813
- ```
2814
-
2815
- ## Built-in
2816
-
2817
- ### Timeout
2818
-
2819
- Ensures task execution doesn't exceed a specified time limit:
2820
-
2821
- ```ruby
2822
- class ProcessReport < CMDx::Task
2823
- # Default timeout: 3 seconds
2824
- register :middleware, CMDx::Middlewares::Timeout
2825
-
2826
- # Seconds (takes Numeric, Symbol, Proc, Lambda, Class, Module)
2827
- register :middleware, CMDx::Middlewares::Timeout, seconds: :max_processing_time
2828
-
2829
- # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
2830
- register :middleware, CMDx::Middlewares::Timeout, unless: -> { self.class.name.include?("Quick") }
2831
-
2832
- def work
2833
- # Your logic here...
2834
- end
2835
-
2836
- private
2837
-
2838
- def max_processing_time
2839
- Rails.env.production? ? 2 : 10
2840
- end
2841
- end
2842
-
2843
- # Slow task
2844
- result = ProcessReport.execute
2845
-
2846
- result.state #=> "interrupted"
2847
- result.status #=> "failure"
2848
- result.reason #=> "[CMDx::TimeoutError] execution exceeded 3 seconds"
2849
- result.cause #=> <CMDx::TimeoutError>
2850
- result.metadata #=> { limit: 3 }
2851
- ```
2852
-
2853
- ### Correlate
2854
-
2855
- Tags tasks with a global correlation ID for distributed tracing:
2856
-
2857
- ```ruby
2858
- class ProcessExport < CMDx::Task
2859
- # Default correlation ID generation
2860
- register :middleware, CMDx::Middlewares::Correlate
2861
-
2862
- # Seconds (takes Object, Symbol, Proc, Lambda, Class, Module)
2863
- register :middleware, CMDx::Middlewares::Correlate, id: proc { |task| task.context.session_id }
2864
-
2865
- # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
2866
- register :middleware, CMDx::Middlewares::Correlate, if: :correlation_enabled?
2867
-
2868
- def work
2869
- # Your logic here...
2870
- end
2871
-
2872
- private
2873
-
2874
- def correlation_enabled?
2875
- ENV["CORRELATION_ENABLED"] == "true"
2876
- end
2877
- end
2878
-
2879
- result = ProcessExport.execute
2880
- result.metadata #=> { correlation_id: "550e8400-e29b-41d4-a716-446655440000" }
2881
- ```
2882
-
2883
- ### Runtime
2884
-
2885
- The runtime middleware tags tasks with how long it took to execute the task.
2886
- The calculation uses a monotonic clock and the time is returned in milliseconds.
2887
-
2888
- ```ruby
2889
- class PerformanceMonitoringCheck
2890
- def call(task)
2891
- task.context.tenant.monitoring_enabled?
2892
- end
2893
- end
2894
-
2895
- class ProcessExport < CMDx::Task
2896
- # Default timeout is 3 seconds
2897
- register :middleware, CMDx::Middlewares::Runtime
2898
-
2899
- # If or Unless (takes Symbol, Proc, Lambda, Class, Module)
2900
- register :middleware, CMDx::Middlewares::Runtime, if: PerformanceMonitoringCheck
2901
- end
2902
-
2903
- result = ProcessExport.execute
2904
- result.metadata #=> { runtime: 1247 } (ms)
2905
- ```
2906
-
2907
- ---
2908
-
2909
- url: https://github.com/drexed/cmdx/blob/main/docs/logging.md
2910
- ---
2911
-
2912
- # Logging
2913
-
2914
- CMDx provides comprehensive automatic logging for task execution with structured data, customizable formatters, and intelligent severity mapping. All task results are logged after completion with rich metadata for debugging and monitoring.
2915
-
2916
- ## Formatters
2917
-
2918
- CMDx supports multiple log formatters to integrate with various logging systems:
2919
-
2920
- | Formatter | Use Case | Output Style |
2921
- |-----------|----------|--------------|
2922
- | `Line` | Traditional logging | Single-line format |
2923
- | `Json` | Structured systems | Compact JSON |
2924
- | `KeyValue` | Log parsing | `key=value` pairs |
2925
- | `Logstash` | ELK stack | JSON with @version/@timestamp |
2926
- | `Raw` | Minimal output | Message content only |
2927
-
2928
- Sample output:
2929
-
2930
- ```log
2931
- <!-- Success (INFO level) -->
2932
- I, [2022-07-17T18:43:15.000000 #3784] INFO -- GenerateInvoice:
2933
- index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="GenerateInvoice" state="complete" status="success" metadata={runtime: 187}
2934
-
2935
- <!-- Skipped (WARN level) -->
2936
- W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidateCustomer:
2937
- index=1 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="ValidateCustomer" state="interrupted" status="skipped" reason="Customer already validated"
2938
-
2939
- <!-- Failed (ERROR level) -->
2940
- E, [2022-07-17T18:43:15.000000 #3784] ERROR -- CalculateTax:
2941
- index=2 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CalculateTax" state="interrupted" status="failed" metadata={error_code: "TAX_SERVICE_UNAVAILABLE"}
2942
-
2943
- <!-- Failed Chain -->
2944
- E, [2022-07-17T18:43:15.000000 #3784] ERROR -- BillingWorkflow:
2945
- index=3 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="BillingWorkflow" state="interrupted" status="failed" caused_failure={index: 2, class: "CalculateTax", status: "failed"} threw_failure={index: 1, class: "ValidateCustomer", status: "failed"}
2946
- ```
2947
-
2948
- > [!TIP]
2949
- > Logging can be used as low-level eventing system, ingesting all tasks performed within a small action or long running request. This ie where correlation is especially handy.
2950
-
2951
- ## Structure
2952
-
2953
- All log entries include comprehensive execution metadata. Field availability depends on execution context and outcome.
2954
-
2955
- ### Core Fields
2956
-
2957
- | Field | Description | Example |
2958
- |-------|-------------|---------|
2959
- | `severity` | Log level | `INFO`, `WARN`, `ERROR` |
2960
- | `timestamp` | ISO 8601 execution time | `2022-07-17T18:43:15.000000` |
2961
- | `pid` | Process ID | `3784` |
2962
-
2963
- ### Task Information
2964
-
2965
- | Field | Description | Example |
2966
- |-------|-------------|---------|
2967
- | `index` | Execution sequence position | `0`, `1`, `2` |
2968
- | `chain_id` | Unique execution chain ID | `018c2b95-b764-7615...` |
2969
- | `type` | Execution unit type | `Task`, `Workflow` |
2970
- | `class` | Task class name | `GenerateInvoiceTask` |
2971
- | `id` | Unique task instance ID | `018c2b95-b764-7615...` |
2972
- | `tags` | Custom categorization | `["billing", "financial"]` |
2973
-
2974
- ### Execution Data
2975
-
2976
- | Field | Description | Example |
2977
- |-------|-------------|---------|
2978
- | `state` | Lifecycle state | `complete`, `interrupted` |
2979
- | `status` | Business outcome | `success`, `skipped`, `failed` |
2980
- | `outcome` | Final classification | `success`, `interrupted` |
2981
- | `metadata` | Custom task data | `{order_id: 123, amount: 99.99}` |
2982
-
2983
- ### Failure Chain
2984
-
2985
- | Field | Description |
2986
- |-------|-------------|
2987
- | `reason` | Reason given for the stoppage |
2988
- | `caused` | Cause exception details |
2989
- | `caused_failure` | Original failing task details |
2990
- | `threw_failure` | Task that propagated the failure |
2991
-
2992
- ## Usage
2993
-
2994
- Tasks have access to the frameworks logger.
2995
-
2996
- ```ruby
2997
- class ProcessSubscription < CMDx::Task
2998
- def work
2999
- logger.debug { "Activated feature flags: #{Features.active_flags}" }
3000
- # Your logic here...
3001
- logger.info("Subscription processed")
3002
- end
3003
- end
3004
- ```
3005
-
3006
- ---
3007
-
3008
- url: https://github.com/drexed/cmdx/blob/main/docs/internationalization.md
3009
- ---
3010
-
3011
- # Internationalization (i18n)
3012
-
3013
- CMDx provides comprehensive internationalization support for all error messages, attribute validation failures, coercion errors, and fault messages. All user-facing text is automatically localized based on the current `I18n.locale`, ensuring your applications can serve global audiences with native-language error reporting.
3014
-
3015
- ## Localization
3016
-
3017
- CMDx automatically localizes all error messages based on the `I18n.locale` setting.
3018
-
3019
- ```ruby
3020
- class ProcessQuote < CMDx::Task
3021
- attribute :price, type: :float
3022
-
3023
- def work
3024
- # Your logic here...
3025
- end
3026
- end
3027
-
3028
- I18n.with_locale(:fr) do
3029
- result = ProcessQuote.execute(price: "invalid")
3030
- result.metadata[:messages][:price] #=> ["impossible de contraindre en float"]
3031
- end
3032
- ```
3033
-
3034
- ## Configuration
3035
-
3036
- Localization is handled by the `I18n` gem. In Rails applications, locales are loaded automatically and managed via the `I18n.available_locales` setting.
3037
-
3038
- ### Local Copies
3039
-
3040
- Execute the following command to copy any locale into the Rails applications `config/locales` directory:
3041
-
3042
- ```bash
3043
- rails generate cmdx:locale [LOCALE]
3044
-
3045
- # Eg: generate french locale
3046
- rails generate cmdx:locale fr
3047
- ```
3048
-
3049
- ### Available Locales
3050
-
3051
- - af - Afrikaans
3052
- - ar - Arabic
3053
- - az - Azerbaijani
3054
- - be - Belarusian
3055
- - bg - Bulgarian
3056
- - bn - Bengali
3057
- - bs - Bosnian
3058
- - ca - Catalan
3059
- - cnr - Montenegrin
3060
- - cs - Czech
3061
- - cy - Welsh
3062
- - da - Danish
3063
- - de - German
3064
- - dz - Dzongkha
3065
- - el - Greek
3066
- - en - English
3067
- - eo - Esperanto
3068
- - es - Spanish
3069
- - et - Estonian
3070
- - eu - Basque
3071
- - fa - Persian
3072
- - fi - Finnish
3073
- - fr - French
3074
- - fy - Western Frisian
3075
- - gd - Scottish Gaelic
3076
- - gl - Galician
3077
- - he - Hebrew
3078
- - hi - Hindi
3079
- - hr - Croatian
3080
- - hu - Hungarian
3081
- - hy - Armenian
3082
- - id - Indonesian
3083
- - is - Icelandic
3084
- - it - Italian
3085
- - ja - Japanese
3086
- - ka - Georgian
3087
- - kk - Kazakh
3088
- - km - Khmer
3089
- - kn - Kannada
3090
- - ko - Korean
3091
- - lb - Luxembourgish
3092
- - lo - Lao
3093
- - lt - Lithuanian
3094
- - lv - Latvian
3095
- - mg - Malagasy
3096
- - mk - Macedonian
3097
- - ml - Malayalam
3098
- - mn - Mongolian
3099
- - mr-IN - Marathi (India)
3100
- - ms - Malay
3101
- - nb - Norwegian Bokmål
3102
- - ne - Nepali
3103
- - nl - Dutch
3104
- - nn - Norwegian Nynorsk
3105
- - oc - Occitan
3106
- - or - Odia
3107
- - pa - Punjabi
3108
- - pl - Polish
3109
- - pt - Portuguese
3110
- - rm - Romansh
3111
- - ro - Romanian
3112
- - ru - Russian
3113
- - sc - Sardinian
3114
- - sk - Slovak
3115
- - sl - Slovenian
3116
- - sq - Albanian
3117
- - sr - Serbian
3118
- - st - Southern Sotho
3119
- - sv - Swedish
3120
- - sw - Swahili
3121
- - ta - Tamil
3122
- - te - Telugu
3123
- - th - Thai
3124
- - tl - Tagalog
3125
- - tr - Turkish
3126
- - tt - Tatar
3127
- - ug - Uyghur
3128
- - uk - Ukrainian
3129
- - ur - Urdu
3130
- - uz - Uzbek
3131
- - vi - Vietnamese
3132
- - wo - Wolof
3133
- - zh-CN - Chinese (Simplified)
3134
- - zh-HK - Chinese (Hong Kong)
3135
- - zh-TW - Chinese (Traditional)
3136
- - zh-YUE - Chinese (Yue)
3137
-
3138
- ---
3139
-
3140
- url: https://github.com/drexed/cmdx/blob/main/docs/deprecation.md
3141
- ---
3142
-
3143
- # Task Deprecation
3144
-
3145
- Task deprecation provides a systematic approach to managing legacy tasks in CMDx applications. The deprecation system enables controlled migration paths by issuing warnings, logging messages, or preventing execution of deprecated tasks entirely, helping teams maintain code quality while providing clear upgrade paths.
3146
-
3147
- ## Modes
3148
-
3149
- ### Raise
3150
-
3151
- `:raise` mode prevents task execution entirely. Use this for tasks that should no longer be used under any circumstances.
3152
-
3153
- > [!WARNING]
3154
- > Use `:raise` mode carefully in production environments as it will break existing workflows immediately.
3155
-
3156
- ```ruby
3157
- class ProcessObsoleteAPI < CMDx::Task
3158
- settings(deprecated: :raise)
3159
-
3160
- def work
3161
- # Will never execute...
3162
- end
3163
- end
3164
-
3165
- result = ProcessObsoleteAPI.execute
3166
- #=> raises CMDx::DeprecationError: "ProcessObsoleteAPI usage prohibited"
3167
- ```
3168
-
3169
- ### Log
3170
-
3171
- `:log` mode allows continued usage while tracking deprecation warnings. Perfect for gradual migration scenarios where immediate replacement isn't feasible.
3172
-
3173
- ```ruby
3174
- class ProcessLegacyFormat < CMDx::Task
3175
- settings(deprecated: :log)
3176
-
3177
- # Same
3178
- settings(deprecated: true)
3179
-
3180
- def work
3181
- # Executes but logs deprecation warning...
3182
- end
3183
- end
3184
-
3185
- result = ProcessLegacyFormat.execute
3186
- result.successful? #=> true
3187
-
3188
- # Deprecation warning appears in logs:
3189
- # WARN -- : DEPRECATED: ProcessLegacyFormat - migrate to replacement or discontinue use
3190
- ```
3191
-
3192
- ### Warn
3193
-
3194
- `:warn` mode issues Ruby warnings visible in development and testing environments. Useful for alerting developers without affecting production logging.
3195
-
3196
- ```ruby
3197
- class ProcessOldData < CMDx::Task
3198
- settings(deprecated: :warn)
3199
-
3200
- def work
3201
- # Executes but emits Ruby warning...
3202
- end
3203
- end
3204
-
3205
- result = ProcessOldData.execute
3206
- result.successful? #=> true
3207
-
3208
- # Ruby warning appears in stderr:
3209
- # [ProcessOldData] DEPRECATED: migrate to a replacement or discontinue use
3210
- ```
3211
-
3212
- ## Declarations
3213
-
3214
- ### Symbol or String
3215
-
3216
- ```ruby
3217
- class OutdatedConnector < CMDx::Task
3218
- # Symbol
3219
- settings(deprecated: :raise)
3220
-
3221
- # String
3222
- settings(deprecated: "warn")
3223
- end
3224
- ```
3225
-
3226
- ### Boolean or Nil
3227
-
3228
- ```ruby
3229
- class OutdatedConnector < CMDx::Task
3230
- # Deprecates with default :log mode
3231
- settings(deprecated: true)
3232
-
3233
- # Skips deprecation
3234
- settings(deprecated: false)
3235
- settings(deprecated: nil)
3236
- end
3237
- ```
3238
-
3239
- ### Method
3240
-
3241
- ```ruby
3242
- class OutdatedConnector < CMDx::Task
3243
- # Symbol
3244
- settings(deprecated: :deprecated?)
3245
-
3246
- def work
3247
- # Your logic here...
3248
- end
3249
-
3250
- private
3251
-
3252
- def deprecated?
3253
- Time.now.year > 2024 ? :raise : false
3254
- end
3255
- end
3256
- ```
3257
-
3258
- ### Proc or Lambda
3259
-
3260
- ```ruby
3261
- class OutdatedConnector < CMDx::Task
3262
- # Proc
3263
- settings(deprecated: proc { Rails.env.development? ? :raise : :log })
3264
-
3265
- # Lambda
3266
- settings(deprecated: -> { Current.tenant.legacy_mode? ? :warn : :raise })
3267
- end
3268
- ```
3269
-
3270
- ### Class or Module
3271
-
3272
- ```ruby
3273
- class OutdatedTaskDeprecator
3274
- def call(task)
3275
- task.class.name.include?("Outdated")
3276
- end
3277
- end
3278
-
3279
- class OutdatedConnector < CMDx::Task
3280
- # Class or Module
3281
- settings(deprecated: OutdatedTaskDeprecator)
3282
-
3283
- # Instance
3284
- settings(deprecated: OutdatedTaskDeprecator.new)
3285
- end
3286
- ```
3287
-
3288
- ---
3289
-
3290
- url: https://github.com/drexed/cmdx/blob/main/docs/workflows.md
3291
- ---
3292
-
3293
- # Workflows
3294
-
3295
- 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.
3296
-
3297
- ## Declarations
3298
-
3299
- Tasks execute sequentially in declaration order (FIFO). The workflow context propagates to each task, allowing access to data from previous executions.
3300
-
3301
- > [!IMPORTANT]
3302
- > Do **NOT** define a `work` method in workflow tasks. The included module automatically provides the execution logic.
3303
-
3304
- ### Task
3305
-
3306
- ```ruby
3307
- class OnboardingWorkflow < CMDx::Task
3308
- include CMDx::Workflow
3309
-
3310
- task CreateUserProfile
3311
- task SetupAccountPreferences
3312
-
3313
- tasks SendWelcomeEmail, SendWelcomeSms, CreateDashboard
3314
- end
3315
- ```
3316
-
3317
- > [!TIP]
3318
- > Execute tasks in parallel via the [cmdx-parallel](https://github.com/drexed/cmdx-parallel) gem.
3319
-
3320
- ### Group
3321
-
3322
- Group related tasks for better organization and shared configuration:
3323
-
3324
- > [!IMPORTANT]
3325
- > Settings and conditionals for a group apply to all tasks within that group.
3326
-
3327
- ```ruby
3328
- class ContentModerationWorkflow < CMDx::Task
3329
- include CMDx::Workflow
3330
-
3331
- # Screening phase
3332
- tasks ScanForProfanity, CheckForSpam, ValidateImages, breakpoints: ["skipped"]
3333
-
3334
- # Review phase
3335
- tasks ApplyFilters, ScoreContent, FlagSuspicious
3336
-
3337
- # Decision phase
3338
- tasks PublishContent, QueueForReview, NotifyModerators
3339
- end
3340
- ```
3341
-
3342
- ### Conditionals
3343
-
3344
- Conditionals support multiple syntaxes for flexible execution control:
3345
-
3346
- ```ruby
3347
- class ContentAccessCheck
3348
- def call(task)
3349
- task.context.user.can?(:publish_content)
3350
- end
3351
- end
3352
-
3353
- class OnboardingWorkflow < CMDx::Task
3354
- include CMDx::Workflow
3355
-
3356
- # If and/or Unless
3357
- task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
3358
-
3359
- # Proc
3360
- task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
3361
-
3362
- # Lambda
3363
- task SendWelcomeEmail, if: proc { context.features_enabled? }
3364
-
3365
- # Class or Module
3366
- task SendWelcomeEmail, unless: ContentAccessCheck
3367
-
3368
- # Instance
3369
- task SendWelcomeEmail, if: ContentAccessCheck.new
3370
-
3371
- # Conditional applies to all tasks of this declaration group
3372
- tasks SendWelcomeEmail, CreateDashboard, SetupTutorial, if: :email_configured?
3373
-
3374
- private
3375
-
3376
- def email_configured?
3377
- context.user.email_address == true
3378
- end
3379
-
3380
- def email_disabled?
3381
- context.user.communication_preference == :disabled
3382
- end
3383
- end
3384
- ```
3385
-
3386
- ## Halt Behavior
3387
-
3388
- By default skipped tasks are considered no-op executions and does not stop workflow execution.
3389
- This is configurable via global and task level breakpoint settings. Task and group configurations
3390
- can be used together within a workflow.
3391
-
3392
- ```ruby
3393
- class AnalyticsWorkflow < CMDx::Task
3394
- include CMDx::Workflow
3395
-
3396
- task CollectMetrics # If fails → workflow stops
3397
- task FilterOutliers # If skipped → workflow continues
3398
- task GenerateDashboard # Only runs if no failures occurred
3399
- end
3400
- ```
3401
-
3402
- ### Task Configuration
3403
-
3404
- Configure halt behavior for the entire workflow:
3405
-
3406
- ```ruby
3407
- class SecurityWorkflow < CMDx::Task
3408
- include CMDx::Workflow
3409
-
3410
- # Halt on both failed and skipped results
3411
- settings(workflow_breakpoints: ["skipped", "failed"])
3412
-
3413
- task PerformSecurityScan
3414
- task ValidateSecurityRules
3415
- end
3416
-
3417
- class OptionalTasksWorkflow < CMDx::Task
3418
- include CMDx::Workflow
3419
-
3420
- # Never halt, always continue
3421
- settings(breakpoints: [])
3422
-
3423
- task TryBackupData
3424
- task TryCleanupLogs
3425
- task TryOptimizeCache
3426
- end
3427
- ```
3428
-
3429
- ### Group Configuration
3430
-
3431
- Different task groups can have different halt behavior:
3432
-
3433
- ```ruby
3434
- class SubscriptionWorkflow < CMDx::Task
3435
- include CMDx::Workflow
3436
-
3437
- task CreateSubscription, ValidatePayment, workflow_breakpoints: ["skipped", "failed"]
3438
-
3439
- # Never halt, always continue
3440
- task SendConfirmationEmail, UpdateBilling, breakpoints: []
3441
- end
3442
- ```
3443
-
3444
- ## Nested Workflows
3445
-
3446
- Workflows can task other workflows for hierarchical composition:
3447
-
3448
- ```ruby
3449
- class EmailPreparationWorkflow < CMDx::Task
3450
- include CMDx::Workflow
3451
-
3452
- task ValidateRecipients
3453
- task CompileTemplate
3454
- end
3455
-
3456
- class EmailDeliveryWorkflow < CMDx::Task
3457
- include CMDx::Workflow
3458
-
3459
- tasks SendEmails, TrackDeliveries
3460
- end
3461
-
3462
- class CompleteEmailWorkflow < CMDx::Task
3463
- include CMDx::Workflow
3464
-
3465
- task EmailPreparationWorkflow
3466
- task EmailDeliveryWorkflow, if: proc { context.preparation_successful? }
3467
- task GenerateDeliveryReport
3468
- end
3469
- ```
3470
-
3471
- ## Parallel Execution
3472
-
3473
- 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.
3474
-
3475
- > [!IMPORTANT]
3476
- > Context cannot be modified during parallel execution. Ensure that all required data is preloaded into the context before parallelization begins.
3477
-
3478
- ```ruby
3479
- class SendWelcomeNotifications < CMDx::Task
3480
- include CMDx::Workflow
3481
-
3482
- # Default options (dynamically calculated to available processors)
3483
- tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel
3484
-
3485
- # Fix number of threads
3486
- tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_threads: 2
3487
-
3488
- # Fix number of forked processes
3489
- tasks SendWelcomeEmail, SendWelcomeSms, SendWelcomePush, strategy: :parallel, in_processes: 2
3490
-
3491
- # NOTE: Reactors are not supported
3492
- end
3493
- ```
3494
-
3495
- ## Task Generator
3496
-
3497
- Generate new CMDx workflow tasks quickly using the built-in generator:
3498
-
3499
- ```bash
3500
- rails generate cmdx:workflow SendNotifications
3501
- ```
3502
-
3503
- This creates a new workflow task file with the basic structure:
3504
-
3505
- ```ruby
3506
- # app/tasks/send_notifications.rb
3507
- class SendNotifications < CMDx::Task
3508
- include CMDx::Workflow
3509
-
3510
- tasks Task1, Task2
3511
- end
3512
- ```
3513
-
3514
- > [!TIP]
3515
- > Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
3516
-
3517
- ---
3518
-
3519
- url: https://github.com/drexed/cmdx/blob/main/docs/tips_and_tricks.md
3520
- ---
3521
-
3522
- # Tips and Tricks
3523
-
3524
- This guide covers advanced patterns and optimization techniques for getting the most out of CMDx in production applications.
3525
-
3526
- ## Project Organization
3527
-
3528
- ### Directory Structure
3529
-
3530
- Create a well-organized command structure for maintainable applications:
3531
-
3532
- ```text
3533
- /app/
3534
- └── /tasks/
3535
- ├── /invoices/
3536
- │ ├── calculate_tax.rb
3537
- │ ├── validate_invoice.rb
3538
- │ ├── send_invoice.rb
3539
- │ └── process_invoice.rb # workflow
3540
- ├── /reports/
3541
- │ ├── generate_pdf.rb
3542
- │ ├── compile_data.rb
3543
- │ ├── export_csv.rb
3544
- │ └── create_reports.rb # workflow
3545
- ├── application_task.rb # base class
3546
- ├── authenticate_session.rb
3547
- └── activate_account.rb
3548
- ```
3549
-
3550
- ### Naming Conventions
3551
-
3552
- Follow consistent naming patterns for clarity and maintainability:
3553
-
3554
- ```ruby
3555
- # Verb + Noun
3556
- class ExportData < CMDx::Task; end
3557
- class CompressFile < CMDx::Task; end
3558
- class ValidateSchema < CMDx::Task; end
3559
-
3560
- # Use present tense verbs for actions
3561
- class GenerateToken < CMDx::Task; end # ✓ Good
3562
- class GeneratingToken < CMDx::Task; end # ❌ Avoid
3563
- class TokenGeneration < CMDx::Task; end # ❌ Avoid
3564
- ```
3565
-
3566
- ### Story Telling
3567
-
3568
- Consider using descriptive methods to express the task’s flow, rather than concentrating all logic inside the `work` method.
3569
-
3570
- ```ruby
3571
- class ProcessOrder < CMDx::Task
3572
- def work
3573
- charge_payment_method
3574
- assign_to_warehouse
3575
- send_notification
3576
- end
3577
-
3578
- private
3579
-
3580
- def charge_payment_method
3581
- order.primary_payment_method.charge!
3582
- end
3583
-
3584
- def assign_to_warehouse
3585
- order.ready_for_shipping!
3586
- end
3587
-
3588
- def send_notification
3589
- if order.products_out_of_stock?
3590
- OrderMailer.pending(order).deliver
3591
- else
3592
- OrderMailer.preparing(order).deliver
3593
- end
3594
- end
3595
- end
3596
- ```
3597
-
3598
- ### Style Guide
3599
-
3600
- Follow a style pattern for consistent task design:
3601
-
3602
- ```ruby
3603
- class ExportReport < CMDx::Task
3604
-
3605
- # 1. Register functions
3606
- register :middleware, CMDx::Middlewares::Correlate
3607
- register :validator, :format, FormatValidator
3608
-
3609
- # 2. Define callbacks
3610
- before_execution :find_report
3611
- on_complete :track_export_metrics, if: ->(task) { Current.tenant.analytics? }
3612
-
3613
- # 3. Declare attributes
3614
- attributes :user_id
3615
- required :report_id
3616
- optional :format_type
3617
-
3618
- # 4. Define work method
3619
- def work
3620
- report.compile!
3621
- report.export!
3622
-
3623
- context.exported_at = Time.now
3624
- end
3625
-
3626
- # TIP: Favor private business logic to reduce the surface of the public API.
3627
- private
3628
-
3629
- # 5. Build helper functions
3630
- def find_report
3631
- @report ||= Report.find(report_id)
3632
- end
3633
-
3634
- def track_export_metrics
3635
- Analytics.increment(:report_exported)
3636
- end
3637
-
3638
- end
3639
- ```
3640
-
3641
- ## Attribute Options
3642
-
3643
- Use Rails `with_options` to reduce duplication and improve readability:
3644
-
3645
- ```ruby
3646
- class ConfigureCompany < CMDx::Task
3647
- # Apply common options to multiple attributes
3648
- with_options(type: :string, presence: true) do
3649
- attributes :website, format: { with: URI::DEFAULT_PARSER.make_regexp(%w[http https]) }
3650
- required :company_name, :industry
3651
- optional :description, format: { with: /\A[\w\s\-\.,!?]+\z/ }
3652
- end
3653
-
3654
- # Nested attributes with shared prefix
3655
- required :headquarters do
3656
- with_options(prefix: :hq_) do
3657
- attributes :street, :city, :zip_code, type: :string
3658
- required :country, type: :string, inclusion: { in: VALID_COUNTRIES }
3659
- optional :region, type: :string
3660
- end
3661
- end
3662
-
3663
- def work
3664
- # Your logic here...
3665
- end
3666
- end
3667
- ```
3668
-
3669
- ## Advance Examples
3670
-
3671
- - [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
3672
- - [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
3673
-
3674
- ---