cmdx 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.DS_Store +0 -0
- data/.cursor/prompts/docs.md +3 -3
- data/.cursor/prompts/llms.md +1 -3
- data/.cursor/prompts/yardoc.md +1 -0
- data/.irbrc +14 -2
- data/CHANGELOG.md +64 -45
- data/LLM.md +159 -53
- data/README.md +26 -83
- data/docs/.DS_Store +0 -0
- data/docs/assets/favicon.ico +0 -0
- data/docs/assets/favicon.svg +1 -0
- data/docs/attributes/coercions.md +12 -24
- data/docs/attributes/defaults.md +3 -16
- data/docs/attributes/definitions.md +16 -30
- data/docs/attributes/naming.md +3 -13
- data/docs/attributes/transformations.md +63 -0
- data/docs/attributes/validations.md +14 -33
- data/docs/basics/chain.md +14 -23
- data/docs/basics/context.md +13 -22
- data/docs/basics/execution.md +8 -26
- data/docs/basics/setup.md +8 -19
- data/docs/callbacks.md +19 -32
- data/docs/deprecation.md +8 -25
- data/docs/getting_started.md +109 -76
- data/docs/index.md +132 -0
- data/docs/internationalization.md +6 -18
- data/docs/interruptions/exceptions.md +10 -16
- data/docs/interruptions/faults.md +8 -25
- data/docs/interruptions/halt.md +12 -27
- data/docs/logging.md +7 -17
- data/docs/middlewares.md +13 -29
- data/docs/outcomes/result.md +21 -38
- data/docs/outcomes/states.md +8 -22
- data/docs/outcomes/statuses.md +10 -21
- data/docs/stylesheets/extra.css +42 -0
- data/docs/tips_and_tricks.md +7 -46
- data/docs/workflows.md +23 -38
- data/examples/active_record_query_tagging.md +46 -0
- data/examples/paper_trail_whatdunnit.md +39 -0
- data/lib/cmdx/attribute.rb +88 -6
- data/lib/cmdx/attribute_registry.rb +20 -0
- data/lib/cmdx/attribute_value.rb +56 -10
- data/lib/cmdx/callback_registry.rb +31 -2
- data/lib/cmdx/chain.rb +34 -1
- data/lib/cmdx/coercion_registry.rb +18 -0
- data/lib/cmdx/coercions/array.rb +2 -0
- data/lib/cmdx/coercions/big_decimal.rb +3 -0
- data/lib/cmdx/coercions/boolean.rb +5 -0
- data/lib/cmdx/coercions/complex.rb +2 -0
- data/lib/cmdx/coercions/date.rb +4 -0
- data/lib/cmdx/coercions/date_time.rb +5 -0
- data/lib/cmdx/coercions/float.rb +2 -0
- data/lib/cmdx/coercions/hash.rb +4 -0
- data/lib/cmdx/coercions/integer.rb +2 -0
- data/lib/cmdx/coercions/rational.rb +2 -0
- data/lib/cmdx/coercions/string.rb +2 -0
- data/lib/cmdx/coercions/symbol.rb +2 -0
- data/lib/cmdx/coercions/time.rb +5 -0
- data/lib/cmdx/configuration.rb +119 -3
- data/lib/cmdx/context.rb +36 -0
- data/lib/cmdx/deprecator.rb +6 -3
- data/lib/cmdx/errors.rb +22 -0
- data/lib/cmdx/executor.rb +136 -7
- data/lib/cmdx/faults.rb +14 -0
- data/lib/cmdx/identifier.rb +2 -0
- data/lib/cmdx/locale.rb +3 -0
- data/lib/cmdx/log_formatters/json.rb +2 -0
- data/lib/cmdx/log_formatters/key_value.rb +2 -0
- data/lib/cmdx/log_formatters/line.rb +2 -0
- data/lib/cmdx/log_formatters/logstash.rb +2 -0
- data/lib/cmdx/log_formatters/raw.rb +2 -0
- data/lib/cmdx/middleware_registry.rb +20 -0
- data/lib/cmdx/middlewares/correlate.rb +11 -0
- data/lib/cmdx/middlewares/runtime.rb +4 -0
- data/lib/cmdx/middlewares/timeout.rb +4 -0
- data/lib/cmdx/pipeline.rb +24 -5
- data/lib/cmdx/railtie.rb +13 -0
- data/lib/cmdx/result.rb +133 -2
- data/lib/cmdx/task.rb +103 -8
- data/lib/cmdx/utils/call.rb +2 -0
- data/lib/cmdx/utils/condition.rb +3 -0
- data/lib/cmdx/utils/format.rb +5 -0
- data/lib/cmdx/validator_registry.rb +18 -0
- data/lib/cmdx/validators/exclusion.rb +2 -0
- data/lib/cmdx/validators/format.rb +2 -0
- data/lib/cmdx/validators/inclusion.rb +2 -0
- data/lib/cmdx/validators/length.rb +14 -0
- data/lib/cmdx/validators/numeric.rb +14 -0
- data/lib/cmdx/validators/presence.rb +2 -0
- data/lib/cmdx/version.rb +4 -1
- data/lib/cmdx/workflow.rb +10 -0
- data/lib/cmdx.rb +9 -0
- data/lib/generators/cmdx/locale_generator.rb +0 -1
- data/lib/generators/cmdx/templates/install.rb +9 -0
- data/mkdocs.yml +122 -0
- data/src/cmdx-dark-logo.png +0 -0
- data/src/cmdx-favicon.svg +1 -0
- data/src/cmdx-light-logo.png +0 -0
- data/src/cmdx-logo.svg +1 -0
- metadata +14 -3
- data/lib/cmdx/freezer.rb +0 -51
- data/src/cmdx-logo.png +0 -0
    
        data/docs/outcomes/states.md
    CHANGED
    
    | @@ -1,16 +1,6 @@ | |
| 1 1 | 
             
            # Outcomes - States
         | 
| 2 2 |  | 
| 3 | 
            -
            States  | 
| 4 | 
            -
            the progress of tasks through their complete execution journey. States provide
         | 
| 5 | 
            -
            insight into where a task is in its lifecycle and enable lifecycle-based
         | 
| 6 | 
            -
            decision making and monitoring.
         | 
| 7 | 
            -
             | 
| 8 | 
            -
            ## Table of Contents
         | 
| 9 | 
            -
             | 
| 10 | 
            -
            - [Definitions](#definitions)
         | 
| 11 | 
            -
            - [Transitions](#transitions)
         | 
| 12 | 
            -
            - [Predicates](#predicates)
         | 
| 13 | 
            -
            - [Handlers](#handlers)
         | 
| 3 | 
            +
            States track where a task is in its execution lifecycle—from creation through completion or interruption.
         | 
| 14 4 |  | 
| 15 5 | 
             
            ## Definitions
         | 
| 16 6 |  | 
| @@ -34,8 +24,9 @@ State-Status combinations: | |
| 34 24 |  | 
| 35 25 | 
             
            ## Transitions
         | 
| 36 26 |  | 
| 37 | 
            -
             | 
| 38 | 
            -
             | 
| 27 | 
            +
            !!! danger "Caution"
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                States are managed automatically—never modify them manually.
         | 
| 39 30 |  | 
| 40 31 | 
             
            ```ruby
         | 
| 41 32 | 
             
            # Valid state transition flow
         | 
| @@ -62,19 +53,14 @@ result.executed?    #=> true (complete OR interrupted) | |
| 62 53 |  | 
| 63 54 | 
             
            ## Handlers
         | 
| 64 55 |  | 
| 65 | 
            -
             | 
| 56 | 
            +
            Handle lifecycle events with state-based handlers. Use `handle_executed` for cleanup that runs regardless of outcome:
         | 
| 66 57 |  | 
| 67 58 | 
             
            ```ruby
         | 
| 68 59 | 
             
            result = ProcessVideoUpload.execute
         | 
| 69 60 |  | 
| 70 61 | 
             
            # Individual state handlers
         | 
| 71 62 | 
             
            result
         | 
| 72 | 
            -
              . | 
| 73 | 
            -
              . | 
| 74 | 
            -
              . | 
| 63 | 
            +
              .handle_complete { |result| send_upload_notification(result) }
         | 
| 64 | 
            +
              .handle_interrupted { |result| cleanup_temp_files(result) }
         | 
| 65 | 
            +
              .handle_executed { |result| log_upload_metrics(result) }
         | 
| 75 66 | 
             
            ```
         | 
| 76 | 
            -
             | 
| 77 | 
            -
            ---
         | 
| 78 | 
            -
             | 
| 79 | 
            -
            - **Prev:** [Outcomes - Result](result.md)
         | 
| 80 | 
            -
            - **Next:** [Outcomes - Statuses](statuses.md)
         | 
    
        data/docs/outcomes/statuses.md
    CHANGED
    
    | @@ -1,13 +1,6 @@ | |
| 1 1 | 
             
            # Outcomes - Statuses
         | 
| 2 2 |  | 
| 3 | 
            -
            Statuses represent the business outcome  | 
| 4 | 
            -
             | 
| 5 | 
            -
            ## Table of Contents
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            - [Definitions](#definitions)
         | 
| 8 | 
            -
            - [Transitions](#transitions)
         | 
| 9 | 
            -
            - [Predicates](#predicates)
         | 
| 10 | 
            -
            - [Handlers](#handlers)
         | 
| 3 | 
            +
            Statuses represent the business outcome—did the task succeed, skip, or fail? This differs from state, which tracks the execution lifecycle.
         | 
| 11 4 |  | 
| 12 5 | 
             
            ## Definitions
         | 
| 13 6 |  | 
| @@ -19,8 +12,9 @@ Statuses represent the business outcome of task execution logic, indicating how | |
| 19 12 |  | 
| 20 13 | 
             
            ## Transitions
         | 
| 21 14 |  | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 15 | 
            +
            !!! warning "Important"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                Status transitions are final and unidirectional. Once skipped or failed, tasks can't return to success.
         | 
| 24 18 |  | 
| 25 19 | 
             
            ```ruby
         | 
| 26 20 | 
             
            # Valid status transitions
         | 
| @@ -53,24 +47,19 @@ result.bad?     #=> true if skipped OR failed (not success) | |
| 53 47 |  | 
| 54 48 | 
             
            ## Handlers
         | 
| 55 49 |  | 
| 56 | 
            -
             | 
| 50 | 
            +
            Branch business logic with status-based handlers. Use `handle_good` and `handle_bad` for success/skip vs failed outcomes:
         | 
| 57 51 |  | 
| 58 52 | 
             
            ```ruby
         | 
| 59 53 | 
             
            result = ProcessNotification.execute
         | 
| 60 54 |  | 
| 61 55 | 
             
            # Individual status handlers
         | 
| 62 56 | 
             
            result
         | 
| 63 | 
            -
              . | 
| 64 | 
            -
              . | 
| 65 | 
            -
              . | 
| 57 | 
            +
              .handle_success { |result| mark_notification_sent(result) }
         | 
| 58 | 
            +
              .handle_skipped { |result| log_notification_skipped(result) }
         | 
| 59 | 
            +
              .handle_failed { |result| queue_retry_notification(result) }
         | 
| 66 60 |  | 
| 67 61 | 
             
            # Outcome-based handlers
         | 
| 68 62 | 
             
            result
         | 
| 69 | 
            -
              . | 
| 70 | 
            -
              . | 
| 63 | 
            +
              .handle_good { |result| update_message_stats(result) }
         | 
| 64 | 
            +
              .handle_bad { |result| track_delivery_failure(result) }
         | 
| 71 65 | 
             
            ```
         | 
| 72 | 
            -
             | 
| 73 | 
            -
            ---
         | 
| 74 | 
            -
             | 
| 75 | 
            -
            - **Prev:** [Outcomes - States](states.md)
         | 
| 76 | 
            -
            - **Next:** [Attributes - Definitions](../attributes/definitions.md)
         | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            :root  > * {
         | 
| 2 | 
            +
              /* Primary color shades */
         | 
| 3 | 
            +
              --md-primary-fg-color:        #fe1817;
         | 
| 4 | 
            +
              --md-primary-fg-color--light: #fe1817;
         | 
| 5 | 
            +
              --md-primary-fg-color--dark:  #fe1817;
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              /* Accent color shades */
         | 
| 8 | 
            +
              --md-accent-fg-color:                hsla(#{hex2hsl(#fe1817)}, 1);
         | 
| 9 | 
            +
              --md-accent-fg-color--transparent:   hsla(#{hex2hsl(#fe1817)}, 0.1);
         | 
| 10 | 
            +
            }
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            /* Atom One Light Pro syntax highlighting */
         | 
| 13 | 
            +
            [data-md-color-scheme="default"] {
         | 
| 14 | 
            +
              --md-code-hl-color:            #2c3036;
         | 
| 15 | 
            +
              --md-code-hl-keyword-color:    #a626a4;
         | 
| 16 | 
            +
              --md-code-hl-string-color:     #50a14f;
         | 
| 17 | 
            +
              --md-code-hl-name-color:       #e4564a;
         | 
| 18 | 
            +
              --md-code-hl-function-color:   #4078f2;
         | 
| 19 | 
            +
              --md-code-hl-number-color:     #ca7601;
         | 
| 20 | 
            +
              --md-code-hl-constant-color:   #c18401;
         | 
| 21 | 
            +
              --md-code-hl-comment-color:    #9ca0a4;
         | 
| 22 | 
            +
              --md-code-hl-operator-color:   #0184bc;
         | 
| 23 | 
            +
              --md-code-hl-punctuation-color:#383a42;
         | 
| 24 | 
            +
              --md-code-hl-variable-color:   #e4564a;
         | 
| 25 | 
            +
              --md-code-hl-generic-color:    #e4564a;
         | 
| 26 | 
            +
            }
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            /* Atom One Dark Pro syntax highlighting */
         | 
| 29 | 
            +
            [data-md-color-scheme="slate"] {
         | 
| 30 | 
            +
              --md-code-hl-color:            #e5e5e6;
         | 
| 31 | 
            +
              --md-code-hl-keyword-color:    #c678dd;
         | 
| 32 | 
            +
              --md-code-hl-string-color:     #98c379;
         | 
| 33 | 
            +
              --md-code-hl-name-color:       #e06c75;
         | 
| 34 | 
            +
              --md-code-hl-function-color:   #61afef;
         | 
| 35 | 
            +
              --md-code-hl-number-color:     #d19a66;
         | 
| 36 | 
            +
              --md-code-hl-constant-color:   #d19a66;
         | 
| 37 | 
            +
              --md-code-hl-comment-color:    #7f848e;
         | 
| 38 | 
            +
              --md-code-hl-operator-color:   #56b6c2;
         | 
| 39 | 
            +
              --md-code-hl-punctuation-color:#abb2bf;
         | 
| 40 | 
            +
              --md-code-hl-variable-color:   #e06c75;
         | 
| 41 | 
            +
              --md-code-hl-generic-color:    #e06c75;
         | 
| 42 | 
            +
            }
         | 
    
        data/docs/tips_and_tricks.md
    CHANGED
    
    | @@ -1,16 +1,6 @@ | |
| 1 1 | 
             
            # Tips and Tricks
         | 
| 2 2 |  | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
            ## Table of Contents
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            - [Project Organization](#project-organization)
         | 
| 8 | 
            -
              - [Directory Structure](#directory-structure)
         | 
| 9 | 
            -
              - [Naming Conventions](#naming-conventions)
         | 
| 10 | 
            -
              - [Story Telling](#story-telling)
         | 
| 11 | 
            -
              - [Style Guide](#style-guide)
         | 
| 12 | 
            -
            - [Attribute Options](#attribute-options)
         | 
| 13 | 
            -
            - [ActiveRecord Query Tagging](#activerecord-query-tagging)
         | 
| 3 | 
            +
            Best practices, patterns, and techniques to build maintainable CMDx applications.
         | 
| 14 4 |  | 
| 15 5 | 
             
            ## Project Organization
         | 
| 16 6 |  | 
| @@ -54,7 +44,7 @@ class TokenGeneration < CMDx::Task; end    # ❌ Avoid | |
| 54 44 |  | 
| 55 45 | 
             
            ### Story Telling
         | 
| 56 46 |  | 
| 57 | 
            -
             | 
| 47 | 
            +
            Break down complex logic into descriptive methods that read like a narrative:
         | 
| 58 48 |  | 
| 59 49 | 
             
            ```ruby
         | 
| 60 50 | 
             
            class ProcessOrder < CMDx::Task
         | 
| @@ -86,7 +76,7 @@ end | |
| 86 76 |  | 
| 87 77 | 
             
            ### Style Guide
         | 
| 88 78 |  | 
| 89 | 
            -
            Follow  | 
| 79 | 
            +
            Follow this order for consistent, readable tasks:
         | 
| 90 80 |  | 
| 91 81 | 
             
            ```ruby
         | 
| 92 82 | 
             
            class ExportReport < CMDx::Task
         | 
| @@ -129,7 +119,7 @@ end | |
| 129 119 |  | 
| 130 120 | 
             
            ## Attribute Options
         | 
| 131 121 |  | 
| 132 | 
            -
            Use  | 
| 122 | 
            +
            Use `with_options` to reduce duplication:
         | 
| 133 123 |  | 
| 134 124 | 
             
            ```ruby
         | 
| 135 125 | 
             
            class ConfigureCompany < CMDx::Task
         | 
| @@ -155,36 +145,7 @@ class ConfigureCompany < CMDx::Task | |
| 155 145 | 
             
            end
         | 
| 156 146 | 
             
            ```
         | 
| 157 147 |  | 
| 158 | 
            -
            ##  | 
| 159 | 
            -
             | 
| 160 | 
            -
            Automatically tag SQL queries for better debugging:
         | 
| 161 | 
            -
             | 
| 162 | 
            -
            ```ruby
         | 
| 163 | 
            -
            # config/application.rb
         | 
| 164 | 
            -
            config.active_record.query_log_tags_enabled = true
         | 
| 165 | 
            -
            config.active_record.query_log_tags << :cmdx_task_class
         | 
| 166 | 
            -
            config.active_record.query_log_tags << :cmdx_chain_id
         | 
| 167 | 
            -
             | 
| 168 | 
            -
            # app/tasks/application_task.rb
         | 
| 169 | 
            -
            class ApplicationTask < CMDx::Task
         | 
| 170 | 
            -
              before_execution :set_execution_context
         | 
| 171 | 
            -
             | 
| 172 | 
            -
              private
         | 
| 173 | 
            -
             | 
| 174 | 
            -
              def set_execution_context
         | 
| 175 | 
            -
                # NOTE: This could easily be made into a middleware
         | 
| 176 | 
            -
                ActiveSupport::ExecutionContext.set(
         | 
| 177 | 
            -
                  cmdx_task_class: self.class.name,
         | 
| 178 | 
            -
                  cmdx_chain_id: chain.id
         | 
| 179 | 
            -
                )
         | 
| 180 | 
            -
              end
         | 
| 181 | 
            -
            end
         | 
| 182 | 
            -
             | 
| 183 | 
            -
            # SQL queries will now include comments like:
         | 
| 184 | 
            -
            # /*cmdx_task_class:ExportReportTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM reports WHERE id = 1
         | 
| 185 | 
            -
            ```
         | 
| 186 | 
            -
             | 
| 187 | 
            -
            ---
         | 
| 148 | 
            +
            ## Advanced Examples
         | 
| 188 149 |  | 
| 189 | 
            -
            -  | 
| 190 | 
            -
            -  | 
| 150 | 
            +
            - [Active Record Query Tagging](https://github.com/drexed/cmdx/blob/main/examples/active_record_query_tagging.md)
         | 
| 151 | 
            +
            - [Paper Trail Whatdunnit](https://github.com/drexed/cmdx/blob/main/examples/paper_trail_whatdunnit.md)
         | 
    
        data/docs/workflows.md
    CHANGED
    
    | @@ -1,26 +1,14 @@ | |
| 1 1 | 
             
            # Workflows
         | 
| 2 2 |  | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
            ## Table of Contents
         | 
| 6 | 
            -
             | 
| 7 | 
            -
            - [Declarations](#declarations)
         | 
| 8 | 
            -
              - [Task](#task)
         | 
| 9 | 
            -
              - [Group](#group)
         | 
| 10 | 
            -
              - [Conditionals](#conditionals)
         | 
| 11 | 
            -
            - [Halt Behavior](#halt-behavior)
         | 
| 12 | 
            -
              - [Task Configuration](#task-configuration)
         | 
| 13 | 
            -
              - [Group Configuration](#group-configuration)
         | 
| 14 | 
            -
            - [Nested Workflows](#nested-workflows)
         | 
| 15 | 
            -
            - [Parallel Execution](#parallel-execution)
         | 
| 16 | 
            -
            - [Task Generator](#task-generator)
         | 
| 3 | 
            +
            Compose multiple tasks into powerful, sequential pipelines. Workflows provide a declarative way to build complex business processes with conditional execution, shared context, and flexible error handling.
         | 
| 17 4 |  | 
| 18 5 | 
             
            ## Declarations
         | 
| 19 6 |  | 
| 20 | 
            -
            Tasks  | 
| 7 | 
            +
            Tasks run in declaration order (FIFO), sharing a common context across the pipeline.
         | 
| 21 8 |  | 
| 22 | 
            -
             | 
| 23 | 
            -
             | 
| 9 | 
            +
            !!! warning
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                Don't define a `work` method in workflows—the module handles execution automatically.
         | 
| 24 12 |  | 
| 25 13 | 
             
            ### Task
         | 
| 26 14 |  | 
| @@ -35,15 +23,17 @@ class OnboardingWorkflow < CMDx::Task | |
| 35 23 | 
             
            end
         | 
| 36 24 | 
             
            ```
         | 
| 37 25 |  | 
| 38 | 
            -
             | 
| 39 | 
            -
             | 
| 26 | 
            +
            !!! tip
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                Execute tasks in parallel via the [cmdx-parallel](https://github.com/drexed/cmdx-parallel) gem.
         | 
| 40 29 |  | 
| 41 30 | 
             
            ### Group
         | 
| 42 31 |  | 
| 43 | 
            -
            Group related tasks  | 
| 32 | 
            +
            Group related tasks to share configuration:
         | 
| 44 33 |  | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 34 | 
            +
            !!! warning "Important"
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                Settings and conditionals apply to all tasks in the group.
         | 
| 47 37 |  | 
| 48 38 | 
             
            ```ruby
         | 
| 49 39 | 
             
            class ContentModerationWorkflow < CMDx::Task
         | 
| @@ -78,10 +68,10 @@ class OnboardingWorkflow < CMDx::Task | |
| 78 68 | 
             
              task SendWelcomeEmail, if: :email_configured?, unless: :email_disabled?
         | 
| 79 69 |  | 
| 80 70 | 
             
              # Proc
         | 
| 81 | 
            -
              task SendWelcomeEmail, if: -> | 
| 71 | 
            +
              task SendWelcomeEmail, if: -> { Rails.env.production? && self.class.name.include?("Premium") }
         | 
| 82 72 |  | 
| 83 73 | 
             
              # Lambda
         | 
| 84 | 
            -
              task SendWelcomeEmail, if: proc {  | 
| 74 | 
            +
              task SendWelcomeEmail, if: proc { context.features_enabled? }
         | 
| 85 75 |  | 
| 86 76 | 
             
              # Class or Module
         | 
| 87 77 | 
             
              task SendWelcomeEmail, unless: ContentAccessCheck
         | 
| @@ -95,7 +85,7 @@ class OnboardingWorkflow < CMDx::Task | |
| 95 85 | 
             
              private
         | 
| 96 86 |  | 
| 97 87 | 
             
              def email_configured?
         | 
| 98 | 
            -
                context.user.email_address | 
| 88 | 
            +
                context.user.email_address == true
         | 
| 99 89 | 
             
              end
         | 
| 100 90 |  | 
| 101 91 | 
             
              def email_disabled?
         | 
| @@ -106,9 +96,7 @@ end | |
| 106 96 |  | 
| 107 97 | 
             
            ## Halt Behavior
         | 
| 108 98 |  | 
| 109 | 
            -
            By default skipped tasks  | 
| 110 | 
            -
            This is configurable via global and task level breakpoint settings. Task and group configurations
         | 
| 111 | 
            -
            can be used together within a workflow.
         | 
| 99 | 
            +
            By default, skipped tasks don't stop the workflow—they're treated as no-ops. Configure breakpoints globally or per-task to customize this behavior.
         | 
| 112 100 |  | 
| 113 101 | 
             
            ```ruby
         | 
| 114 102 | 
             
            class AnalyticsWorkflow < CMDx::Task
         | 
| @@ -164,7 +152,7 @@ end | |
| 164 152 |  | 
| 165 153 | 
             
            ## Nested Workflows
         | 
| 166 154 |  | 
| 167 | 
            -
             | 
| 155 | 
            +
            Build hierarchical workflows by composing workflows within workflows:
         | 
| 168 156 |  | 
| 169 157 | 
             
            ```ruby
         | 
| 170 158 | 
             
            class EmailPreparationWorkflow < CMDx::Task
         | 
| @@ -191,10 +179,11 @@ end | |
| 191 179 |  | 
| 192 180 | 
             
            ## Parallel Execution
         | 
| 193 181 |  | 
| 194 | 
            -
             | 
| 182 | 
            +
            Run tasks concurrently using the [Parallel](https://github.com/grosser/parallel) gem. It automatically uses all available processors for maximum throughput.
         | 
| 183 | 
            +
             | 
| 184 | 
            +
            !!! warning
         | 
| 195 185 |  | 
| 196 | 
            -
             | 
| 197 | 
            -
            > Context cannot be modified during parallel execution. Ensure that all required data is preloaded into the context before parallelization begins.
         | 
| 186 | 
            +
                Context is read-only during parallel execution. Load all required data beforehand.
         | 
| 198 187 |  | 
| 199 188 | 
             
            ```ruby
         | 
| 200 189 | 
             
            class SendWelcomeNotifications < CMDx::Task
         | 
| @@ -232,10 +221,6 @@ class SendNotifications < CMDx::Task | |
| 232 221 | 
             
            end
         | 
| 233 222 | 
             
            ```
         | 
| 234 223 |  | 
| 235 | 
            -
             | 
| 236 | 
            -
            > Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
         | 
| 237 | 
            -
             | 
| 238 | 
            -
            ---
         | 
| 224 | 
            +
            !!! tip
         | 
| 239 225 |  | 
| 240 | 
            -
             | 
| 241 | 
            -
            - **Next:** [Tips and Tricks](tips_and_tricks.md)
         | 
| 226 | 
            +
                Use **present tense verbs + pluralized noun** for workflow task names, eg: `SendNotifications`, `DownloadFiles`, `ValidateDocuments`
         | 
| @@ -0,0 +1,46 @@ | |
| 1 | 
            +
            # Active Record Query Tagging
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Add a comment to every query indicating some context to help you track down where that query came from, eg:
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ```sh
         | 
| 6 | 
            +
            /*cmdx_task_class:ExportReportTask,cmdx_chain_id:018c2b95-b764-7615*/ SELECT * FROM reports WHERE id = 1
         | 
| 7 | 
            +
            ```
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ### Setup
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            ```ruby
         | 
| 12 | 
            +
            # config/application.rb
         | 
| 13 | 
            +
            config.active_record.query_log_tags_enabled = true
         | 
| 14 | 
            +
            config.active_record.query_log_tags += [
         | 
| 15 | 
            +
              :cmdx_correlation_id,
         | 
| 16 | 
            +
              :cmdx_chain_id,
         | 
| 17 | 
            +
              :cmdx_task_class,
         | 
| 18 | 
            +
              :cmdx_task_id
         | 
| 19 | 
            +
            ]
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            # lib/cmdx_query_tagging_middleware.rb
         | 
| 22 | 
            +
            class CmdxQueryTaggingMiddleware
         | 
| 23 | 
            +
              def self.call(task, **options, &)
         | 
| 24 | 
            +
                ActiveSupport::ExecutionContext.set(
         | 
| 25 | 
            +
                  cmdx_correlation_id: task.result.metadata[:correlation_id],
         | 
| 26 | 
            +
                  cmdx_chain_id: task.chain.id,
         | 
| 27 | 
            +
                  cmdx_task_class: task.class.name,
         | 
| 28 | 
            +
                  cmdx_task_id: task.id,
         | 
| 29 | 
            +
                  &
         | 
| 30 | 
            +
                )
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         | 
| 33 | 
            +
            ```
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            ### Usage
         | 
| 36 | 
            +
             | 
| 37 | 
            +
            ```ruby
         | 
| 38 | 
            +
            class MyTask < CMDx::Task
         | 
| 39 | 
            +
              register :middleware, CmdxQueryTaggingMiddleware
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              def work
         | 
| 42 | 
            +
                # Do work...
         | 
| 43 | 
            +
              end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
            end
         | 
| 46 | 
            +
            ```
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            # Paper Trail Whatdunnit
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Tag paper trail version records with which service made a change with a custom `whatdunnit` attribute.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            <https://github.com/paper-trail-gem/paper_trail?tab=readme-ov-file#4c-storing-metadata>
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ### Setup
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ```ruby
         | 
| 10 | 
            +
            # lib/cmdx_paper_trail_middleware.rb
         | 
| 11 | 
            +
            class CmdxPaperTrailMiddleware
         | 
| 12 | 
            +
              def self.call(task, **options, &)
         | 
| 13 | 
            +
                # This makes sure to reset the whatdunnit value to the previous
         | 
| 14 | 
            +
                # value for nested task calls
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                begin
         | 
| 17 | 
            +
                  PaperTrail.request.controller_info ||= {}
         | 
| 18 | 
            +
                  old_whatdunnit = PaperTrail.request.controller_info[:whatdunnit]
         | 
| 19 | 
            +
                  PaperTrail.request.controller_info[:whatdunnit] = task.class.name
         | 
| 20 | 
            +
                  yield
         | 
| 21 | 
            +
                ensure
         | 
| 22 | 
            +
                  PaperTrail.request.controller_info[:whatdunnit] = old_whatdunnit
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| 26 | 
            +
            ```
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            ### Usage
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            ```ruby
         | 
| 31 | 
            +
            class MyTask < CMDx::Task
         | 
| 32 | 
            +
              register :middleware, CmdxPaperTrailMiddleware
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              def work
         | 
| 35 | 
            +
                # Do work...
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            end
         | 
| 39 | 
            +
            ```
         | 
    
        data/lib/cmdx/attribute.rb
    CHANGED
    
    | @@ -6,14 +6,71 @@ module CMDx | |
| 6 6 | 
             
              # They can be nested to create complex hierarchical data structures.
         | 
| 7 7 | 
             
              class Attribute
         | 
| 8 8 |  | 
| 9 | 
            +
                # @rbs AFFIX: Proc
         | 
| 9 10 | 
             
                AFFIX = proc do |value, &block|
         | 
| 10 11 | 
             
                  value == true ? block.call : value
         | 
| 11 12 | 
             
                end.freeze
         | 
| 12 13 | 
             
                private_constant :AFFIX
         | 
| 13 14 |  | 
| 15 | 
            +
                # Returns the task instance associated with this attribute.
         | 
| 16 | 
            +
                #
         | 
| 17 | 
            +
                # @return [CMDx::Task] The task instance
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @example
         | 
| 20 | 
            +
                #   attribute.task.context[:user_id] # => 42
         | 
| 21 | 
            +
                #
         | 
| 22 | 
            +
                # @rbs @task: Task
         | 
| 14 23 | 
             
                attr_accessor :task
         | 
| 15 24 |  | 
| 16 | 
            -
                 | 
| 25 | 
            +
                # Returns the name of this attribute.
         | 
| 26 | 
            +
                #
         | 
| 27 | 
            +
                # @return [Symbol] The attribute name
         | 
| 28 | 
            +
                #
         | 
| 29 | 
            +
                # @example
         | 
| 30 | 
            +
                #   attribute.name # => :user_id
         | 
| 31 | 
            +
                #
         | 
| 32 | 
            +
                # @rbs @name: Symbol
         | 
| 33 | 
            +
                attr_reader :name
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                # Returns the configuration options for this attribute.
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                # @return [Hash{Symbol => Object}] Configuration options hash
         | 
| 38 | 
            +
                #
         | 
| 39 | 
            +
                # @example
         | 
| 40 | 
            +
                #   attribute.options # => { required: true, default: 0 }
         | 
| 41 | 
            +
                #
         | 
| 42 | 
            +
                # @rbs @options: Hash[Symbol, untyped]
         | 
| 43 | 
            +
                attr_reader :options
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                # Returns the child attributes for nested structures.
         | 
| 46 | 
            +
                #
         | 
| 47 | 
            +
                # @return [Array<Attribute>] Array of child attributes
         | 
| 48 | 
            +
                #
         | 
| 49 | 
            +
                # @example
         | 
| 50 | 
            +
                #   attribute.children # => [#<Attribute @name=:street>, #<Attribute @name=:city>]
         | 
| 51 | 
            +
                #
         | 
| 52 | 
            +
                # @rbs @children: Array[Attribute]
         | 
| 53 | 
            +
                attr_reader :children
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                # Returns the parent attribute if this is a nested attribute.
         | 
| 56 | 
            +
                #
         | 
| 57 | 
            +
                # @return [Attribute, nil] The parent attribute, or nil if root-level
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                # @example
         | 
| 60 | 
            +
                #   attribute.parent # => #<Attribute @name=:address>
         | 
| 61 | 
            +
                #
         | 
| 62 | 
            +
                # @rbs @parent: (Attribute | nil)
         | 
| 63 | 
            +
                attr_reader :parent
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                # Returns the expected type(s) for this attribute's value.
         | 
| 66 | 
            +
                #
         | 
| 67 | 
            +
                # @return [Array<Class>] Array of expected type classes
         | 
| 68 | 
            +
                #
         | 
| 69 | 
            +
                # @example
         | 
| 70 | 
            +
                #   attribute.types # => [Integer, String]
         | 
| 71 | 
            +
                #
         | 
| 72 | 
            +
                # @rbs @types: Array[Class]
         | 
| 73 | 
            +
                attr_reader :types
         | 
| 17 74 |  | 
| 18 75 | 
             
                # Creates a new attribute with the specified name and configuration.
         | 
| 19 76 | 
             
                #
         | 
| @@ -35,6 +92,8 @@ module CMDx | |
| 35 92 | 
             
                #     required :name, types: String
         | 
| 36 93 | 
             
                #     optional :email, types: String
         | 
| 37 94 | 
             
                #   end
         | 
| 95 | 
            +
                #
         | 
| 96 | 
            +
                # @rbs ((Symbol | String) name, ?Hash[Symbol, untyped] options) ?{ () -> void } -> void
         | 
| 38 97 | 
             
                def initialize(name, options = {}, &)
         | 
| 39 98 | 
             
                  @parent = options.delete(:parent)
         | 
| 40 99 | 
             
                  @required = options.delete(:required) || false
         | 
| @@ -62,11 +121,13 @@ module CMDx | |
| 62 121 | 
             
                  #
         | 
| 63 122 | 
             
                  # @example
         | 
| 64 123 | 
             
                  #   Attribute.build(:first_name, :last_name, required: true, types: String)
         | 
| 124 | 
            +
                  #
         | 
| 125 | 
            +
                  # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
         | 
| 65 126 | 
             
                  def build(*names, **options, &)
         | 
| 66 127 | 
             
                    if names.none?
         | 
| 67 128 | 
             
                      raise ArgumentError, "no attributes given"
         | 
| 68 129 | 
             
                    elsif (names.size > 1) && options.key?(:as)
         | 
| 69 | 
            -
                      raise ArgumentError, ":as option only supports one attribute per definition"
         | 
| 130 | 
            +
                      raise ArgumentError, "the :as option only supports one attribute per definition"
         | 
| 70 131 | 
             
                    end
         | 
| 71 132 |  | 
| 72 133 | 
             
                    names.filter_map { |name| new(name, **options, &) }
         | 
| @@ -83,6 +144,8 @@ module CMDx | |
| 83 144 | 
             
                  #
         | 
| 84 145 | 
             
                  # @example
         | 
| 85 146 | 
             
                  #   Attribute.optional(:description, :tags, types: String)
         | 
| 147 | 
            +
                  #
         | 
| 148 | 
            +
                  # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
         | 
| 86 149 | 
             
                  def optional(*names, **options, &)
         | 
| 87 150 | 
             
                    build(*names, **options.merge(required: false), &)
         | 
| 88 151 | 
             
                  end
         | 
| @@ -98,6 +161,8 @@ module CMDx | |
| 98 161 | 
             
                  #
         | 
| 99 162 | 
             
                  # @example
         | 
| 100 163 | 
             
                  #   Attribute.required(:id, :name, types: [Integer, String])
         | 
| 164 | 
            +
                  #
         | 
| 165 | 
            +
                  # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
         | 
| 101 166 | 
             
                  def required(*names, **options, &)
         | 
| 102 167 | 
             
                    build(*names, **options.merge(required: true), &)
         | 
| 103 168 | 
             
                  end
         | 
| @@ -110,6 +175,8 @@ module CMDx | |
| 110 175 | 
             
                #
         | 
| 111 176 | 
             
                # @example
         | 
| 112 177 | 
             
                #   attribute.required? # => true
         | 
| 178 | 
            +
                #
         | 
| 179 | 
            +
                # @rbs () -> bool
         | 
| 113 180 | 
             
                def required?
         | 
| 114 181 | 
             
                  !!@required
         | 
| 115 182 | 
             
                end
         | 
| @@ -120,6 +187,8 @@ module CMDx | |
| 120 187 | 
             
                #
         | 
| 121 188 | 
             
                # @example
         | 
| 122 189 | 
             
                #   attribute.source # => :context
         | 
| 190 | 
            +
                #
         | 
| 191 | 
            +
                # @rbs () -> untyped
         | 
| 123 192 | 
             
                def source
         | 
| 124 193 | 
             
                  @source ||= parent&.method_name || begin
         | 
| 125 194 | 
             
                    value = options[:source]
         | 
| @@ -140,6 +209,8 @@ module CMDx | |
| 140 209 | 
             
                #
         | 
| 141 210 | 
             
                # @example
         | 
| 142 211 | 
             
                #   attribute.method_name # => :user_name
         | 
| 212 | 
            +
                #
         | 
| 213 | 
            +
                # @rbs () -> Symbol
         | 
| 143 214 | 
             
                def method_name
         | 
| 144 215 | 
             
                  @method_name ||= options[:as] || begin
         | 
| 145 216 | 
             
                    prefix = AFFIX.call(options[:prefix]) { "#{source}_" }
         | 
| @@ -150,6 +221,8 @@ module CMDx | |
| 150 221 | 
             
                end
         | 
| 151 222 |  | 
| 152 223 | 
             
                # Defines and verifies the entire attribute tree including nested children.
         | 
| 224 | 
            +
                #
         | 
| 225 | 
            +
                # @rbs () -> void
         | 
| 153 226 | 
             
                def define_and_verify_tree
         | 
| 154 227 | 
             
                  define_and_verify
         | 
| 155 228 |  | 
| @@ -172,6 +245,8 @@ module CMDx | |
| 172 245 | 
             
                #
         | 
| 173 246 | 
             
                # @example
         | 
| 174 247 | 
             
                #   attributes :street, :city, :zip, types: String
         | 
| 248 | 
            +
                #
         | 
| 249 | 
            +
                # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
         | 
| 175 250 | 
             
                def attributes(*names, **options, &)
         | 
| 176 251 | 
             
                  attrs = self.class.build(*names, **options.merge(parent: self), &)
         | 
| 177 252 | 
             
                  children.concat(attrs)
         | 
| @@ -189,6 +264,8 @@ module CMDx | |
| 189 264 | 
             
                #
         | 
| 190 265 | 
             
                # @example
         | 
| 191 266 | 
             
                #   optional :middle_name, :nickname, types: String
         | 
| 267 | 
            +
                #
         | 
| 268 | 
            +
                # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
         | 
| 192 269 | 
             
                def optional(*names, **options, &)
         | 
| 193 270 | 
             
                  attributes(*names, **options.merge(required: false), &)
         | 
| 194 271 | 
             
                end
         | 
| @@ -204,6 +281,8 @@ module CMDx | |
| 204 281 | 
             
                #
         | 
| 205 282 | 
             
                # @example
         | 
| 206 283 | 
             
                #   required :first_name, :last_name, types: String
         | 
| 284 | 
            +
                #
         | 
| 285 | 
            +
                # @rbs (*untyped names, **untyped options) ?{ () -> void } -> Array[Attribute]
         | 
| 207 286 | 
             
                def required(*names, **options, &)
         | 
| 208 287 | 
             
                  attributes(*names, **options.merge(required: true), &)
         | 
| 209 288 | 
             
                end
         | 
| @@ -211,12 +290,15 @@ module CMDx | |
| 211 290 | 
             
                # Defines the attribute method on the task and validates the configuration.
         | 
| 212 291 | 
             
                #
         | 
| 213 292 | 
             
                # @raise [RuntimeError] When the method name is already defined on the task
         | 
| 293 | 
            +
                #
         | 
| 294 | 
            +
                # @rbs () -> void
         | 
| 214 295 | 
             
                def define_and_verify
         | 
| 215 296 | 
             
                  if task.respond_to?(method_name, true)
         | 
| 216 | 
            -
                    raise <<~MESSAGE | 
| 217 | 
            -
                      #{task.class.name} | 
| 218 | 
            -
                       | 
| 219 | 
            -
             | 
| 297 | 
            +
                    raise <<~MESSAGE
         | 
| 298 | 
            +
                      The method #{method_name.inspect} is already defined on the #{task.class.name} task.
         | 
| 299 | 
            +
                      This may be due conflicts with one of the task's user defined or internal methods/attributes.
         | 
| 300 | 
            +
             | 
| 301 | 
            +
                      Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
         | 
| 220 302 | 
             
                    MESSAGE
         | 
| 221 303 | 
             
                  end
         | 
| 222 304 |  |