From fb2f03c9712ff2f7f303e8b939e5e9f88efed582 Mon Sep 17 00:00:00 2001 From: Juan Gomez Date: Wed, 20 Aug 2025 18:18:39 -0400 Subject: [PATCH 01/12] Improve docs --- docs/attributes/coercions.md | 6 +++--- docs/attributes/validations.md | 8 ++++---- docs/basics/chain.md | 18 +++++++++--------- docs/basics/setup.md | 6 +++--- docs/callbacks.md | 13 ++++++++----- docs/deprecation.md | 6 +++--- docs/internationalization.md | 1 - docs/interruptions/faults.md | 8 ++++---- docs/interruptions/halt.md | 6 +++--- docs/logging.md | 10 +++++----- docs/middlewares.md | 6 +++--- docs/outcomes/result.md | 8 ++++---- docs/outcomes/states.md | 2 +- docs/tips_and_tricks.md | 32 ++++++++++++++++---------------- docs/workflows.md | 8 ++++---- 15 files changed, 70 insertions(+), 68 deletions(-) diff --git a/docs/attributes/coercions.md b/docs/attributes/coercions.md index 0709da9e2..f1ecbc5ef 100644 --- a/docs/attributes/coercions.md +++ b/docs/attributes/coercions.md @@ -117,15 +117,15 @@ end Remove custom coercions when no longer needed: +> [!IMPORTANT] +> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. + ```ruby class ProcessOrder < CMDx::Task deregister :coercion, :point end ``` -> [!IMPORTANT] -> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. - ## Error Handling Coercion failures provide detailed error information including attribute paths, attempted types, and specific failure reasons: diff --git a/docs/attributes/validations.md b/docs/attributes/validations.md index 28121a113..93ca8e0e3 100644 --- a/docs/attributes/validations.md +++ b/docs/attributes/validations.md @@ -210,7 +210,7 @@ end ## Declarations > [!IMPORTANT] -> Custom validators must raise a CMDx::ValidationError and its message is used as part of the fault reason and metadata. +> Custom validators must raise a `CMDx::ValidationError` and its message is used as part of the fault reason and metadata. ### Proc or Lambda @@ -258,15 +258,15 @@ end Remove custom validators when no longer needed: +> [!IMPORTANT] +> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. + ```ruby class CreateWebsite < CMDx::Task deregister :validator, :domain end ``` -> [!IMPORTANT] -> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. - ## Error Handling Validation failures provide detailed error information including attribute paths, validation rules, and specific failure reasons: diff --git a/docs/basics/chain.md b/docs/basics/chain.md index f8b29e2e6..5d4caf1f3 100644 --- a/docs/basics/chain.md +++ b/docs/basics/chain.md @@ -13,6 +13,9 @@ Chains automatically group related task executions within a thread, providing un Each thread maintains its own chain context through thread-local storage, providing automatic isolation without manual coordination. +> [!IMPORTANT] +> Chain operations are thread-local. Never share chain references across threads as this can lead to race conditions and data corruption. + ```ruby # Thread A Thread.new do @@ -31,13 +34,13 @@ CMDx::Chain.current #=> Returns current chain or nil CMDx::Chain.clear #=> Clears current thread's chain ``` -> [!IMPORTANT] -> Chain operations are thread-local. Never share chain references across threads as this can lead to race conditions and data corruption. - ## Links Every task execution automatically creates or joins the current thread's chain: +> [!NOTE] +> Chain creation is automatic and transparent. You don't need to manually manage chain lifecycle. + ```ruby class ProcessOrder < CMDx::Task def work @@ -57,9 +60,6 @@ class ProcessOrder < CMDx::Task end ``` -> [!NOTE] -> Chain creation is automatic and transparent. You don't need to manually manage chain lifecycle. - ## Inheritance When tasks call subtasks within the same thread, all executions automatically inherit the current chain, creating a unified execution trail. @@ -89,6 +89,9 @@ chain.results.map { |r| r.task.class } Chains provide comprehensive execution information with state delegation: +> [!NOTE] +> Chain state always reflects the first (outer-most) task result, not individual subtask outcomes. Subtasks maintain their own success/failure states. + ```ruby result = ProcessOrder.execute(order_id: 123) chain = result.chain @@ -108,9 +111,6 @@ chain.results.each_with_index do |result, index| end ``` -> [!NOTE] -> Chain state always reflects the first (outer-most) task result, not individual subtask outcomes. Subtasks maintain their own success/failure states. - --- - **Prev:** [Basics - Context](context.md) diff --git a/docs/basics/setup.md b/docs/basics/setup.md index f78e7f7f4..bea351dd1 100644 --- a/docs/basics/setup.md +++ b/docs/basics/setup.md @@ -61,6 +61,9 @@ end Tasks follow a predictable call pattern with specific states and statuses: +> [!WARNING] +> Tasks are single-use objects. Once executed, they are frozen and cannot be executed again. + | Stage | State | Status | Description | |-------|-------|--------|-------------| | **Instantiation** | `initialized` | `success` | Task created with context | @@ -69,9 +72,6 @@ Tasks follow a predictable call pattern with specific states and statuses: | **Completion** | `executed` | `success`/`failed`/`skipped` | Result finalized | | **Freezing** | `executed` | `success`/`failed`/`skipped` | Task becomes immutable | -> [!WARNING] -> Tasks are single-use objects. Once executed, they are frozen and cannot be executed again. - --- - **Prev:** [Getting Started](../getting_started.md) diff --git a/docs/callbacks.md b/docs/callbacks.md index ad3875cb8..789af1db8 100644 --- a/docs/callbacks.md +++ b/docs/callbacks.md @@ -2,7 +2,8 @@ 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. -> **Note:** 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). +> [!IMPORTANT] +> 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). ## Table of Contents @@ -21,7 +22,9 @@ Callbacks execute in precise lifecycle order. Here is the complete execution seq ```ruby 1. before_validation # Pre-validation setup 2. before_execution # Setup and preparation -# Task work executed + +# --- Task#work executed --- + 3. on_[complete|interrupted] # Based on execution state 4. on_executed # Task finished (any outcome) 5. on_[success|skipped|failed] # Based on execution status @@ -146,6 +149,9 @@ end Remove callbacks at runtime for dynamic behavior control: +> [!IMPORTANT] +> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. + ```ruby class ProcessOrder < CMDx::Task # Symbol @@ -156,9 +162,6 @@ class ProcessOrder < CMDx::Task end ``` -> [!IMPORTANT] -> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. - --- - **Prev:** [Attributes - Defaults](attributes/defaults.md) diff --git a/docs/deprecation.md b/docs/deprecation.md index 402bc8acf..8f8d3a58d 100644 --- a/docs/deprecation.md +++ b/docs/deprecation.md @@ -21,6 +21,9 @@ Task deprecation provides a systematic approach to managing legacy tasks in CMDx `:raise` mode prevents task execution entirely. Use this for tasks that should no longer be used under any circumstances. +> [!WARNING] +> Use `:raise` mode carefully in production environments as it will break existing workflows immediately. + ```ruby class ProcessLegacyPayment < CMDx::Task settings(deprecated: :raise) @@ -34,9 +37,6 @@ result = ProcessLegacyPayment.execute #=> raises CMDx::DeprecationError: "ProcessLegacyPayment usage prohibited" ``` -> [!WARNING] -> Use `:raise` mode carefully in production environments as it will break existing workflows immediately. - ### Log `:log` mode allows continued usage while tracking deprecation warnings. Perfect for gradual migration scenarios where immediate replacement isn't feasible. diff --git a/docs/internationalization.md b/docs/internationalization.md index 67d828dbc..67dee3875 100644 --- a/docs/internationalization.md +++ b/docs/internationalization.md @@ -5,7 +5,6 @@ CMDx provides comprehensive internationalization support for all error messages, ## Table of Contents - [Localization](#localization) -- [I18n](#i18n) ## Localization diff --git a/docs/interruptions/faults.md b/docs/interruptions/faults.md index f03fa0b1b..efb9702c0 100644 --- a/docs/interruptions/faults.md +++ b/docs/interruptions/faults.md @@ -111,15 +111,15 @@ Use `throw!` to propagate failures while preserving fault context and maintainin ```ruby class OrderProcessor < CMDx::Task def work - # Validate order + # Throw if skipped or failed validation_result = OrderValidator.execute(context) - throw!(validation_result) # Skipped or Failed + throw!(validation_result) - # Check inventory + # Only throw if skipped check_inventory = CheckInventory.execute(context) throw!(check_inventory) if check_inventory.skipped? - # Process payment + # Only throw if failed payment_result = PaymentProcessor.execute(context) throw!(payment_result) if payment_result.failed? diff --git a/docs/interruptions/halt.md b/docs/interruptions/halt.md index 835bd6f71..f35cee267 100644 --- a/docs/interruptions/halt.md +++ b/docs/interruptions/halt.md @@ -17,6 +17,9 @@ Halting stops task execution with explicit intent signaling. Tasks provide two p The `skip!` method indicates a task did not meet criteria to continue execution. This represents a controlled, intentional interruption where the task determines that execution is not necessary or appropriate. +> [!NOTE] +> Skipping is not a failure or error. Skipped tasks are considered successful outcomes. + ```ruby class ProcessOrder < CMDx::Task def work @@ -48,9 +51,6 @@ result.reason #=> "no reason given" result.reason #=> "Outside business hours" ``` -> [!NOTE] -> Skipping is not a failure or error. Skipped tasks are considered successful outcomes. - ## Failing The `fail!` method indicates a task encountered an error condition that prevents successful completion. This represents controlled failure where the task explicitly determines that execution cannot continue. diff --git a/docs/logging.md b/docs/logging.md index b7afb078f..f6d30fa7c 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -22,21 +22,21 @@ CMDx supports multiple log formatters to integrate with various logging systems: Sample output: -```text -# Success (INFO level) +```log + I, [2022-07-17T18:43:15.000000 #3784] INFO -- CreateOrder: index=0 chain_id="018c2b95-b764-7615-a924-cc5b910ed1e5" type="Task" class="CreateOrder" state="complete" status="success" metadata={runtime: 123} -# Skipped (WARN level) + W, [2022-07-17T18:43:15.000000 #3784] WARN -- ValidatePayment: index=1 state="interrupted" status="skipped" reason="Order already processed" -# Failed (ERROR level) + E, [2022-07-17T18:43:15.000000 #3784] ERROR -- ProcessPayment: index=2 state="interrupted" status="failed" metadata={error_code: "INSUFFICIENT_FUNDS"} -# Failed Chain + E, [2022-07-17T18:43:15.000000 #3784] ERROR -- OrderWorkflow: caused_failure={index: 2, class: "ProcessPayment", status: "failed"} threw_failure={index: 1, class: "ValidatePayment", status: "failed"} diff --git a/docs/middlewares.md b/docs/middlewares.md index c94a7a910..ffc29be24 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -97,6 +97,9 @@ end Class and Module based declarations can be removed at a global and task level. +> [!IMPORTANT] +> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. + ```ruby class ProcessOrder < CMDx::Task # Class or Module (no instances) @@ -104,9 +107,6 @@ class ProcessOrder < CMDx::Task end ``` -> [!IMPORTANT] -> Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. - ## Built-in ### Timeout diff --git a/docs/outcomes/result.md b/docs/outcomes/result.md index 4e1c70b7b..3f63a59ec 100644 --- a/docs/outcomes/result.md +++ b/docs/outcomes/result.md @@ -17,11 +17,11 @@ The result object is the comprehensive return value of task execution, providing ## Result Attributes +Every result provides access to essential execution information: + > [!NOTE] > Result objects are immutable after task execution completes and reflect the final state. -Every result provides access to essential execution information: - ```ruby result = ProcessOrder.execute(order_id: 123) @@ -139,11 +139,11 @@ result ## Pattern Matching +Results support Ruby's pattern matching through array and hash deconstruction: + > [!NOTE] > Pattern matching requires Ruby 3.0+. The `deconstruct` method returns a `[state, status]` array pattern, while `deconstruct_keys` provides hash access to result attributes. -Results support Ruby's pattern matching through array and hash deconstruction: - ### Array Pattern ```ruby diff --git a/docs/outcomes/states.md b/docs/outcomes/states.md index e940625d6..a7f66c45c 100644 --- a/docs/outcomes/states.md +++ b/docs/outcomes/states.md @@ -34,7 +34,7 @@ State-Status combinations: ## Transitions -> [!IMPORTANT] +> [!CAUTION] > States are automatically managed during task execution and should **never** be modified manually. State transitions are handled internally by the CMDx framework. ```ruby diff --git a/docs/tips_and_tricks.md b/docs/tips_and_tricks.md index f4a757694..e79fc38f9 100644 --- a/docs/tips_and_tricks.md +++ b/docs/tips_and_tricks.md @@ -17,22 +17,22 @@ This guide covers advanced patterns and optimization techniques for getting the Create a well-organized command structure for maintainable applications: -```txt -/app - /tasks - /orders - - charge_order.rb - - validate_order.rb - - fulfill_order.rb - - process_order.rb # workflow - /notifications - - send_email.rb - - send_sms.rb - - post_slack_message.rb - - deliver_notifications.rb # workflow - - application_task.rb # base class - - login_user.rb - - register_user.rb +```text +/app/ +└── /tasks/ + ├── /orders/ + │ ├── charge_order.rb + │ ├── validate_order.rb + │ ├── fulfill_order.rb + │ └── process_order.rb # workflow + ├── /notifications/ + │ ├── send_email.rb + │ ├── send_sms.rb + │ ├── post_slack_message.rb + │ └── deliver_notifications.rb # workflow + ├── application_task.rb # base class + ├── login_user.rb + └── register_user.rb ``` ### Naming Conventions diff --git a/docs/workflows.md b/docs/workflows.md index 32ab37c8f..ded3ddc2f 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -1,6 +1,6 @@ # Workflows -CMDx::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. +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. ## Table of Contents @@ -38,6 +38,9 @@ end Group related tasks for better organization and shared configuration: +> [!NOTE] +> Settings and conditionals for a group apply to all tasks within that group. + ```ruby class DataProcessingWorkflow < CMDx::Task include CMDx::Workflow @@ -53,9 +56,6 @@ class DataProcessingWorkflow < CMDx::Task end ``` -> [!IMPORTANT] -> Settings and conditionals for a group apply to all tasks within that group. - ### Conditionals Conditionals support multiple syntaxes for flexible execution control: From fc4d50c8056e6521995410d3df6c2ef051120f5b Mon Sep 17 00:00:00 2001 From: Juan Gomez Date: Wed, 20 Aug 2025 21:23:10 -0400 Subject: [PATCH 02/12] Update github alerts --- docs/attributes/coercions.md | 8 ++++---- docs/attributes/definitions.md | 6 +++--- docs/attributes/naming.md | 2 +- docs/attributes/validations.md | 2 +- docs/basics/chain.md | 6 +++--- docs/basics/context.md | 6 +++--- docs/basics/execution.md | 5 ++--- docs/basics/setup.md | 2 +- docs/interruptions/faults.md | 2 +- docs/interruptions/halt.md | 2 +- docs/middlewares.md | 4 ++-- docs/outcomes/result.md | 4 ++-- docs/workflows.md | 4 ++-- 13 files changed, 26 insertions(+), 27 deletions(-) diff --git a/docs/attributes/coercions.md b/docs/attributes/coercions.md index f1ecbc5ef..f02ea6a43 100644 --- a/docs/attributes/coercions.md +++ b/docs/attributes/coercions.md @@ -21,8 +21,8 @@ class ProcessPayment < CMDx::Task # Coerce into a date attribute :paid_with, type: :symbol - # Coerce into a float fallback to big decimal - attribute :total, type: [:float, :big_decimal] + # Coerce into a rational fallback to big decimal + attribute :total, type: [:rational, :big_decimal] # Coerce with options attribute :paid_on, type: :date, strptime: "%m-%d-%Y" @@ -42,7 +42,7 @@ ProcessPayment.execute( ``` > [!TIP] -> Specify multiple types for fallback coercion. CMDx attempts each type in order until one succeeds. +> Specify multiple coercion types for attributes that could be a variety of value formats. CMDx attempts each type in order until one succeeds. ## Built-in Coercions @@ -117,7 +117,7 @@ end Remove custom coercions when no longer needed: -> [!IMPORTANT] +> [!WARNING] > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. ```ruby diff --git a/docs/attributes/definitions.md b/docs/attributes/definitions.md index 1466a4faf..5ad4eb435 100644 --- a/docs/attributes/definitions.md +++ b/docs/attributes/definitions.md @@ -169,7 +169,7 @@ end Nested attributes enable complex attribute structures where child attributes automatically inherit their parent as the source. This allows validation and access of structured data. -> [!IMPORTANT] +> [!NOTE] > All options available to top-level attributes are available to nested attributes, eg: naming, coercions, and validations ```ruby @@ -223,14 +223,14 @@ CreateShipment.execute( ) ``` -> [!TIP] +> [!IMPORTANT] > Child attributes are only required when their parent attribute is provided, enabling flexible optional structures. ## Error Handling Attribute validation failures result in structured error information with details about each failed attribute. -> [!IMPORTANT] +> [!NOTE] > Nested attributes are only ever evaluated when the parent attribute is available and valid. ```ruby diff --git a/docs/attributes/naming.md b/docs/attributes/naming.md index 55be4756c..0cc3a4459 100644 --- a/docs/attributes/naming.md +++ b/docs/attributes/naming.md @@ -2,7 +2,7 @@ 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. -> [!IMPORTANT] +> [!NOTE] > Affixing modifies only the generated accessor method names within tasks. ## Table of Contents diff --git a/docs/attributes/validations.md b/docs/attributes/validations.md index 93ca8e0e3..45c6c48c6 100644 --- a/docs/attributes/validations.md +++ b/docs/attributes/validations.md @@ -258,7 +258,7 @@ end Remove custom validators when no longer needed: -> [!IMPORTANT] +> [!WARNING] > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. ```ruby diff --git a/docs/basics/chain.md b/docs/basics/chain.md index 5d4caf1f3..9f089c6c4 100644 --- a/docs/basics/chain.md +++ b/docs/basics/chain.md @@ -13,7 +13,7 @@ Chains automatically group related task executions within a thread, providing un Each thread maintains its own chain context through thread-local storage, providing automatic isolation without manual coordination. -> [!IMPORTANT] +> [!WARNING] > Chain operations are thread-local. Never share chain references across threads as this can lead to race conditions and data corruption. ```ruby @@ -38,7 +38,7 @@ CMDx::Chain.clear #=> Clears current thread's chain Every task execution automatically creates or joins the current thread's chain: -> [!NOTE] +> [!IMPORTANT] > Chain creation is automatic and transparent. You don't need to manually manage chain lifecycle. ```ruby @@ -89,7 +89,7 @@ chain.results.map { |r| r.task.class } Chains provide comprehensive execution information with state delegation: -> [!NOTE] +> [!IMPORTANT] > Chain state always reflects the first (outer-most) task result, not individual subtask outcomes. Subtasks maintain their own success/failure states. ```ruby diff --git a/docs/basics/context.md b/docs/basics/context.md index 9f543578b..cc2d21881 100644 --- a/docs/basics/context.md +++ b/docs/basics/context.md @@ -21,7 +21,7 @@ ProcessOrder.execute(user_id: 123, currency: "USD") ProcessOrder.new(user_id: 123, "currency" => "USD") ``` -> [!NOTE] +> [!IMPORTANT] > String keys are automatically converted to symbols. Use symbols for consistency in your code. ## Accessing Data @@ -49,7 +49,7 @@ class ProcessOrder < CMDx::Task end ``` -> [!NOTE] +> [!IMPORTANT] > Accessing undefined context attributes returns `nil` instead of raising errors, enabling graceful handling of optional attributes. ## Modifying Context @@ -91,7 +91,7 @@ end ``` > [!TIP] -> Use context for both input attributes and intermediate results. This creates natural data flow through your task execution pipeline. +> Use context for both input values and intermediate results. This creates natural data flow through your task execution pipeline. ## Data Sharing diff --git a/docs/basics/execution.md b/docs/basics/execution.md index 7e048523d..713d412ee 100644 --- a/docs/basics/execution.md +++ b/docs/basics/execution.md @@ -52,9 +52,8 @@ It raises any unhandled non-fault exceptions caused during execution. | `CMDx::FailFault` | Task execution fails | | `CMDx::SkipFault` | Task execution is skipped | -> [!WARNING] -> `execute!` behavior depends on the `task_breakpoints` or `workflow_breakpoints` configuration. -> By default, it raises exceptions only on failures. +> [!IMPORTANT] +> `execute!` behavior depends on the `task_breakpoints` or `workflow_breakpoints` configuration. By default, it raises exceptions only on failures. ```ruby begin diff --git a/docs/basics/setup.md b/docs/basics/setup.md index bea351dd1..6853d5c90 100644 --- a/docs/basics/setup.md +++ b/docs/basics/setup.md @@ -61,7 +61,7 @@ end Tasks follow a predictable call pattern with specific states and statuses: -> [!WARNING] +> [!CAUTION] > Tasks are single-use objects. Once executed, they are frozen and cannot be executed again. | Stage | State | Status | Description | diff --git a/docs/interruptions/faults.md b/docs/interruptions/faults.md index efb9702c0..a4e511d9f 100644 --- a/docs/interruptions/faults.md +++ b/docs/interruptions/faults.md @@ -23,7 +23,7 @@ Faults are exception mechanisms that halt task execution via `skip!` and `fail!` | `CMDx::SkipFault` | `skip!` method | Optional processing, early returns | | `CMDx::FailFault` | `fail!` method | Validation errors, processing failures | -> [!NOTE] +> [!IMPORTANT] > All fault exceptions inherit from `CMDx::Fault` and provide access to the complete task execution context including result, task, context, and chain information. ## Fault Handling diff --git a/docs/interruptions/halt.md b/docs/interruptions/halt.md index f35cee267..59a3ba54a 100644 --- a/docs/interruptions/halt.md +++ b/docs/interruptions/halt.md @@ -17,7 +17,7 @@ Halting stops task execution with explicit intent signaling. Tasks provide two p The `skip!` method indicates a task did not meet criteria to continue execution. This represents a controlled, intentional interruption where the task determines that execution is not necessary or appropriate. -> [!NOTE] +> [!IMPORTANT] > Skipping is not a failure or error. Skipped tasks are considered successful outcomes. ```ruby diff --git a/docs/middlewares.md b/docs/middlewares.md index ffc29be24..25584ddd5 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -18,7 +18,7 @@ Middleware provides Rack-style wrappers around task execution for cross-cutting Middleware executes in a nested fashion, creating an onion-like execution pattern: -> [!IMPORTANT] +> [!NOTE] > Middleware executes in the order they are registered, with the first registered middleware being the outermost wrapper. ```ruby @@ -97,7 +97,7 @@ end Class and Module based declarations can be removed at a global and task level. -> [!IMPORTANT] +> [!WARNING] > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. ```ruby diff --git a/docs/outcomes/result.md b/docs/outcomes/result.md index 3f63a59ec..31a4b1206 100644 --- a/docs/outcomes/result.md +++ b/docs/outcomes/result.md @@ -19,7 +19,7 @@ The result object is the comprehensive return value of task execution, providing Every result provides access to essential execution information: -> [!NOTE] +> [!IMPORTANT] > Result objects are immutable after task execution completes and reflect the final state. ```ruby @@ -141,7 +141,7 @@ result Results support Ruby's pattern matching through array and hash deconstruction: -> [!NOTE] +> [!IMPORTANT] > Pattern matching requires Ruby 3.0+. The `deconstruct` method returns a `[state, status]` array pattern, while `deconstruct_keys` provides hash access to result attributes. ### Array Pattern diff --git a/docs/workflows.md b/docs/workflows.md index ded3ddc2f..306d3be6b 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -17,7 +17,7 @@ Workflow orchestrates sequential execution of multiple tasks in a linear pipelin Tasks execute in declaration order (FIFO). The workflow context propagates to each task, allowing access to data from previous executions. -> [!WARNING] +> [!IMPORTANT] > Do **NOT** define a `work` method in workflow tasks. > The included module automatically provides the execution logic. @@ -38,7 +38,7 @@ end Group related tasks for better organization and shared configuration: -> [!NOTE] +> [!IMPORTANT] > Settings and conditionals for a group apply to all tasks within that group. ```ruby From 89c1b87c8a1badd96e7d7d915e43db339c3c5de6 Mon Sep 17 00:00:00 2001 From: Juan Gomez Date: Wed, 20 Aug 2025 21:53:52 -0400 Subject: [PATCH 03/12] Update README.md --- README.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 085880702..a5c808a07 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,14 @@ # CMDx -`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 that scales from simple tasks to complex multi-step processes. +CMDx is a framework for building maintainable business processes. It simplifies building task objects by offering integrated: + +- Flow controls +- Composable workflows +- Comprehensive logging +- Attribute definition +- Validations and coercions +- And much more... ## Installation @@ -30,9 +37,11 @@ Or install it yourself as: ## Quick Example +Here's how a quick 3 step process can open up a world of possibilities: + ```ruby -# Setup task -# --- +# 1. Setup task +# --------------------------------- class SendWelcomeEmail < CMDx::Task register :middleware, CMDx::Middlewares::Correlate, id: -> { Current.request_id } @@ -63,15 +72,15 @@ class SendWelcomeEmail < CMDx::Task end end -# Execute task -# --- +# 2. Execute task +# --------------------------------- result = SendWelcomeEmail.execute( user_id: 123, "template" => "admin" ) -# Handle result -# --- +# 3. Handle result +# --------------------------------- if result.success? puts "Welcome email sent at #{result.context.sent_at}" elsif result.skipped? From c78c563862394f1641bf04c3a4104057a976d3cf Mon Sep 17 00:00:00 2001 From: Juan Gomez Date: Wed, 20 Aug 2025 22:00:57 -0400 Subject: [PATCH 04/12] Update logging.md --- docs/logging.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/logging.md b/docs/logging.md index f6d30fa7c..f35f412d4 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -42,6 +42,9 @@ caused_failure={index: 2, class: "ProcessPayment", status: "failed"} threw_failure={index: 1, class: "ValidatePayment", status: "failed"} ``` +> [!TIP] +> 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. + ## Structure All log entries include comprehensive execution metadata. Field availability depends on execution context and outcome. From b22e970605abd9cbb878d0d931c9c99772b97181 Mon Sep 17 00:00:00 2001 From: Juan Gomez Date: Wed, 20 Aug 2025 22:04:28 -0400 Subject: [PATCH 05/12] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5c808a07..36de5ab96 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,7 @@ end ## Ecosystem -The following gems are under development: +The following gems are currently under development: - `cmdx-i18n` I18n locales - `cmdx-rspec` RSpec matchers From 4810b8320d1f497ee88a511b2f974e7f116b608f Mon Sep 17 00:00:00 2001 From: Juan Gomez Date: Wed, 20 Aug 2025 22:55:28 -0400 Subject: [PATCH 06/12] Update README.md --- README.md | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 36de5ab96..f386880fa 100644 --- a/README.md +++ b/README.md @@ -42,51 +42,51 @@ Here's how a quick 3 step process can open up a world of possibilities: ```ruby # 1. Setup task # --------------------------------- -class SendWelcomeEmail < CMDx::Task +class AnalyzeMetrics < CMDx::Task register :middleware, CMDx::Middlewares::Correlate, id: -> { Current.request_id } - on_success :track_email_delivery! + on_success :track_analysis_completion! - required :user_id, type: :integer, numeric: { min: 1 } - optional :template, default: "customer" + required :dataset_id, type: :integer, numeric: { min: 1 } + optional :analysis_type, default: "standard" def work - if user.nil? - fail!("User not found", code: 404) - elsif user.unconfirmed? - skip!("Email not verified") + if dataset.nil? + fail!("Dataset not found", code: 404) + elsif dataset.unprocessed? + skip!("Dataset not ready for analysis") else - context.message = UserMailer.welcome(user, template).deliver_now - context.sent_at = Time.now + context.result = PValueAnalyzer.analyze(dataset, analysis_type) + context.analyzed_at = Time.now end end private - def user - @user ||= User.find_by(id: user_id) + def dataset + @dataset ||= Dataset.find_by(id: dataset_id) end - def track_email_delivery! - user.update!(welcome_email_message_id: context.message.id) + def track_analysis_completion! + dataset.update!(analysis_result_id: context.result.id) end end # 2. Execute task # --------------------------------- -result = SendWelcomeEmail.execute( - user_id: 123, - "template" => "admin" +result = AnalyzeMetrics.execute( + dataset_id: 123, + "analysis_type" => "advanced" ) # 3. Handle result # --------------------------------- if result.success? - puts "Welcome email sent at #{result.context.sent_at}" + puts "Metrics analyzed at #{result.context.analyzed_at}" elsif result.skipped? - puts "Skipped: #{result.reason}" + puts "Skipping analyzation due to: #{result.reason}" elsif result.failed? - puts "Failed: #{result.reason} with code: #{result.metadata[:code]}" + puts "Analyzation failed due to: #{result.reason} with code #{result.metadata[:code]}" end ``` From 05f333ea928c993caa1c1364c7aa822f72db75da Mon Sep 17 00:00:00 2001 From: Juan Gomez Date: Wed, 20 Aug 2025 23:37:51 -0400 Subject: [PATCH 07/12] Update examples --- docs/attributes/coercions.md | 72 ++++++------ docs/attributes/defaults.md | 46 ++++---- docs/attributes/definitions.md | 192 +++++++++++++++---------------- docs/attributes/naming.md | 32 +++--- docs/attributes/validations.md | 116 +++++++++---------- docs/basics/chain.md | 26 ++--- docs/basics/context.md | 65 +++++------ docs/basics/execution.md | 24 ++-- docs/basics/setup.md | 18 +-- docs/callbacks.md | 64 +++++------ docs/deprecation.md | 42 +++---- docs/getting_started.md | 94 +++++++-------- docs/internationalization.md | 8 +- docs/interruptions/exceptions.md | 18 +-- docs/interruptions/faults.md | 76 ++++++------ docs/interruptions/halt.md | 88 +++++++------- docs/logging.md | 26 ++--- docs/middlewares.md | 80 ++++++------- docs/outcomes/result.md | 46 ++++---- docs/outcomes/states.md | 10 +- docs/outcomes/statuses.md | 14 +-- docs/tips_and_tricks.md | 82 ++++++------- docs/workflows.md | 94 +++++++-------- 23 files changed, 667 insertions(+), 666 deletions(-) diff --git a/docs/attributes/coercions.md b/docs/attributes/coercions.md index f02ea6a43..17b79d312 100644 --- a/docs/attributes/coercions.md +++ b/docs/attributes/coercions.md @@ -17,27 +17,27 @@ Attribute coercions automatically convert task arguments to expected types, ensu Define attribute types to enable automatic coercion: ```ruby -class ProcessPayment < CMDx::Task - # Coerce into a date - attribute :paid_with, type: :symbol +class ParseMetrics < CMDx::Task + # Coerce into a symbol + attribute :measurement_type, type: :symbol # Coerce into a rational fallback to big decimal - attribute :total, type: [:rational, :big_decimal] + attribute :value, type: [:rational, :big_decimal] # Coerce with options - attribute :paid_on, type: :date, strptime: "%m-%d-%Y" + attribute :recorded_at, type: :date, strptime: "%m-%d-%Y" def work - paid_with #=> :amex - paid_on #=> - total #=> 34.99 (Float) + measurement_type #=> :temperature + recorded_at #=> + value #=> 98.6 (Float) end end -ProcessPayment.execute( - paid_with: "amex", - paid_on: "01-23-2020", - total: "34.99" +ParseMetrics.execute( + measurement_type: "temperature", + recorded_at: "01-23-2020", + value: "98.6" ) ``` @@ -72,22 +72,22 @@ ProcessPayment.execute( Use anonymous functions for simple coercion logic: ```ruby -class FindLocation < CMDx::Task +class TransformCoordinates < CMDx::Task # Proc - register :callback, :point, proc do |value, options = {}| + register :callback, :geolocation, proc do |value, options = {}| begin - Point(value) + Geolocation(value) rescue StandardError - raise CMDx::CoercionError, "could not convert into a point" + raise CMDx::CoercionError, "could not convert into a geolocation" end end # Lambda - register :callback, :point, ->(value, options = {}) { + register :callback, :geolocation, ->(value, options = {}) { begin - Point(value) + Geolocation(value) rescue StandardError - raise CMDx::CoercionError, "could not convert into a point" + raise CMDx::CoercionError, "could not convert into a geolocation" end } end @@ -98,18 +98,18 @@ end Register custom coercion logic for specialized type handling: ```ruby -class PointCoercion +class GeolocationCoercion def self.call(value, options = {}) - Point(value) + Geolocation(value) rescue StandardError - raise CMDx::CoercionError, "could not convert into a point" + raise CMDx::CoercionError, "could not convert into a geolocation" end end -class FindLocation < CMDx::Task - register :coercion, :point, PointCoercion +class TransformCoordinates < CMDx::Task + register :coercion, :geolocation, GeolocationCoercion - attribute :longitude, type: :point + attribute :latitude, type: :geolocation end ``` @@ -121,8 +121,8 @@ Remove custom coercions when no longer needed: > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. ```ruby -class ProcessOrder < CMDx::Task - deregister :coercion, :point +class TransformCoordinates < CMDx::Task + deregister :coercion, :geolocation end ``` @@ -131,27 +131,27 @@ end Coercion failures provide detailed error information including attribute paths, attempted types, and specific failure reasons: ```ruby -class ProcessData < CMDx::Task - attribute :count, type: :integer - attribute :amount, type: [:float, :big_decimal] +class AnalyzePerformance < CMDx::Task + attribute :iterations, type: :integer + attribute :score, type: [:float, :big_decimal] def work # Your logic here... end end -result = ProcessData.execute( - count: "not-a-number", - amount: "invalid-float" +result = AnalyzePerformance.execute( + iterations: "not-a-number", + score: "invalid-float" ) result.state #=> "interrupted" result.status #=> "failed" -result.reason #=> "count could not coerce into an integer. amount could not coerce into one of: float, big_decimal." +result.reason #=> "iterations could not coerce into an integer. score could not coerce into one of: float, big_decimal." result.metadata #=> { # messages: { - # count: ["could not coerce into an integer"], - # amount: ["could not coerce into one of: float, big_decimal"] + # iterations: ["could not coerce into an integer"], + # score: ["could not coerce into one of: float, big_decimal"] # } # } ``` diff --git a/docs/attributes/defaults.md b/docs/attributes/defaults.md index cddefdfab..f0dc71702 100644 --- a/docs/attributes/defaults.md +++ b/docs/attributes/defaults.md @@ -17,21 +17,21 @@ Defaults apply when attributes are not provided or resolve to `nil`. They work s ### Static Values ```ruby -class ProcessOrder < CMDx::Task - attribute :charge_type, default: :credit_card - attribute :priority, default: "standard" - attribute :send_email, default: true - attribute :max_retries, default: 3 - attribute :tags, default: [] - attribute :data, default: {} +class OptimizeDatabase < CMDx::Task + attribute :strategy, default: :incremental + attribute :level, default: "basic" + attribute :notify_admin, default: true + attribute :timeout_minutes, default: 30 + attribute :indexes, default: [] + attribute :options, default: {} def work - charge_type #=> :credit_card - priority #=> "standard" - send_email #=> true - max_retries #=> 3 - tags #=> [] - data #=> {} + strategy #=> :incremental + level #=> "basic" + notify_admin #=> true + timeout_minutes #=> 30 + indexes #=> [] + options #=> {} end end ``` @@ -41,8 +41,8 @@ end Reference instance methods by symbol for dynamic default values: ```ruby -class ProcessOrder < CMDx::Task - attribute :priority, default: :default_priority +class ProcessAnalytics < CMDx::Task + attribute :granularity, default: :default_granularity def work # Your logic here... @@ -50,8 +50,8 @@ class ProcessOrder < CMDx::Task private - def default_priority - Current.account.pro? ? "priority" : "standard" + def default_granularity + Current.user.premium? ? "hourly" : "daily" end end ``` @@ -61,12 +61,12 @@ end Use anonymous functions for dynamic default values: ```ruby -class ProcessOrder < CMDx::Task +class CacheContent < CMDx::Task # Proc - attribute :send_email, default: proc { Current.account.email_api_key? } + attribute :expire_hours, default: proc { Current.tenant.cache_duration || 24 } # Lambda - attribute :priority, default: -> { Current.account.pro? ? "priority" : "standard" } + attribute :compression, default: -> { Current.tenant.premium? ? "gzip" : "none" } end ``` @@ -75,12 +75,12 @@ end Defaults are subject to the same coercion and validation rules as provided values, ensuring consistency and catching configuration errors early. ```ruby -class ConfigureService < CMDx::Task +class ScheduleBackup < CMDx::Task # Coercions - attribute :retry_count, default: "3", type: :integer + attribute :retention_days, default: "7", type: :integer # Validations - optional :priority, default: "medium", inclusion: { in: %w[low medium high urgent] } + optional :frequency, default: "daily", inclusion: { in: %w[hourly daily weekly monthly] } end ``` diff --git a/docs/attributes/definitions.md b/docs/attributes/definitions.md index 5ad4eb435..7a4e1faf7 100644 --- a/docs/attributes/definitions.md +++ b/docs/attributes/definitions.md @@ -25,29 +25,29 @@ Attributes define the interface between task callers and implementation, enablin Optional attributes return `nil` when not provided. ```ruby -class CreateUser < CMDx::Task - attribute :email - attributes :age, :ssn +class ScheduleEvent < CMDx::Task + attribute :title + attributes :duration, :location # Alias for attributes (preferred) - optional :phone - optional :sex, :tags + optional :description + optional :visibility, :attendees def work - email #=> "user@example.com" - age #=> 25 - ssn #=> nil - phone #=> nil - sex #=> nil - tags #=> ["premium", "beta"] + title #=> "Team Standup" + duration #=> 30 + location #=> nil + description #=> nil + visibility #=> nil + attendees #=> ["alice@company.com", "bob@company.com"] end end # Attributes passed as keyword arguments -CreateUser.execute( - email: "user@example.com", - age: 25, - tags: ["premium", "beta"] +ScheduleEvent.execute( + title: "Team Standup", + duration: 30, + attendees: ["alice@company.com", "bob@company.com"] ) ``` @@ -56,32 +56,32 @@ CreateUser.execute( Required attributes must be provided in call arguments or task execution will fail. ```ruby -class CreateUser < CMDx::Task - attribute :email, required: true - attributes :age, :ssn, required: true +class PublishArticle < CMDx::Task + attribute :title, required: true + attributes :content, :author_id, required: true # Alias for attributes => required: true (preferred) - required :phone - required :sex, :tags + required :category + required :status, :tags def work - email #=> "user@example.com" - age #=> 25 - ssn #=> "123-456" - phone #=> "888-9909" - sex #=> :male - tags #=> ["premium", "beta"] + title #=> "Getting Started with Ruby" + content #=> "This is a comprehensive guide..." + author_id #=> 42 + category #=> "programming" + status #=> :published + tags #=> ["ruby", "beginner"] end end # Attributes passed as keyword arguments -CreateUser.execute( - email: "user@example.com", - age: 25, - ssn: "123-456", - phone: "888-9909", - sex: :male, - tags: ["premium", "beta"] +PublishArticle.execute( + title: "Getting Started with Ruby", + content: "This is a comprehensive guide...", + author_id: 42, + category: "programming", + status: :published, + tags: ["ruby", "beginner"] ) ``` @@ -92,18 +92,18 @@ Attributes delegate to accessible objects within the task. The default source is ### Context ```ruby -class UpdateProfile < CMDx::Task +class BackupDatabase < CMDx::Task # Default source is :context - required :user_id - optional :avatar_url + required :database_name + optional :compression_level # Explicitly specify context source - attribute :email, source: :context + attribute :backup_path, source: :context def work - user_id #=> context.user_id - email #=> context.email - avatar_url #=> context.avatar_url + database_name #=> context.database_name + backup_path #=> context.backup_path + compression_level #=> context.compression_level end end ``` @@ -113,11 +113,11 @@ end Reference instance methods by symbol for dynamic source values: ```ruby -class UpdateProfile < CMDx::Task - attributes :email, :settings, source: :user +class BackupDatabase < CMDx::Task + attributes :host, :credentials, source: :database_config # Access from declared attributes - attribute :email_token, source: :settings + attribute :connection_string, source: :credentials def work # Your logic here... @@ -125,8 +125,8 @@ class UpdateProfile < CMDx::Task private - def user - @user ||= User.find(1) + def database_config + @database_config ||= DatabaseConfig.find(context.database_name) end end ``` @@ -136,12 +136,12 @@ end Use anonymous functions for dynamic source values: ```ruby -class UpdateProfile < CMDx::Task +class BackupDatabase < CMDx::Task # Proc - attribute :email, source: proc { Current.user } + attribute :timestamp, source: proc { Time.current } # Lambda - attribute :email, source: -> { Current.user } + attribute :server, source: -> { Current.server } end ``` @@ -150,18 +150,18 @@ end For complex source logic, use classes or modules: ```ruby -class UserSourcer +class DatabaseResolver def self.call(task) - User.find(task.context.user_id) + Database.find(task.context.database_name) end end -class UpdateProfile < CMDx::Task +class BackupDatabase < CMDx::Task # Class or Module - attribute :email, source: UserSourcer + attribute :schema, source: DatabaseResolver # Instance - attribute :email, source: UserSourcer.new + attribute :metadata, source: DatabaseResolver.new end ``` @@ -173,51 +173,51 @@ Nested attributes enable complex attribute structures where child attributes aut > All options available to top-level attributes are available to nested attributes, eg: naming, coercions, and validations ```ruby -class CreateShipment < CMDx::Task +class ConfigureServer < CMDx::Task # Required parent with required children - required :shipping_address do - required :street, :city, :state, :zip - optional :apartment - attribute :instructions + required :network_config do + required :hostname, :port, :protocol, :subnet + optional :load_balancer + attribute :firewall_rules end # Optional parent with conditional children - optional :billing_address do - required :street, :city # Only required if billing_address provided - optional :same_as_shipping, prefix: true + optional :ssl_config do + required :certificate_path, :private_key # Only required if ssl_config provided + optional :enable_http2, prefix: true end # Multi-level nesting - attribute :special_handling do - required :type + attribute :monitoring do + required :provider - optional :insurance do - required :coverage_amount - optional :carrier + optional :alerting do + required :threshold_percentage + optional :notification_channel end end def work - shipping_address #=> { street: "123 Main St" ... } - street #=> "123 Main St" - apartment #=> nil + network_config #=> { hostname: "api.company.com" ... } + hostname #=> "api.company.com" + load_balancer #=> nil end end -CreateShipment.execute( - order_id: 123, - shipping_address: { - street: "123 Main St", - city: "Miami", - state: "FL", - zip: "33101", - instructions: "Leave at door" +ConfigureServer.execute( + server_id: "srv-001", + network_config: { + hostname: "api.company.com", + port: 443, + protocol: "https", + subnet: "10.0.1.0/24", + firewall_rules: "allow_web_traffic" }, - special_handling: { - type: "fragile", - insurance: { - coverage_amount: 500.00, - carrier: "FedEx" + monitoring: { + provider: "datadog", + alerting: { + threshold_percentage: 85.0, + notification_channel: "slack" } } ) @@ -234,10 +234,10 @@ Attribute validation failures result in structured error information with detail > Nested attributes are only ever evaluated when the parent attribute is available and valid. ```ruby -class ProcessOrder < CMDx::Task - required :user_id, :order_id - required :shipping_address do - required :street, :city +class ConfigureServer < CMDx::Task + required :server_id, :environment + required :network_config do + required :hostname, :port end def work @@ -246,31 +246,31 @@ class ProcessOrder < CMDx::Task end # Missing required top-level attributes -result = ProcessOrder.execute(user_id: 123) +result = ConfigureServer.execute(server_id: "srv-001") result.state #=> "interrupted" result.status #=> "failed" -result.reason #=> "order_id is required. shipping_address is required." +result.reason #=> "environment is required. network_config is required." result.metadata #=> { # messages: { - # order_id: ["is required"], - # shipping_address: ["is required"] + # environment: ["is required"], + # network_config: ["is required"] # } # } # Missing required nested attributes -result = ProcessOrder.execute( - user_id: 123, - order_id: 456, - shipping_address: { street: "123 Main St" } # Missing city +result = ConfigureServer.execute( + server_id: "srv-001", + environment: "production", + network_config: { hostname: "api.company.com" } # Missing port ) result.state #=> "interrupted" result.status #=> "failed" -result.reason #=> "city is required." +result.reason #=> "port is required." result.metadata #=> { # messages: { - # city: ["is required"] + # port: ["is required"] # } # } ``` diff --git a/docs/attributes/naming.md b/docs/attributes/naming.md index 0cc3a4459..dbe562f75 100644 --- a/docs/attributes/naming.md +++ b/docs/attributes/naming.md @@ -16,21 +16,21 @@ Attribute naming provides method name customization to prevent conflicts and ena Adds a prefix to the generated accessor method name. ```ruby -class UpdateCustomer < CMDx::Task +class GenerateReport < CMDx::Task # Dynamic from attribute source - attribute :id, prefix: true + attribute :template, prefix: true # Static - attribute :name, prefix: "customer_" + attribute :format, prefix: "report_" def work - context_id #=> 123 - customer_name #=> "Jane Smith" + context_template #=> "monthly_sales" + report_format #=> "pdf" end end # Attributes passed as original attribute names -UpdateCustomer.execute(id: 123, name: "Jane Smith") +GenerateReport.execute(template: "monthly_sales", format: "pdf") ``` ## Suffix @@ -38,21 +38,21 @@ UpdateCustomer.execute(id: 123, name: "Jane Smith") Adds a suffix to the generated accessor method name. ```ruby -class UpdateCustomer < CMDx::Task +class DeployApplication < CMDx::Task # Dynamic from attribute source - attribute :email, suffix: true + attribute :branch, suffix: true # Static - attribute :phone, suffix: "_number" + attribute :version, suffix: "_tag" def work - email_context #=> "jane@example.com" - phone_number #=> "555-0123" + branch_context #=> "main" + version_tag #=> "v1.2.3" end end # Attributes passed as original attribute names -UpdateCustomer.execute(email: "jane@example.com", phone: "555-0123") +DeployApplication.execute(branch: "main", version: "v1.2.3") ``` ## As @@ -60,16 +60,16 @@ UpdateCustomer.execute(email: "jane@example.com", phone: "555-0123") Completely renames the generated accessor method. ```ruby -class UpdateCustomer < CMDx::Task - attribute :birthday, as: :bday +class ScheduleMaintenance < CMDx::Task + attribute :scheduled_at, as: :when def work - bday #=> + when #=> end end # Attributes passed as original attribute names -UpdateCustomer.execute(birthday: Date.new(2020, 10, 31)) +ScheduleMaintenance.execute(scheduled_at: DateTime.new(2024, 12, 15, 2, 0, 0)) ``` --- diff --git a/docs/attributes/validations.md b/docs/attributes/validations.md index 45c6c48c6..7c5fa7c96 100644 --- a/docs/attributes/validations.md +++ b/docs/attributes/validations.md @@ -24,32 +24,32 @@ Attribute validations ensure task arguments meet specified requirements before e Define validation rules on attributes to enforce data requirements: ```ruby -class ProcessOrder < CMDx::Task +class ProcessSubscription < CMDx::Task # Required field with presence validation - attribute :customer_id, presence: true + attribute :user_id, presence: true # String with length constraints - attribute :notes, length: { minimum: 10, maximum: 500 } + attribute :preferences, length: { minimum: 10, maximum: 500 } # Numeric range validation - attribute :quantity, inclusion: { in: 1..100 } + attribute :tier_level, inclusion: { in: 1..5 } # Format validation for email - attribute :email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i + attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i def work - customer_id #=> "12345" - notes #=> "Please deliver to front door" - quantity #=> 5 - email #=> "customer@example.com" + user_id #=> "98765" + preferences #=> "Send weekly digest emails" + tier_level #=> 3 + contact_email #=> "user@company.com" end end -ProcessOrder.execute( - customer_id: "12345", - notes: "Please deliver to front door", - quantity: 5, - email: "customer@example.com" +ProcessSubscription.execute( + user_id: "98765", + preferences: "Send weekly digest emails", + tier_level: 3, + contact_email: "user@company.com" ) ``` @@ -72,8 +72,8 @@ This list of options is available to all validators: ### Exclusion ```ruby -class ProcessOrder < CMDx::Task - attribute :status, exclusion: { in: %w[out_of_stock discontinued] } +class ProcessProduct < CMDx::Task + attribute :status, exclusion: { in: %w[recalled archived] } def work # Your logic here... @@ -92,10 +92,10 @@ end ### Format ```ruby -class ProcessOrder < CMDx::Task - attribute :email, exclusion: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i +class ProcessProduct < CMDx::Task + attribute :sku, format: /\A[A-Z]{3}-[0-9]{4}\z/ - attribute :email, exclusion: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i } + attribute :sku, format: { with: /\A[A-Z]{3}-[0-9]{4}\z/ } def work # Your logic here... @@ -112,8 +112,8 @@ end ### Inclusion ```ruby -class ProcessOrder < CMDx::Task - attribute :status, inclusion: { in: %w[preorder in_stock] } +class ProcessProduct < CMDx::Task + attribute :availability, inclusion: { in: %w[available limited] } def work # Your logic here... @@ -132,8 +132,8 @@ end ### Length ```ruby -class CreateUser < CMDx::Task - attribute :username, length: { within: 1..30 } +class CreateBlogPost < CMDx::Task + attribute :title, length: { within: 5..100 } def work # Your logic here... @@ -163,8 +163,8 @@ end ### Numeric ```ruby -class CreateUser < CMDx::Task - attribute :age, length: { min: 13 } +class CreateBlogPost < CMDx::Task + attribute :word_count, numeric: { min: 100 } def work # Your logic here... @@ -192,10 +192,10 @@ end ### Presence ```ruby -class CreateUser < CMDx::Task - attribute :accept_tos, presence: true +class CreateBlogPost < CMDx::Task + attribute :content, presence: true - attribute :accept_tos, presence: { message: "needs to be accepted" } + attribute :content, presence: { message: "cannot be blank" } def work # Your logic here... @@ -217,18 +217,18 @@ end Use anonymous functions for simple validation logic: ```ruby -class CreateWebsite < CMDx::Task +class SetupApplication < CMDx::Task # Proc - register :validator, :domain, proc do |value, options = {}| - unless value.match?(/\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}\z/) - raise CMDx::ValidationError, "invalid domain format" + register :validator, :api_key, proc do |value, options = {}| + unless value.match?(/\A[a-zA-Z0-9]{32}\z/) + raise CMDx::ValidationError, "invalid API key format" end end # Lambda - register :validator, :domain, ->(value, options = {}) { - unless value.match?(/\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}\z/) - raise CMDx::ValidationError, "invalid domain format" + register :validator, :api_key, ->(value, options = {}) { + unless value.match?(/\A[a-zA-Z0-9]{32}\z/) + raise CMDx::ValidationError, "invalid API key format" end } end @@ -239,18 +239,18 @@ end Register custom validation logic for specialized requirements: ```ruby -class DomainValidator +class ApiKeyValidator def self.call(value, options = {}) - unless value.match?(/\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,}\z/) - raise CMDx::ValidationError, "invalid domain format" + unless value.match?(/\A[a-zA-Z0-9]{32}\z/) + raise CMDx::ValidationError, "invalid API key format" end end end -class CreateWebsite < CMDx::Task - register :validator, :domain, DomainValidator +class SetupApplication < CMDx::Task + register :validator, :api_key, ApiKeyValidator - attribute :domain_name, domain: true + attribute :access_key, api_key: true end ``` @@ -262,8 +262,8 @@ Remove custom validators when no longer needed: > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. ```ruby -class CreateWebsite < CMDx::Task - deregister :validator, :domain +class SetupApplication < CMDx::Task + deregister :validator, :api_key end ``` @@ -272,33 +272,33 @@ end Validation failures provide detailed error information including attribute paths, validation rules, and specific failure reasons: ```ruby -class CreateUser < CMDx::Task - attribute :username, presence: true, length: { minimum: 3, maximum: 20 } - attribute :age, numeric: { greater_than: 13, less_than: 120 } - attribute :role, inclusion: { in: [:user, :moderator, :admin] } - attribute :email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i +class CreateProject < CMDx::Task + attribute :project_name, presence: true, length: { minimum: 3, maximum: 50 } + attribute :budget, numeric: { greater_than: 1000, less_than: 1000000 } + attribute :priority, inclusion: { in: [:low, :medium, :high] } + attribute :contact_email, format: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i def work # Your logic here... end end -result = CreateUser.execute( - username: "ab", # Too short - age: 10, # Too young - role: :superuser, # Not in allowed list - email: "invalid-email" # Invalid format +result = CreateProject.execute( + project_name: "AB", # Too short + budget: 500, # Too low + priority: :urgent, # Not in allowed list + contact_email: "invalid-email" # Invalid format ) result.state #=> "interrupted" result.status #=> "failed" -result.reason #=> "username is too short (minimum is 3 characters). age must be greater than 13. role is not included in the list. email is invalid." +result.reason #=> "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." result.metadata #=> { # messages: { - # username: ["is too short (minimum is 3 characters)"], - # age: ["must be greater than 13"], - # role: ["is not included in the list"], - # email: ["is invalid"] + # 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"] # } # } ``` diff --git a/docs/basics/chain.md b/docs/basics/chain.md index 9f089c6c4..21fb18071 100644 --- a/docs/basics/chain.md +++ b/docs/basics/chain.md @@ -19,13 +19,13 @@ Each thread maintains its own chain context through thread-local storage, provid ```ruby # Thread A Thread.new do - result = ProcessOrder.execute(order_id: 123) + result = ImportDataset.execute(file_path: "/data/batch1.csv") result.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5" end # Thread B (completely separate chain) Thread.new do - result = ProcessOrder.execute(order_id: 456) + result = ImportDataset.execute(file_path: "/data/batch2.csv") result.chain.id #=> "z3a42b95-c821-7892-b156-dd7c921fe2a3" end @@ -42,15 +42,15 @@ Every task execution automatically creates or joins the current thread's chain: > Chain creation is automatic and transparent. You don't need to manually manage chain lifecycle. ```ruby -class ProcessOrder < CMDx::Task +class ImportDataset < CMDx::Task def work # First task creates new chain - result1 = ProcessOrder.execute(order_id: 123) + result1 = ValidateHeaders.execute(file_path: context.file_path) result1.chain.id #=> "018c2b95-b764-7615-a924-cc5b910ed1e5" result1.chain.results.size #=> 1 # Second task joins existing chain - result2 = SendEmail.execute(to: "user@example.com") + result2 = SendNotification.execute(to: "admin@company.com") result2.chain.id == result1.chain.id #=> true result2.chain.results.size #=> 2 @@ -65,24 +65,24 @@ end When tasks call subtasks within the same thread, all executions automatically inherit the current chain, creating a unified execution trail. ```ruby -class ProcessOrder < CMDx::Task +class ImportDataset < CMDx::Task def work - context.order = Order.find(order_id) + context.dataset = Dataset.find(context.dataset_id) # Subtasks automatically inherit current chain - ValidateOrder.execute - ChargePayment.execute!(context) - SendConfirmation.execute(order_id: order_id) + ValidateSchema.execute + TransformData.execute!(context) + SaveToDatabase.execute(dataset_id: context.dataset_id) end end -result = ProcessOrder.execute(order_id: 123) +result = ImportDataset.execute(dataset_id: 456) chain = result.chain # All tasks share the same chain chain.results.size #=> 4 (main task + 3 subtasks) chain.results.map { |r| r.task.class } -#=> [ProcessOrder, ValidateOrder, ChargePayment, SendConfirmation] +#=> [ImportDataset, ValidateSchema, TransformData, SaveToDatabase] ``` ## Structure @@ -93,7 +93,7 @@ Chains provide comprehensive execution information with state delegation: > Chain state always reflects the first (outer-most) task result, not individual subtask outcomes. Subtasks maintain their own success/failure states. ```ruby -result = ProcessOrder.execute(order_id: 123) +result = ImportDataset.execute(dataset_id: 456) chain = result.chain # Chain identification diff --git a/docs/basics/context.md b/docs/basics/context.md index cc2d21881..d3956336c 100644 --- a/docs/basics/context.md +++ b/docs/basics/context.md @@ -15,10 +15,10 @@ Context is automatically populated with all inputs passed to a task. All keys ar ```ruby # Direct execution -ProcessOrder.execute(user_id: 123, currency: "USD") +CalculateShipping.execute(weight: 2.5, destination: "CA") # Instance creation -ProcessOrder.new(user_id: 123, "currency" => "USD") +CalculateShipping.new(weight: 2.5, "destination" => "CA") ``` > [!IMPORTANT] @@ -29,22 +29,22 @@ ProcessOrder.new(user_id: 123, "currency" => "USD") Context provides multiple access patterns with automatic nil safety: ```ruby -class ProcessOrder < CMDx::Task +class CalculateShipping < CMDx::Task def work # Method style access (preferred) - user_id = context.user_id - amount = context.amount + weight = context.weight + destination = context.destination # Hash style access - order_id = context[:order_id] - metadata = context["metadata"] + service_type = context[:service_type] + options = context["options"] # Safe access with defaults - priority = context.fetch!(:priority, "normal") - source = context.dig(:metadata, :source) + rush_delivery = context.fetch!(:rush_delivery, false) + carrier = context.dig(:options, :carrier) # Shorter alias - total = ctx.amount * ctx.tax_rate # ctx aliases context + cost = ctx.weight * ctx.rate_per_pound # ctx aliases context end end ``` @@ -57,35 +57,36 @@ end Context supports dynamic modification during task execution: ```ruby -class ProcessOrder < CMDx::Task +class CalculateShipping < CMDx::Task def work # Direct assignment - context.user = User.find(context.user_id) - context.order = Order.find(context.order_id) - context.processed_at = Time.now + context.carrier = Carrier.find_by(code: context.carrier_code) + context.package = Package.new(weight: context.weight) + context.calculated_at = Time.now # Hash-style assignment - context[:status] = "processing" - context["result_code"] = "SUCCESS" + context[:status] = "calculating" + context["tracking_number"] = "SHIP#{SecureRandom.hex(6)}" # Conditional assignment - context.notification_sent ||= false + context.insurance_included ||= false # Batch updates context.merge!( status: "completed", - total_amount: calculate_total, - completion_time: Time.now + shipping_cost: calculate_cost, + estimated_delivery: Time.now + 3.days ) # Remove sensitive data - context.delete!(:credit_card_number) + context.delete!(:credit_card_token) end private - def calculate_total - context.amount + (context.amount * context.tax_rate) + def calculate_cost + base_rate = context.weight * context.rate_per_pound + base_rate + (base_rate * context.tax_percentage) end end ``` @@ -99,28 +100,28 @@ Context enables seamless data flow between related tasks in complex workflows: ```ruby # During execution -class ProcessOrder < CMDx::Task +class CalculateShipping < CMDx::Task def work - # Validate order data - validation_result = ValidateOrder.execute(context) + # Validate shipping data + validation_result = ValidateAddress.execute(context) # Via context - ProcessPayment.execute(context) + CalculateInsurance.execute(context) # Via result - NotifyOrderProcessed.execute(validation_result) + NotifyShippingCalculated.execute(validation_result) # Context now contains accumulated data from all tasks - context.order_validated #=> true (from validation) - context.payment_processed #=> true (from payment) - context.notification_sent #=> true (from notification) + context.address_validated #=> true (from validation) + context.insurance_calculated #=> true (from insurance) + context.notification_sent #=> true (from notification) end end # After execution -result = ProcessOrder.execute(order_number: 123) +result = CalculateShipping.execute(destination: "New York, NY") -ShipOrder.execute(result) +CreateShippingLabel.execute(result) ``` --- diff --git a/docs/basics/execution.md b/docs/basics/execution.md index 713d412ee..b75dd094f 100644 --- a/docs/basics/execution.md +++ b/docs/basics/execution.md @@ -28,7 +28,7 @@ This is the preferred method for most use cases. Any unhandled exceptions will be caught and returned as a task failure. ```ruby -result = ProcessOrder.execute(order_id: 12345) +result = CreateAccount.execute(email: "user@example.com") # Check execution state result.success? #=> true/false @@ -36,7 +36,7 @@ result.failed? #=> true/false result.skipped? #=> true/false # Access result data -result.context.order_id #=> 12345 +result.context.email #=> "user@example.com" result.state #=> "complete" result.status #=> "success" ``` @@ -57,14 +57,14 @@ It raises any unhandled non-fault exceptions caused during execution. ```ruby begin - result = ProcessOrder.execute!(order_id: 12345) - SendConfirmation.execute(result.context) + result = CreateAccount.execute!(email: "user@example.com") + SendWelcomeEmail.execute(result.context) rescue CMDx::FailFault => e - RetryOrderJob.perform_later(e.result.context.order_id) + ScheduleAccountRetryJob.perform_later(e.result.context.email) rescue CMDx::SkipFault => e - Rails.logger.info("Order skipped: #{e.result.reason}") + Rails.logger.info("Account creation skipped: #{e.result.reason}") rescue Exception => e - BugTracker.notify(unhandled_exception: e) + ErrorTracker.capture(unhandled_exception: e) end ``` @@ -74,12 +74,12 @@ Tasks can be instantiated directly for advanced use cases, testing, and custom e ```ruby # Direct instantiation -task = ProcessOrder.new(order_id: 12345, notify_customer: true) +task = CreateAccount.new(email: "user@example.com", send_welcome: true) # Access properties before execution task.id #=> "abc123..." (unique task ID) -task.context.order_id #=> 12345 -task.context.notify_customer #=> true +task.context.email #=> "user@example.com" +task.context.send_welcome #=> true task.result.state #=> "initialized" task.result.status #=> "success" @@ -96,11 +96,11 @@ task.result.success? #=> true/false The `Result` object provides comprehensive execution information: ```ruby -result = ProcessOrder.execute(order_id: 12345) +result = CreateAccount.execute(email: "user@example.com") # Execution metadata result.id #=> "abc123..." (unique execution ID) -result.task #=> ProcessOrderTask instance (frozen) +result.task #=> CreateAccount instance (frozen) result.chain #=> Task execution chain # Context and metadata diff --git a/docs/basics/setup.md b/docs/basics/setup.md index 6853d5c90..83041e4e9 100644 --- a/docs/basics/setup.md +++ b/docs/basics/setup.md @@ -13,7 +13,7 @@ Tasks are the core building blocks of CMDx, encapsulating business logic within Tasks inherit from `CMDx::Task` and require only a `work` method: ```ruby -class ProcessUserOrder < CMDx::Task +class ValidateDocument < CMDx::Task def work # Your logic here... end @@ -23,11 +23,11 @@ end An exception will be raised if a work method is not defined. ```ruby -class InvalidTask < CMDx::Task +class IncompleteTask < CMDx::Task # No `work` method defined end -InvalidTask.execute #=> raises CMDx::UndefinedMethodError +IncompleteTask.execute #=> raises CMDx::UndefinedMethodError ``` ## Inheritance @@ -37,20 +37,20 @@ Create a base class to share common configuration across tasks: ```ruby class ApplicationTask < CMDx::Task - register :middleware, AuthenticateUserMiddleware + register :middleware, SecurityMiddleware - before_execution :set_correlation_id + before_execution :initialize_request_tracking - attribute :request_id + attribute :session_id private - def set_correlation_id - context.correlation_id ||= SecureRandom.uuid + def initialize_request_tracking + context.tracking_id ||= SecureRandom.uuid end end -class ProcessOrder < ApplicationTask +class SyncInventory < ApplicationTask def work # Your logic here... end diff --git a/docs/callbacks.md b/docs/callbacks.md index 789af1db8..4ef988a23 100644 --- a/docs/callbacks.md +++ b/docs/callbacks.md @@ -38,11 +38,11 @@ Callbacks execute in precise lifecycle order. Here is the complete execution seq Reference instance methods by symbol for simple callback logic: ```ruby -class ProcessOrder < CMDx::Task - before_execution :find_order +class ProcessBooking < CMDx::Task + before_execution :find_reservation # Batch declarations (works for any type) - on_complete :notify_customer, :update_inventory + on_complete :notify_guest, :update_availability def work # Your logic here... @@ -50,16 +50,16 @@ class ProcessOrder < CMDx::Task private - def find_order - @order ||= Order.find(context.order_id) + def find_reservation + @reservation ||= Reservation.find(context.reservation_id) end - def notify_customer - CustomerNotifier.call(context.user, result) + def notify_guest + GuestNotifier.call(context.guest, result) end - def update_inventory - InventoryService.update(context.product_ids, result) + def update_availability + AvailabilityService.update(context.room_ids, result) end end ``` @@ -69,12 +69,12 @@ end Use anonymous functions for inline callback logic: ```ruby -class ProcessOrder < CMDx::Task +class ProcessBooking < CMDx::Task # Proc - on_interrupted proc { |task| BuildLine.stop! } + on_interrupted proc { |task| ReservationSystem.pause! } # Lambda - on_complete -> { BuildLine.resume! } + on_complete -> { ReservationSystem.resume! } end ``` @@ -83,22 +83,22 @@ end Implement reusable callback logic in dedicated classes: ```ruby -class SendNotificationCallback +class BookingConfirmationCallback def call(task) if task.result.success? - EmailApi.deliver_success_email(task.context.user) + MessagingApi.send_confirmation(task.context.guest) else - EmailApi.deliver_issue_email(task.context.admin) + MessagingApi.send_issue_alert(task.context.manager) end end end -class ProcessOrder < CMDx::Task +class ProcessBooking < CMDx::Task # Class or Module - on_success SendNotificationCallback + on_success BookingConfirmationCallback # Instance - on_interrupted SendNotificationCallback.new + on_interrupted BookingConfirmationCallback.new end ``` @@ -107,27 +107,27 @@ end Control callback execution with conditional logic: ```ruby -class AbilityCheck +class MessagingPermissionCheck def call(task) - task.context.user.can?(:send_email) + task.context.guest.can?(:receive_messages) end end -class ProcessOrder < CMDx::Task +class ProcessBooking < CMDx::Task # If and/or Unless - before_execution :notify_customer, if: :email_available?, unless: :email_temporary? + before_execution :notify_guest, if: :messaging_enabled?, unless: :messaging_blocked? # Proc on_failure :increment_failure, if: ->(task) { Rails.env.production? && task.class.name.include?("Legacy") } # Lambda - on_success :ping_warehouse, if: proc { |task| task.context.products_on_backorder? } + on_success :ping_housekeeping, if: proc { |task| task.context.rooms_need_cleaning? } # Class or Module - on_complete :send_notification, unless: AbilityCheck + on_complete :send_confirmation, unless: MessagingPermissionCheck # Instance - on_complete :send_notification, if: AbilityCheck.new + on_complete :send_confirmation, if: MessagingPermissionCheck.new def work # Your logic here... @@ -135,12 +135,12 @@ class ProcessOrder < CMDx::Task private - def email_available? - context.user.email.present? + def messaging_enabled? + context.guest.messaging_preference.present? end - def email_temporary? - context.user.email_service == :temporary + def messaging_blocked? + context.guest.communication_status == :blocked end end ``` @@ -153,12 +153,12 @@ Remove callbacks at runtime for dynamic behavior control: > Only one removal operation is allowed per `deregister` call. Multiple removals require separate calls. ```ruby -class ProcessOrder < CMDx::Task +class ProcessBooking < CMDx::Task # Symbol - deregister :callback, :before_execution, :notify_customer + deregister :callback, :before_execution, :notify_guest # Class or Module (no instances) - deregister :callback, :on_complete, SendNotificationCallback + deregister :callback, :on_complete, BookingConfirmationCallback end ``` diff --git a/docs/deprecation.md b/docs/deprecation.md index 8f8d3a58d..d617fdafb 100644 --- a/docs/deprecation.md +++ b/docs/deprecation.md @@ -25,7 +25,7 @@ Task deprecation provides a systematic approach to managing legacy tasks in CMDx > Use `:raise` mode carefully in production environments as it will break existing workflows immediately. ```ruby -class ProcessLegacyPayment < CMDx::Task +class ProcessObsoleteAPI < CMDx::Task settings(deprecated: :raise) def work @@ -33,8 +33,8 @@ class ProcessLegacyPayment < CMDx::Task end end -result = ProcessLegacyPayment.execute -#=> raises CMDx::DeprecationError: "ProcessLegacyPayment usage prohibited" +result = ProcessObsoleteAPI.execute +#=> raises CMDx::DeprecationError: "ProcessObsoleteAPI usage prohibited" ``` ### Log @@ -42,7 +42,7 @@ result = ProcessLegacyPayment.execute `:log` mode allows continued usage while tracking deprecation warnings. Perfect for gradual migration scenarios where immediate replacement isn't feasible. ```ruby -class ProcessOldPayment < CMDx::Task +class ProcessLegacyFormat < CMDx::Task settings(deprecated: :log) # Same @@ -53,11 +53,11 @@ class ProcessOldPayment < CMDx::Task end end -result = ProcessOldPayment.execute +result = ProcessLegacyFormat.execute result.successful? #=> true # Deprecation warning appears in logs: -# WARN -- : DEPRECATED: ProcessOldPayment - migrate to replacement or discontinue use +# WARN -- : DEPRECATED: ProcessLegacyFormat - migrate to replacement or discontinue use ``` ### Warn @@ -65,7 +65,7 @@ result.successful? #=> true `:warn` mode issues Ruby warnings visible in development and testing environments. Useful for alerting developers without affecting production logging. ```ruby -class ProcessObsoletePayment < CMDx::Task +class ProcessOldData < CMDx::Task settings(deprecated: :warn) def work @@ -73,11 +73,11 @@ class ProcessObsoletePayment < CMDx::Task end end -result = ProcessObsoletePayment.execute +result = ProcessOldData.execute result.successful? #=> true # Ruby warning appears in stderr: -# [ProcessObsoletePayment] DEPRECATED: migrate to replacement or discontinue use +# [ProcessOldData] DEPRECATED: migrate to replacement or discontinue use ``` ## Declarations @@ -85,7 +85,7 @@ result.successful? #=> true ### Symbol or String ```ruby -class LegacyIntegration < CMDx::Task +class OutdatedConnector < CMDx::Task # Symbol settings(deprecated: :raise) @@ -97,7 +97,7 @@ end ### Boolean or Nil ```ruby -class LegacyIntegration < CMDx::Task +class OutdatedConnector < CMDx::Task # Deprecates with default :log mode settings(deprecated: true) @@ -110,7 +110,7 @@ end ### Method ```ruby -class LegacyIntegration < CMDx::Task +class OutdatedConnector < CMDx::Task # Symbol settings(deprecated: :deprecated?) @@ -121,7 +121,7 @@ class LegacyIntegration < CMDx::Task private def deprecated? - Time.now.year > 2020 ? :raise : false + Time.now.year > 2024 ? :raise : false end end ``` @@ -129,30 +129,30 @@ end ### Proc or Lambda ```ruby -class LegacyIntegration < CMDx::Task +class OutdatedConnector < CMDx::Task # Proc - settings(deprecated: proc { Rails.env.local? ? :raise : :log }) + settings(deprecated: proc { Rails.env.development? ? :raise : :log }) # Lambda - settings(deprecated: -> { Current.user.legacy? ? :warn : :raise }) + settings(deprecated: -> { Current.tenant.legacy_mode? ? :warn : :raise }) end ``` ### Class or Module ```ruby -class LegacyTaskDeprecator +class OutdatedTaskDeprecator def call(task) - task.class.name.include?("Legacy") + task.class.name.include?("Outdated") end end -class LegacyIntegration < CMDx::Task +class OutdatedConnector < CMDx::Task # Class or Module - settings(deprecated: LegacyTaskDeprecator) + settings(deprecated: OutdatedTaskDeprecator) # Instance - settings(deprecated: LegacyTaskDeprecator.new) + settings(deprecated: OutdatedTaskDeprecator.new) end ``` diff --git a/docs/getting_started.md b/docs/getting_started.md index ea4242841..2a5f57525 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -81,15 +81,15 @@ CMDx.configure do |config| # Via proc or lambda config.middlewares.register proc { |task, options| - start = Time.now + start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = yield - finish = Time.now - Rails.logger.debug { "task complete in #{finish - start}ms" } + end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) + Rails.logger.debug { "task completed in #{((end_time - start_time) * 1000).round(2)}ms" } result } # With options - config.middlewares.register MetricsMiddleware, namespace: "app.tasks" + config.middlewares.register AuditTrailMiddleware, service_name: "document_processor" # Remove middleware config.middlewares.deregister CMDx::Middlewares::Timeout @@ -105,22 +105,22 @@ end ```ruby CMDx.configure do |config| # Via method - config.callbacks.register :before_execution, :setup_request_context + config.callbacks.register :before_execution, :initialize_user_session # Via callable (must respond to `call(task)`) - config.callbacks.register :on_success, TrackSuccessfulPurchase + config.callbacks.register :on_success, LogUserActivity # Via proc or lambda config.callbacks.register :on_complete, proc { |task| - duration = task.metadata[:runtime] - StatsD.histogram("task.duration", duration, tags: ["class:#{task.class.name}"]) + execution_time = task.metadata[:runtime] + Metrics.timer("task.execution_time", execution_time, tags: ["task:#{task.class.name.underscore}"]) } # With options - config.callbacks.register :on_failure, :notify_admin, if: :production? + config.callbacks.register :on_failure, :send_alert_notification, if: :critical_task? # Remove callback - config.callbacks.deregister :on_success, TrackSuccessfulPurchase + config.callbacks.deregister :on_success, LogUserActivity end ``` @@ -129,22 +129,22 @@ end ```ruby CMDx.configure do |config| # Via callable (must respond to `call(value, options)`) - config.coercions.register :money, MoneyCoercion + config.coercions.register :currency, CurrencyCoercion - # Via method (must match signature `def point_coercion(value, options)`) - config.coercions.register :point, :point_coercion + # Via method (must match signature `def coordinates_coercion(value, options)`) + config.coercions.register :coordinates, :coordinates_coercion # Via proc or lambda - config.coercions.register :csv_array, proc { |value, options| - separator = options[:separator] || ',' - max_items = options[:max_items] || 100 + config.coercions.register :tag_list, proc { |value, options| + delimiter = options[:delimiter] || ',' + max_tags = options[:max_tags] || 50 - items = value.to_s.split(separator).map(&:strip).reject(&:empty?) - items.first(max_items) + tags = value.to_s.split(delimiter).map(&:strip).reject(&:empty?) + tags.first(max_tags) } # Remove coercion - config.coercions.deregister :money + config.coercions.deregister :currency end ``` @@ -153,21 +153,21 @@ end ```ruby CMDx.configure do |config| # Via callable (must respond to `call(value, options)`) - config.validators.register :email, EmailValidator + config.validators.register :username, UsernameValidator - # Via method (must match signature `def phone_validator(value, options)`) - config.validators.register :phone, :phone_validator + # Via method (must match signature `def url_validator(value, options)`) + config.validators.register :url, :url_validator # Via proc or lambda - config.validators.register :api_key, proc { |value, options| - required_prefix = options[:prefix] || "sk_" - min_length = options[:min_length] || 32 + config.validators.register :access_token, proc { |value, options| + expected_prefix = options[:prefix] || "tok_" + minimum_length = options[:min_length] || 40 - value.start_with?(required_prefix) && value.length >= min_length + value.start_with?(expected_prefix) && value.length >= minimum_length } # Remove validator - config.validators.deregister :email + config.validators.deregister :username end ``` @@ -178,7 +178,7 @@ end Override global configuration for specific tasks using `settings`: ```ruby -class ProcessPayment < CMDx::Task +class GenerateInvoice < CMDx::Task settings( # Global configuration overrides task_breakpoints: ["failed"], # Breakpoint override @@ -189,7 +189,7 @@ class ProcessPayment < CMDx::Task breakpoints: ["failed"], # Contextual pointer for :task_breakpoints and :workflow_breakpoints log_level: :info, # Log level override log_formatter: CMDx::LogFormatters::Json.new # Log formatter override - tags: ["payments", "critical"], # Logging tags + tags: ["billing", "financial"], # Logging tags deprecated: true # Task deprecations ) @@ -200,8 +200,8 @@ end ``` > [!TIP] -> Use task-level settings for tasks that require special handling, such as payment processing, -> external API calls, or critical system operations. +> Use task-level settings for tasks that require special handling, such as financial reporting, +> external API integrations, or critical system operations. ### Registrations @@ -209,25 +209,25 @@ Register middlewares, callbacks, coercions, and validators on a specific task. Deregister options that should not be available. ```ruby -class ProcessPayment < CMDx::Task +class SendCampaignEmail < CMDx::Task # Middlewares register :middleware, CMDx::Middlewares::Timeout - deregister :middleware, MetricsMiddleware + deregister :middleware, AuditTrailMiddleware # Callbacks register :callback, :on_complete, proc { |task| - duration = task.metadata[:runtime] - StatsD.histogram("task.duration", duration, tags: ["class:#{task.class.name}"]) + runtime = task.metadata[:runtime] + Analytics.track("email_campaign.sent", runtime, tags: ["task:#{task.class.name}"]) } - deregister :callback, :before_execution, :setup_request_context + deregister :callback, :before_execution, :initialize_user_session # Coercions - register :coercion, :money, MoneyCoercion - deregister :coercion, :point + register :coercion, :currency, CurrencyCoercion + deregister :coercion, :coordinates # Validators - register :validator, :email, :email_validator - deregister :validator, :phone + register :validator, :username, :username_validator + deregister :validator, :url def work # Your logic here... @@ -246,12 +246,12 @@ CMDx.configuration.task_breakpoints #=> ["failed"] CMDx.configuration.middlewares.registry #=> [, ...] # Task configuration access -class AnalyzeData < CMDx::Task - settings(tags: ["data", "analytics"]) +class ProcessUpload < CMDx::Task + settings(tags: ["files", "storage"]) def work self.class.settings[:logger] #=> Global configuration value - self.class.settings[:tags] #=> Task configuration value => ["data", "analytics"] + self.class.settings[:tags] #=> Task configuration value => ["files", "storage"] end end ``` @@ -283,14 +283,14 @@ end Generate new CMDx tasks quickly using the built-in generator: ```bash -rails generate cmdx:task ProcessOrder +rails generate cmdx:task ModerateBlogPost ``` This creates a new task file with the basic structure: ```ruby -# app/tasks/process_order.rb -class ProcessOrder < CMDx::Task +# app/tasks/moderate_blog_post.rb +class ModerateBlogPost < CMDx::Task def work # Your logic here... end @@ -299,7 +299,7 @@ end > [!TIP] > Use **present tense verbs + noun** for task names, eg: -> `ProcessOrder`, `SendWelcomeEmail`, `ValidatePaymentDetails` +> `ModerateBlogPost`, `ScheduleAppointment`, `ValidateDocument` --- diff --git a/docs/internationalization.md b/docs/internationalization.md index 67dee3875..73d344b8c 100644 --- a/docs/internationalization.md +++ b/docs/internationalization.md @@ -12,8 +12,8 @@ CMDx provides comprehensive internationalization support for all error messages, > CMDx automatically localizes all error messages based on the `I18n.locale` setting. ```ruby -class ProcessOrder < CMDx::Task - attribute :amount, type: :float +class ProcessQuote < CMDx::Task + attribute :price, type: :float def work # Your logic here... @@ -21,8 +21,8 @@ class ProcessOrder < CMDx::Task end I18n.with_locale(:fr) do - result = ProcessOrder.execute(amount: "invalid") - result.metadata[:messages][:amount] #=> ["impossible de contraindre en float"] + result = ProcessQuote.execute(price: "invalid") + result.metadata[:messages][:price] #=> ["impossible de contraindre en float"] end ``` diff --git a/docs/interruptions/exceptions.md b/docs/interruptions/exceptions.md index 971d794fe..f6f0a56f6 100644 --- a/docs/interruptions/exceptions.md +++ b/docs/interruptions/exceptions.md @@ -15,18 +15,18 @@ CMDx provides robust exception handling that differs between the `execute` and ` The `execute` method captures **all** unhandled exceptions and converts them to failed results, ensuring predictable behavior and consistent result processing. ```ruby -class ProcessPayment < CMDx::Task +class ProcessDocument < CMDx::Task def work - raise UnknownPaymentMethod, "unsupported payment method" + raise UnsupportedFormat, "document format not supported" end end -result = ProcessPayment.execute +result = ProcessDocument.execute result.state #=> "interrupted" result.status #=> "failed" result.failed? #=> true -result.reason #=> "[UnknownPaymentMethod] unsupported payment method" -result.cause #=> +result.reason #=> "[UnsupportedFormat] document format not supported" +result.cause #=> ``` ### Bang execution @@ -34,15 +34,15 @@ result.cause #=> The `execute!` method allows unhandled exceptions to propagate, enabling standard Ruby exception handling while respecting CMDx fault configuration. ```ruby -class ProcessPayment < CMDx::Task +class ProcessDocument < CMDx::Task def work - raise UnknownPaymentMethod, "unsupported payment method" + raise UnsupportedFormat, "document format not supported" end end begin - ProcessPayment.execute! -rescue UnknownPaymentMethod => e + ProcessDocument.execute! +rescue UnsupportedFormat => e puts "Handle exception: #{e.message}" end ``` diff --git a/docs/interruptions/faults.md b/docs/interruptions/faults.md index a4e511d9f..e14beb43f 100644 --- a/docs/interruptions/faults.md +++ b/docs/interruptions/faults.md @@ -30,16 +30,16 @@ Faults are exception mechanisms that halt task execution via `skip!` and `fail!` ```ruby begin - ProcessOrder.execute!(order_id: 123) + ProcessTicket.execute!(ticket_id: 456) rescue CMDx::SkipFault => e - logger.info "Order processing skipped: #{e.message}" - schedule_retry(e.context.order_id) + logger.info "Ticket processing skipped: #{e.message}" + schedule_retry(e.context.ticket_id) rescue CMDx::FailFault => e - logger.error "Order processing failed: #{e.message}" - notify_customer(e.context.customer_email, e.result.metadata[:code]) + logger.error "Ticket processing failed: #{e.message}" + notify_admin(e.context.assigned_agent, e.result.metadata[:error_code]) rescue CMDx::Fault => e - logger.warn "Order processing interrupted: #{e.message}" - rollback_transaction + logger.warn "Ticket processing interrupted: #{e.message}" + rollback_changes end ``` @@ -49,20 +49,20 @@ Faults provide comprehensive access to execution context, eg: ```ruby begin - UserRegistration.execute!(email: email, password: password) + LicenseActivation.execute!(license_key: key, machine_id: machine) rescue CMDx::Fault => e # Result information e.result.state #=> "interrupted" e.result.status #=> "failed" or "skipped" - e.result.reason #=> "Email already exists" + e.result.reason #=> "License key already activated" # Task information - e.task.class #=> + e.task.class #=> e.task.id #=> "abc123..." # Context data - e.context.email #=> "user@example.com" - e.context.password #=> "[FILTERED]" + e.context.license_key #=> "ABC-123-DEF" + e.context.machine_id #=> "[FILTERED]" # Chain information e.chain.id #=> "def456..." @@ -78,13 +78,13 @@ Use `for?` to handle faults only from specific task classes, enabling targeted e ```ruby begin - PaymentWorkflow.execute!(payment_data: data) -rescue CMDx::FailFault.for?(CardValidator, PaymentProcessor) => e - # Handle only payment-related failures - retry_with_backup_method(e.context) -rescue CMDx::SkipFault.for?(FraudCheck, RiskAssessment) => e + DocumentWorkflow.execute!(document_data: data) +rescue CMDx::FailFault.for?(FormatValidator, ContentProcessor) => e + # Handle only document-related failures + retry_with_alternate_parser(e.context) +rescue CMDx::SkipFault.for?(VirusScanner, ContentFilter) => e # Handle security-related skips - flag_for_manual_review(e.context.transaction_id) + quarantine_for_review(e.context.document_id) end ``` @@ -92,13 +92,13 @@ end ```ruby begin - OrderProcessor.execute!(order: order_data) -rescue CMDx::Fault.matches? { |f| f.context.order_value > 1000 } => e - escalate_high_value_failure(e) -rescue CMDx::FailFault.matches? { |f| f.result.metadata[:retry_count] > 3 } => e - abandon_processing(e) -rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "timeout" } => e - increase_timeout_and_retry(e) + ReportGenerator.execute!(report: report_data) +rescue CMDx::Fault.matches? { |f| f.context.data_size > 10_000 } => e + escalate_large_dataset_failure(e) +rescue CMDx::FailFault.matches? { |f| f.result.metadata[:attempt_count] > 3 } => e + abandon_report_generation(e) +rescue CMDx::Fault.matches? { |f| f.result.metadata[:error_type] == "memory" } => e + increase_memory_and_retry(e) end ``` @@ -109,22 +109,22 @@ Use `throw!` to propagate failures while preserving fault context and maintainin ### Basic Propagation ```ruby -class OrderProcessor < CMDx::Task +class ReportGenerator < CMDx::Task def work # Throw if skipped or failed - validation_result = OrderValidator.execute(context) + validation_result = DataValidator.execute(context) throw!(validation_result) # Only throw if skipped - check_inventory = CheckInventory.execute(context) - throw!(check_inventory) if check_inventory.skipped? + check_permissions = CheckPermissions.execute(context) + throw!(check_permissions) if check_permissions.skipped? # Only throw if failed - payment_result = PaymentProcessor.execute(context) - throw!(payment_result) if payment_result.failed? + data_result = DataProcessor.execute(context) + throw!(data_result) if data_result.failed? # Continue processing - complete_order + generate_report end end ``` @@ -132,19 +132,19 @@ end ### Additional Metadata ```ruby -class WorkflowProcessor < CMDx::Task +class BatchProcessor < CMDx::Task def work - step_result = DataValidation.execute(context) + step_result = FileValidation.execute(context) if step_result.failed? throw!(step_result, { - workflow_stage: "validation", + batch_stage: "validation", can_retry: true, - next_step: "data_cleanup" + next_step: "file_repair" }) end - continue_workflow + continue_batch end end ``` @@ -154,7 +154,7 @@ end Results provide methods to analyze fault propagation and identify original failure sources in complex execution chains. ```ruby -result = PaymentWorkflow.execute(invalid_data) +result = DocumentWorkflow.execute(invalid_data) if result.failed? # Trace the original failure diff --git a/docs/interruptions/halt.md b/docs/interruptions/halt.md index 59a3ba54a..2ad892a61 100644 --- a/docs/interruptions/halt.md +++ b/docs/interruptions/halt.md @@ -21,25 +21,25 @@ The `skip!` method indicates a task did not meet criteria to continue execution. > Skipping is not a failure or error. Skipped tasks are considered successful outcomes. ```ruby -class ProcessOrder < CMDx::Task +class ProcessInventory < CMDx::Task def work # Without a reason - skip! if Array(ENV["PHASED_OUT_TASKS"]).include?(self.class.name) + skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name) # With a reason - skip!("Outside business hours") unless Time.now.hour.between?(9, 17) + skip!("Warehouse closed") unless Time.now.hour.between?(8, 18) - order = Order.find(context.order_id) + inventory = Inventory.find(context.inventory_id) - if order.processed? - skip!("Order already processed") + if inventory.already_counted? + skip!("Inventory already counted today") else - order.process! + inventory.count! end end end -result = ProcessSubscription.execute(user_id: 123) +result = ProcessInventory.execute(inventory_id: 456) # Executed result.status #=> "skipped" @@ -48,7 +48,7 @@ result.status #=> "skipped" result.reason #=> "no reason given" # With a reason -result.reason #=> "Outside business hours" +result.reason #=> "Warehouse closed" ``` ## Failing @@ -56,25 +56,25 @@ result.reason #=> "Outside business hours" The `fail!` method indicates a task encountered an error condition that prevents successful completion. This represents controlled failure where the task explicitly determines that execution cannot continue. ```ruby -class ProcessPayment < CMDx::Task +class ProcessRefund < CMDx::Task def work # Without a reason - skip! if Array(ENV["PHASED_OUT_TASKS"]).include?(self.class.name) + skip! if Array(ENV["DISABLED_TASKS"]).include?(self.class.name) - payment = Payment.find(context.payment_id) + refund = Refund.find(context.refund_id) # With a reason - if payment.unsupported_type? - fail!("Unsupported payment type") - elsif !payment.amount.positive? - fail!("Payment amount must be positive") + if refund.expired? + fail!("Refund period has expired") + elsif !refund.amount.positive? + fail!("Refund amount must be positive") else - payment.charge! + refund.process! end end end -result = ProcessSubscription.execute(user_id: 123) +result = ProcessRefund.execute(refund_id: 789) # Executed result.status #=> "failed" @@ -83,7 +83,7 @@ result.status #=> "failed" result.reason #=> "no reason given" # With a reason -result.reason #=> "Unsupported payment type" +result.reason #=> "Refund period has expired" ``` ## Metadata Enrichment @@ -91,37 +91,37 @@ result.reason #=> "Unsupported payment type" 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. ```ruby -class ProcessSubscription < CMDx::Task +class ProcessRenewal < CMDx::Task def work - user = User.find(context.user_id) + license = License.find(context.license_id) - if user.subscription_expired? + if license.already_renewed? # Without metadata - skip!("Subscription expired") + skip!("License already renewed") end - unless user.payment_method_valid? + unless license.renewal_eligible? # With metadata fail!( - "Invalid payment method", - error_code: "PAYMENT_METHOD.INVALID", - retry_after: Time.current + 1.hour + "License not eligible for renewal", + error_code: "LICENSE.NOT_ELIGIBLE", + retry_after: Time.current + 30.days ) end - process_subscription + process_renewal end end -result = ProcessSubscription.execute(user_id: 123) +result = ProcessRenewal.execute(license_id: 567) # Without metadata result.metadata #=> {} # With metadata result.metadata #=> { - # error_code: "PAYMENT_METHOD.INVALID", - # retry_after: