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/lib/cmdx/configuration.rb
    CHANGED
    
    | @@ -3,13 +3,112 @@ | |
| 3 3 | 
             
            module CMDx
         | 
| 4 4 |  | 
| 5 5 | 
             
              # Configuration class that manages global settings for CMDx including middlewares,
         | 
| 6 | 
            -
              # callbacks, coercions, validators, breakpoints, and logging.
         | 
| 6 | 
            +
              # callbacks, coercions, validators, breakpoints, backtraces, and logging.
         | 
| 7 7 | 
             
              class Configuration
         | 
| 8 8 |  | 
| 9 | 
            +
                # @rbs DEFAULT_BREAKPOINTS: Array[String]
         | 
| 9 10 | 
             
                DEFAULT_BREAKPOINTS = %w[failed].freeze
         | 
| 10 11 |  | 
| 11 | 
            -
                 | 
| 12 | 
            -
             | 
| 12 | 
            +
                # Returns the middleware registry for task execution.
         | 
| 13 | 
            +
                #
         | 
| 14 | 
            +
                # @return [MiddlewareRegistry] The middleware registry
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                # @example
         | 
| 17 | 
            +
                #   config.middlewares.register(CustomMiddleware)
         | 
| 18 | 
            +
                #
         | 
| 19 | 
            +
                # @rbs @middlewares: MiddlewareRegistry
         | 
| 20 | 
            +
                attr_accessor :middlewares
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # Returns the callback registry for task lifecycle hooks.
         | 
| 23 | 
            +
                #
         | 
| 24 | 
            +
                # @return [CallbackRegistry] The callback registry
         | 
| 25 | 
            +
                #
         | 
| 26 | 
            +
                # @example
         | 
| 27 | 
            +
                #   config.callbacks.register(:before_execution, :log_start)
         | 
| 28 | 
            +
                #
         | 
| 29 | 
            +
                # @rbs @callbacks: CallbackRegistry
         | 
| 30 | 
            +
                attr_accessor :callbacks
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # Returns the coercion registry for type conversions.
         | 
| 33 | 
            +
                #
         | 
| 34 | 
            +
                # @return [CoercionRegistry] The coercion registry
         | 
| 35 | 
            +
                #
         | 
| 36 | 
            +
                # @example
         | 
| 37 | 
            +
                #   config.coercions.register(:custom, CustomCoercion)
         | 
| 38 | 
            +
                #
         | 
| 39 | 
            +
                # @rbs @coercions: CoercionRegistry
         | 
| 40 | 
            +
                attr_accessor :coercions
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                # Returns the validator registry for attribute validation.
         | 
| 43 | 
            +
                #
         | 
| 44 | 
            +
                # @return [ValidatorRegistry] The validator registry
         | 
| 45 | 
            +
                #
         | 
| 46 | 
            +
                # @example
         | 
| 47 | 
            +
                #   config.validators.register(:email, EmailValidator)
         | 
| 48 | 
            +
                #
         | 
| 49 | 
            +
                # @rbs @validators: ValidatorRegistry
         | 
| 50 | 
            +
                attr_accessor :validators
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                # Returns the breakpoint statuses for task execution interruption.
         | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
                # @return [Array<String>] Array of status names that trigger breakpoints
         | 
| 55 | 
            +
                #
         | 
| 56 | 
            +
                # @example
         | 
| 57 | 
            +
                #   config.task_breakpoints = ["failed", "skipped"]
         | 
| 58 | 
            +
                #
         | 
| 59 | 
            +
                # @rbs @task_breakpoints: Array[String]
         | 
| 60 | 
            +
                attr_accessor :task_breakpoints
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                # Returns the breakpoint statuses for workflow execution interruption.
         | 
| 63 | 
            +
                #
         | 
| 64 | 
            +
                # @return [Array<String>] Array of status names that trigger breakpoints
         | 
| 65 | 
            +
                #
         | 
| 66 | 
            +
                # @example
         | 
| 67 | 
            +
                #   config.workflow_breakpoints = ["failed", "skipped"]
         | 
| 68 | 
            +
                #
         | 
| 69 | 
            +
                # @rbs @task_breakpoints: Array[String]
         | 
| 70 | 
            +
                # @rbs @workflow_breakpoints: Array[String]
         | 
| 71 | 
            +
                attr_accessor :workflow_breakpoints
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                # Returns the logger instance for CMDx operations.
         | 
| 74 | 
            +
                #
         | 
| 75 | 
            +
                # @return [Logger] The logger instance
         | 
| 76 | 
            +
                #
         | 
| 77 | 
            +
                # @example
         | 
| 78 | 
            +
                #   config.logger.level = Logger::DEBUG
         | 
| 79 | 
            +
                #
         | 
| 80 | 
            +
                # @rbs @logger: Logger
         | 
| 81 | 
            +
                attr_accessor :logger
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                # Returns whether to log backtraces for failed tasks.
         | 
| 84 | 
            +
                #
         | 
| 85 | 
            +
                # @return [Boolean] true if backtraces should be logged
         | 
| 86 | 
            +
                #
         | 
| 87 | 
            +
                # @example
         | 
| 88 | 
            +
                #   config.backtrace = true
         | 
| 89 | 
            +
                #
         | 
| 90 | 
            +
                # @rbs @backtrace: bool
         | 
| 91 | 
            +
                attr_accessor :backtrace
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                # Returns the proc used to clean backtraces before logging.
         | 
| 94 | 
            +
                #
         | 
| 95 | 
            +
                # @return [Proc, nil] The backtrace cleaner proc, or nil if not set
         | 
| 96 | 
            +
                #
         | 
| 97 | 
            +
                # @example
         | 
| 98 | 
            +
                #   config.backtrace_cleaner = ->(bt) { bt.first(5) }
         | 
| 99 | 
            +
                #
         | 
| 100 | 
            +
                # @rbs @backtrace_cleaner: (Proc | nil)
         | 
| 101 | 
            +
                attr_accessor :backtrace_cleaner
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                # Returns the proc called when exceptions occur during execution.
         | 
| 104 | 
            +
                #
         | 
| 105 | 
            +
                # @return [Proc, nil] The exception handler proc, or nil if not set
         | 
| 106 | 
            +
                #
         | 
| 107 | 
            +
                # @example
         | 
| 108 | 
            +
                #   config.exception_handler = ->(task, error) { Sentry.capture_exception(error) }
         | 
| 109 | 
            +
                #
         | 
| 110 | 
            +
                # @rbs @exception_handler: (Proc | nil)
         | 
| 111 | 
            +
                attr_accessor :exception_handler
         | 
| 13 112 |  | 
| 14 113 | 
             
                # Initializes a new Configuration instance with default values.
         | 
| 15 114 | 
             
                #
         | 
| @@ -22,6 +121,8 @@ module CMDx | |
| 22 121 | 
             
                #   config = Configuration.new
         | 
| 23 122 | 
             
                #   config.middlewares.class # => MiddlewareRegistry
         | 
| 24 123 | 
             
                #   config.task_breakpoints # => ["failed"]
         | 
| 124 | 
            +
                #
         | 
| 125 | 
            +
                # @rbs () -> void
         | 
| 25 126 | 
             
                def initialize
         | 
| 26 127 | 
             
                  @middlewares = MiddlewareRegistry.new
         | 
| 27 128 | 
             
                  @callbacks = CallbackRegistry.new
         | 
| @@ -31,6 +132,10 @@ module CMDx | |
| 31 132 | 
             
                  @task_breakpoints = DEFAULT_BREAKPOINTS
         | 
| 32 133 | 
             
                  @workflow_breakpoints = DEFAULT_BREAKPOINTS
         | 
| 33 134 |  | 
| 135 | 
            +
                  @backtrace = false
         | 
| 136 | 
            +
                  @backtrace_cleaner = nil
         | 
| 137 | 
            +
                  @exception_handler = nil
         | 
| 138 | 
            +
             | 
| 34 139 | 
             
                  @logger = Logger.new(
         | 
| 35 140 | 
             
                    $stdout,
         | 
| 36 141 | 
             
                    progname: "cmdx",
         | 
| @@ -47,6 +152,8 @@ module CMDx | |
| 47 152 | 
             
                #   config = Configuration.new
         | 
| 48 153 | 
             
                #   config.to_h
         | 
| 49 154 | 
             
                #   # => { middlewares: #<MiddlewareRegistry>, callbacks: #<CallbackRegistry>, ... }
         | 
| 155 | 
            +
                #
         | 
| 156 | 
            +
                # @rbs () -> Hash[Symbol, untyped]
         | 
| 50 157 | 
             
                def to_h
         | 
| 51 158 | 
             
                  {
         | 
| 52 159 | 
             
                    middlewares: @middlewares,
         | 
| @@ -55,6 +162,9 @@ module CMDx | |
| 55 162 | 
             
                    validators: @validators,
         | 
| 56 163 | 
             
                    task_breakpoints: @task_breakpoints,
         | 
| 57 164 | 
             
                    workflow_breakpoints: @workflow_breakpoints,
         | 
| 165 | 
            +
                    backtrace: @backtrace,
         | 
| 166 | 
            +
                    backtrace_cleaner: @backtrace_cleaner,
         | 
| 167 | 
            +
                    exception_handler: @exception_handler,
         | 
| 58 168 | 
             
                    logger: @logger
         | 
| 59 169 | 
             
                  }
         | 
| 60 170 | 
             
                end
         | 
| @@ -70,6 +180,8 @@ module CMDx | |
| 70 180 | 
             
              # @example
         | 
| 71 181 | 
             
              #   config = CMDx.configuration
         | 
| 72 182 | 
             
              #   config.middlewares # => #<MiddlewareRegistry>
         | 
| 183 | 
            +
              #
         | 
| 184 | 
            +
              # @rbs () -> Configuration
         | 
| 73 185 | 
             
              def configuration
         | 
| 74 186 | 
             
                return @configuration if @configuration
         | 
| 75 187 |  | 
| @@ -91,6 +203,8 @@ module CMDx | |
| 91 203 | 
             
              #     config.task_breakpoints = ["failed", "skipped"]
         | 
| 92 204 | 
             
              #     config.logger.level = Logger::DEBUG
         | 
| 93 205 | 
             
              #   end
         | 
| 206 | 
            +
              #
         | 
| 207 | 
            +
              # @rbs () { (Configuration) -> void } -> Configuration
         | 
| 94 208 | 
             
              def configure
         | 
| 95 209 | 
             
                raise ArgumentError, "block required" unless block_given?
         | 
| 96 210 |  | 
| @@ -106,6 +220,8 @@ module CMDx | |
| 106 220 | 
             
              # @example
         | 
| 107 221 | 
             
              #   CMDx.reset_configuration!
         | 
| 108 222 | 
             
              #   # Configuration is now reset to defaults
         | 
| 223 | 
            +
              #
         | 
| 224 | 
            +
              # @rbs () -> Configuration
         | 
| 109 225 | 
             
              def reset_configuration!
         | 
| 110 226 | 
             
                @configuration = Configuration.new
         | 
| 111 227 | 
             
              end
         | 
    
        data/lib/cmdx/context.rb
    CHANGED
    
    | @@ -11,6 +11,14 @@ module CMDx | |
| 11 11 |  | 
| 12 12 | 
             
                extend Forwardable
         | 
| 13 13 |  | 
| 14 | 
            +
                # Returns the internal hash storing context data.
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                # @return [Hash{Symbol => Object}] The internal hash table
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @example
         | 
| 19 | 
            +
                #   context.table # => { name: "John", age: 30 }
         | 
| 20 | 
            +
                #
         | 
| 21 | 
            +
                # @rbs @table: Hash[Symbol, untyped]
         | 
| 14 22 | 
             
                attr_reader :table
         | 
| 15 23 | 
             
                alias to_h table
         | 
| 16 24 |  | 
| @@ -28,6 +36,8 @@ module CMDx | |
| 28 36 | 
             
                # @example
         | 
| 29 37 | 
             
                #   context = Context.new(name: "John", age: 30)
         | 
| 30 38 | 
             
                #   context[:name] # => "John"
         | 
| 39 | 
            +
                #
         | 
| 40 | 
            +
                # @rbs (untyped args) -> void
         | 
| 31 41 | 
             
                def initialize(args = {})
         | 
| 32 42 | 
             
                  @table =
         | 
| 33 43 | 
             
                    if args.respond_to?(:to_hash)
         | 
| @@ -50,6 +60,8 @@ module CMDx | |
| 50 60 | 
             
                #   existing = Context.new(name: "John")
         | 
| 51 61 | 
             
                #   built = Context.build(existing) # reuses existing context
         | 
| 52 62 | 
             
                #   built.object_id == existing.object_id # => true
         | 
| 63 | 
            +
                #
         | 
| 64 | 
            +
                # @rbs (untyped context) -> Context
         | 
| 53 65 | 
             
                def self.build(context = {})
         | 
| 54 66 | 
             
                  if context.is_a?(self) && !context.frozen?
         | 
| 55 67 | 
             
                    context
         | 
| @@ -70,6 +82,8 @@ module CMDx | |
| 70 82 | 
             
                #   context = Context.new(name: "John")
         | 
| 71 83 | 
             
                #   context[:name] # => "John"
         | 
| 72 84 | 
             
                #   context["name"] # => "John" (automatically converted to symbol)
         | 
| 85 | 
            +
                #
         | 
| 86 | 
            +
                # @rbs ((String | Symbol) key) -> untyped
         | 
| 73 87 | 
             
                def [](key)
         | 
| 74 88 | 
             
                  table[key.to_sym]
         | 
| 75 89 | 
             
                end
         | 
| @@ -85,6 +99,8 @@ module CMDx | |
| 85 99 | 
             
                #   context = Context.new
         | 
| 86 100 | 
             
                #   context.store(:name, "John")
         | 
| 87 101 | 
             
                #   context[:name] # => "John"
         | 
| 102 | 
            +
                #
         | 
| 103 | 
            +
                # @rbs ((String | Symbol) key, untyped value) -> untyped
         | 
| 88 104 | 
             
                def store(key, value)
         | 
| 89 105 | 
             
                  table[key.to_sym] = value
         | 
| 90 106 | 
             
                end
         | 
| @@ -104,6 +120,8 @@ module CMDx | |
| 104 120 | 
             
                #   context.fetch(:name) # => "John"
         | 
| 105 121 | 
             
                #   context.fetch(:age, 25) # => 25
         | 
| 106 122 | 
             
                #   context.fetch(:city) { |key| "Unknown #{key}" } # => "Unknown city"
         | 
| 123 | 
            +
                #
         | 
| 124 | 
            +
                # @rbs ((String | Symbol) key, *untyped) ?{ ((String | Symbol)) -> untyped } -> untyped
         | 
| 107 125 | 
             
                def fetch(key, ...)
         | 
| 108 126 | 
             
                  table.fetch(key.to_sym, ...)
         | 
| 109 127 | 
             
                end
         | 
| @@ -122,6 +140,8 @@ module CMDx | |
| 122 140 | 
             
                #   context.fetch_or_store(:name, "Default") # => "John" (existing value)
         | 
| 123 141 | 
             
                #   context.fetch_or_store(:age, 25) # => 25 (stored and returned)
         | 
| 124 142 | 
             
                #   context.fetch_or_store(:city) { |key| "Unknown #{key}" } # => "Unknown city" (stored and returned)
         | 
| 143 | 
            +
                #
         | 
| 144 | 
            +
                # @rbs ((String | Symbol) key, ?untyped value) ?{ () -> untyped } -> untyped
         | 
| 125 145 | 
             
                def fetch_or_store(key, value = nil)
         | 
| 126 146 | 
             
                  table.fetch(key.to_sym) do
         | 
| 127 147 | 
             
                    table[key.to_sym] = block_given? ? yield : value
         | 
| @@ -139,6 +159,8 @@ module CMDx | |
| 139 159 | 
             
                #   context = Context.new(name: "John")
         | 
| 140 160 | 
             
                #   context.merge!(age: 30, city: "NYC")
         | 
| 141 161 | 
             
                #   context.to_h # => {name: "John", age: 30, city: "NYC"}
         | 
| 162 | 
            +
                #
         | 
| 163 | 
            +
                # @rbs (?untyped args) -> self
         | 
| 142 164 | 
             
                def merge!(args = {})
         | 
| 143 165 | 
             
                  args.to_h.each { |key, value| self[key.to_sym] = value }
         | 
| 144 166 | 
             
                  self
         | 
| @@ -156,6 +178,8 @@ module CMDx | |
| 156 178 | 
             
                #   context = Context.new(name: "John", age: 30)
         | 
| 157 179 | 
             
                #   context.delete!(:age) # => 30
         | 
| 158 180 | 
             
                #   context.delete!(:city) { |key| "Key #{key} not found" } # => "Key city not found"
         | 
| 181 | 
            +
                #
         | 
| 182 | 
            +
                # @rbs ((String | Symbol) key) ?{ ((String | Symbol)) -> untyped } -> untyped
         | 
| 159 183 | 
             
                def delete!(key, &)
         | 
| 160 184 | 
             
                  table.delete(key.to_sym, &)
         | 
| 161 185 | 
             
                end
         | 
| @@ -170,6 +194,8 @@ module CMDx | |
| 170 194 | 
             
                #   context1 = Context.new(name: "John")
         | 
| 171 195 | 
             
                #   context2 = Context.new(name: "John")
         | 
| 172 196 | 
             
                #   context1 == context2 # => true
         | 
| 197 | 
            +
                #
         | 
| 198 | 
            +
                # @rbs (untyped other) -> bool
         | 
| 173 199 | 
             
                def eql?(other)
         | 
| 174 200 | 
             
                  other.is_a?(self.class) && (to_h == other.to_h)
         | 
| 175 201 | 
             
                end
         | 
| @@ -185,6 +211,8 @@ module CMDx | |
| 185 211 | 
             
                #   context = Context.new(name: "John")
         | 
| 186 212 | 
             
                #   context.key?(:name) # => true
         | 
| 187 213 | 
             
                #   context.key?(:age) # => false
         | 
| 214 | 
            +
                #
         | 
| 215 | 
            +
                # @rbs ((String | Symbol) key) -> bool
         | 
| 188 216 | 
             
                def key?(key)
         | 
| 189 217 | 
             
                  table.key?(key.to_sym)
         | 
| 190 218 | 
             
                end
         | 
| @@ -200,6 +228,8 @@ module CMDx | |
| 200 228 | 
             
                #   context = Context.new(user: {profile: {name: "John"}})
         | 
| 201 229 | 
             
                #   context.dig(:user, :profile, :name) # => "John"
         | 
| 202 230 | 
             
                #   context.dig(:user, :profile, :age) # => nil
         | 
| 231 | 
            +
                #
         | 
| 232 | 
            +
                # @rbs ((String | Symbol) key, *(String | Symbol) keys) -> untyped
         | 
| 203 233 | 
             
                def dig(key, *keys)
         | 
| 204 234 | 
             
                  table.dig(key.to_sym, *keys)
         | 
| 205 235 | 
             
                end
         | 
| @@ -211,6 +241,8 @@ module CMDx | |
| 211 241 | 
             
                # @example
         | 
| 212 242 | 
             
                #   context = Context.new(name: "John", age: 30)
         | 
| 213 243 | 
             
                #   context.to_s # => "name: John, age: 30"
         | 
| 244 | 
            +
                #
         | 
| 245 | 
            +
                # @rbs () -> String
         | 
| 214 246 | 
             
                def to_s
         | 
| 215 247 | 
             
                  Utils::Format.to_str(to_h)
         | 
| 216 248 | 
             
                end
         | 
| @@ -227,6 +259,8 @@ module CMDx | |
| 227 259 | 
             
                # @yield [Object] optional block
         | 
| 228 260 | 
             
                #
         | 
| 229 261 | 
             
                # @return [Object] the result of the method call
         | 
| 262 | 
            +
                #
         | 
| 263 | 
            +
                # @rbs (Symbol method_name, *untyped args, **untyped _kwargs) ?{ () -> untyped } -> untyped
         | 
| 230 264 | 
             
                def method_missing(method_name, *args, **_kwargs, &)
         | 
| 231 265 | 
             
                  fetch(method_name) do
         | 
| 232 266 | 
             
                    store(method_name[0..-2], args.first) if method_name.end_with?("=")
         | 
| @@ -244,6 +278,8 @@ module CMDx | |
| 244 278 | 
             
                #   context = Context.new(name: "John")
         | 
| 245 279 | 
             
                #   context.respond_to?(:name) # => true
         | 
| 246 280 | 
             
                #   context.respond_to?(:age) # => false
         | 
| 281 | 
            +
                #
         | 
| 282 | 
            +
                # @rbs (Symbol method_name, ?bool include_private) -> bool
         | 
| 247 283 | 
             
                def respond_to_missing?(method_name, include_private = false)
         | 
| 248 284 | 
             
                  key?(method_name) || super
         | 
| 249 285 | 
             
                end
         | 
    
        data/lib/cmdx/deprecator.rb
    CHANGED
    
    | @@ -10,6 +10,7 @@ module CMDx | |
| 10 10 |  | 
| 11 11 | 
             
                extend self
         | 
| 12 12 |  | 
| 13 | 
            +
                # @rbs EVAL: Proc
         | 
| 13 14 | 
             
                EVAL = proc do |target, callable|
         | 
| 14 15 | 
             
                  case callable
         | 
| 15 16 | 
             
                  when /raise|log|warn/ then callable
         | 
| @@ -44,15 +45,17 @@ module CMDx | |
| 44 45 | 
             
                #     settings(deprecate: :warn)
         | 
| 45 46 | 
             
                #   end
         | 
| 46 47 | 
             
                #
         | 
| 47 | 
            -
                #   MyTask.new # => [MyTask] DEPRECATED: migrate to replacement or discontinue use
         | 
| 48 | 
            +
                #   MyTask.new # => [MyTask] DEPRECATED: migrate to a replacement or discontinue use
         | 
| 49 | 
            +
                #
         | 
| 50 | 
            +
                # @rbs (Task task) -> void
         | 
| 48 51 | 
             
                def restrict(task)
         | 
| 49 52 | 
             
                  type = EVAL.call(task, task.class.settings[:deprecate])
         | 
| 50 53 |  | 
| 51 54 | 
             
                  case type
         | 
| 52 55 | 
             
                  when NilClass, FalseClass # Do nothing
         | 
| 53 56 | 
             
                  when TrueClass, /raise/ then raise DeprecationError, "#{task.class.name} usage prohibited"
         | 
| 54 | 
            -
                  when /log/ then task.logger.warn { "DEPRECATED: migrate to replacement or discontinue use" }
         | 
| 55 | 
            -
                  when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to replacement or discontinue use", category: :deprecated)
         | 
| 57 | 
            +
                  when /log/ then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
         | 
| 58 | 
            +
                  when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
         | 
| 56 59 | 
             
                  else raise "unknown deprecation type #{type.inspect}"
         | 
| 57 60 | 
             
                  end
         | 
| 58 61 | 
             
                end
         | 
    
        data/lib/cmdx/errors.rb
    CHANGED
    
    | @@ -8,11 +8,21 @@ module CMDx | |
| 8 8 |  | 
| 9 9 | 
             
                extend Forwardable
         | 
| 10 10 |  | 
| 11 | 
            +
                # Returns the internal hash of error messages by attribute.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # @return [Hash{Symbol => Set<String>}] Hash mapping attribute names to error message sets
         | 
| 14 | 
            +
                #
         | 
| 15 | 
            +
                # @example
         | 
| 16 | 
            +
                #   errors.messages # => { email: #<Set: ["must be valid", "is required"]> }
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @rbs @messages: Hash[Symbol, Set[String]]
         | 
| 11 19 | 
             
                attr_reader :messages
         | 
| 12 20 |  | 
| 13 21 | 
             
                def_delegators :messages, :empty?
         | 
| 14 22 |  | 
| 15 23 | 
             
                # Initialize a new error collection.
         | 
| 24 | 
            +
                #
         | 
| 25 | 
            +
                # @rbs () -> void
         | 
| 16 26 | 
             
                def initialize
         | 
| 17 27 | 
             
                  @messages = {}
         | 
| 18 28 | 
             
                end
         | 
| @@ -26,6 +36,8 @@ module CMDx | |
| 26 36 | 
             
                #   errors = CMDx::Errors.new
         | 
| 27 37 | 
             
                #   errors.add(:email, "must be valid format")
         | 
| 28 38 | 
             
                #   errors.add(:email, "cannot be blank")
         | 
| 39 | 
            +
                #
         | 
| 40 | 
            +
                # @rbs (Symbol attribute, String message) -> void
         | 
| 29 41 | 
             
                def add(attribute, message)
         | 
| 30 42 | 
             
                  return if message.empty?
         | 
| 31 43 |  | 
| @@ -42,6 +54,8 @@ module CMDx | |
| 42 54 | 
             
                # @example
         | 
| 43 55 | 
             
                #   errors.for?(:email) # => true
         | 
| 44 56 | 
             
                #   errors.for?(:name)  # => false
         | 
| 57 | 
            +
                #
         | 
| 58 | 
            +
                # @rbs (Symbol attribute) -> bool
         | 
| 45 59 | 
             
                def for?(attribute)
         | 
| 46 60 | 
             
                  return false unless messages.key?(attribute)
         | 
| 47 61 |  | 
| @@ -54,6 +68,8 @@ module CMDx | |
| 54 68 | 
             
                #
         | 
| 55 69 | 
             
                # @example
         | 
| 56 70 | 
             
                #   errors.full_messages # => { email: ["email must be valid format", "email cannot be blank"] }
         | 
| 71 | 
            +
                #
         | 
| 72 | 
            +
                # @rbs () -> Hash[Symbol, Array[String]]
         | 
| 57 73 | 
             
                def full_messages
         | 
| 58 74 | 
             
                  messages.each_with_object({}) do |(attribute, messages), hash|
         | 
| 59 75 | 
             
                    hash[attribute] = messages.map { |message| "#{attribute} #{message}" }
         | 
| @@ -66,6 +82,8 @@ module CMDx | |
| 66 82 | 
             
                #
         | 
| 67 83 | 
             
                # @example
         | 
| 68 84 | 
             
                #   errors.to_h # => { email: ["must be valid format", "cannot be blank"] }
         | 
| 85 | 
            +
                #
         | 
| 86 | 
            +
                # @rbs () -> Hash[Symbol, Array[String]]
         | 
| 69 87 | 
             
                def to_h
         | 
| 70 88 | 
             
                  messages.transform_values(&:to_a)
         | 
| 71 89 | 
             
                end
         | 
| @@ -78,6 +96,8 @@ module CMDx | |
| 78 96 | 
             
                # @example
         | 
| 79 97 | 
             
                #   errors.to_hash # => { email: ["must be valid format", "cannot be blank"] }
         | 
| 80 98 | 
             
                #   errors.to_hash(true) # => { email: ["email must be valid format", "email cannot be blank"] }
         | 
| 99 | 
            +
                #
         | 
| 100 | 
            +
                # @rbs (?bool full) -> Hash[Symbol, Array[String]]
         | 
| 81 101 | 
             
                def to_hash(full = false)
         | 
| 82 102 | 
             
                  full ? full_messages : to_h
         | 
| 83 103 | 
             
                end
         | 
| @@ -88,6 +108,8 @@ module CMDx | |
| 88 108 | 
             
                #
         | 
| 89 109 | 
             
                # @example
         | 
| 90 110 | 
             
                #   errors.to_s # => "email must be valid format. email cannot be blank"
         | 
| 111 | 
            +
                #
         | 
| 112 | 
            +
                # @rbs () -> String
         | 
| 91 113 | 
             
                def to_s
         | 
| 92 114 | 
             
                  full_messages.values.flatten.join(". ")
         | 
| 93 115 | 
             
                end
         | 
    
        data/lib/cmdx/executor.rb
    CHANGED
    
    | @@ -8,6 +8,14 @@ module CMDx | |
| 8 8 | 
             
              # and proper error handling for different types of failures.
         | 
| 9 9 | 
             
              class Executor
         | 
| 10 10 |  | 
| 11 | 
            +
                # Returns the task being executed.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # @return [Task] The task instance
         | 
| 14 | 
            +
                #
         | 
| 15 | 
            +
                # @example
         | 
| 16 | 
            +
                #   executor.task.id # => "abc123"
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @rbs @task: Task
         | 
| 11 19 | 
             
                attr_reader :task
         | 
| 12 20 |  | 
| 13 21 | 
             
                # @param task [CMDx::Task] The task to execute
         | 
| @@ -16,6 +24,8 @@ module CMDx | |
| 16 24 | 
             
                #
         | 
| 17 25 | 
             
                # @example
         | 
| 18 26 | 
             
                #   executor = CMDx::Executor.new(my_task)
         | 
| 27 | 
            +
                #
         | 
| 28 | 
            +
                # @rbs (Task task) -> void
         | 
| 19 29 | 
             
                def initialize(task)
         | 
| 20 30 | 
             
                  @task = task
         | 
| 21 31 | 
             
                end
         | 
| @@ -32,6 +42,8 @@ module CMDx | |
| 32 42 | 
             
                # @example
         | 
| 33 43 | 
             
                #   CMDx::Executor.execute(my_task)
         | 
| 34 44 | 
             
                #   CMDx::Executor.execute(my_task, raise: true)
         | 
| 45 | 
            +
                #
         | 
| 46 | 
            +
                # @rbs (Task task, raise: bool) -> Result
         | 
| 35 47 | 
             
                def self.execute(task, raise: false)
         | 
| 36 48 | 
             
                  instance = new(task)
         | 
| 37 49 | 
             
                  raise ? instance.execute! : instance.execute
         | 
| @@ -44,16 +56,20 @@ module CMDx | |
| 44 56 | 
             
                # @example
         | 
| 45 57 | 
             
                #   executor = CMDx::Executor.new(my_task)
         | 
| 46 58 | 
             
                #   result = executor.execute
         | 
| 59 | 
            +
                #
         | 
| 60 | 
            +
                # @rbs () -> Result
         | 
| 47 61 | 
             
                def execute
         | 
| 48 62 | 
             
                  task.class.settings[:middlewares].call!(task) do
         | 
| 49 | 
            -
                    pre_execution!
         | 
| 63 | 
            +
                    pre_execution! unless @pre_execution
         | 
| 50 64 | 
             
                    execution!
         | 
| 51 65 | 
             
                  rescue UndefinedMethodError => e
         | 
| 52 66 | 
             
                    raise(e) # No need to clear the Chain since exception is not being re-raised
         | 
| 53 67 | 
             
                  rescue Fault => e
         | 
| 54 68 | 
             
                    task.result.throw!(e.result, halt: false, cause: e)
         | 
| 55 69 | 
             
                  rescue StandardError => e
         | 
| 70 | 
            +
                    retry if retry_execution?(e)
         | 
| 56 71 | 
             
                    task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
         | 
| 72 | 
            +
                    task.class.settings[:exception_handler]&.call(task, e)
         | 
| 57 73 | 
             
                  ensure
         | 
| 58 74 | 
             
                    task.result.executed!
         | 
| 59 75 | 
             
                    post_execution!
         | 
| @@ -71,9 +87,11 @@ module CMDx | |
| 71 87 | 
             
                # @example
         | 
| 72 88 | 
             
                #   executor = CMDx::Executor.new(my_task)
         | 
| 73 89 | 
             
                #   result = executor.execute!
         | 
| 90 | 
            +
                #
         | 
| 91 | 
            +
                # @rbs () -> Result
         | 
| 74 92 | 
             
                def execute!
         | 
| 75 93 | 
             
                  task.class.settings[:middlewares].call!(task) do
         | 
| 76 | 
            -
                    pre_execution!
         | 
| 94 | 
            +
                    pre_execution! unless @pre_execution
         | 
| 77 95 | 
             
                    execution!
         | 
| 78 96 | 
             
                  rescue UndefinedMethodError => e
         | 
| 79 97 | 
             
                    raise_exception(e)
         | 
| @@ -81,6 +99,7 @@ module CMDx | |
| 81 99 | 
             
                    task.result.throw!(e.result, halt: false, cause: e)
         | 
| 82 100 | 
             
                    halt_execution?(e) ? raise_exception(e) : post_execution!
         | 
| 83 101 | 
             
                  rescue StandardError => e
         | 
| 102 | 
            +
                    retry if retry_execution?(e)
         | 
| 84 103 | 
             
                    task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
         | 
| 85 104 | 
             
                    raise_exception(e)
         | 
| 86 105 | 
             
                  else
         | 
| @@ -101,6 +120,8 @@ module CMDx | |
| 101 120 | 
             
                #
         | 
| 102 121 | 
             
                # @example
         | 
| 103 122 | 
             
                #   halt_execution?(fault_exception)
         | 
| 123 | 
            +
                #
         | 
| 124 | 
            +
                # @rbs (Exception exception) -> bool
         | 
| 104 125 | 
             
                def halt_execution?(exception)
         | 
| 105 126 | 
             
                  breakpoints = task.class.settings[:breakpoints] || task.class.settings[:task_breakpoints]
         | 
| 106 127 | 
             
                  breakpoints = Array(breakpoints).map(&:to_s).uniq
         | 
| @@ -108,6 +129,40 @@ module CMDx | |
| 108 129 | 
             
                  breakpoints.include?(exception.result.status)
         | 
| 109 130 | 
             
                end
         | 
| 110 131 |  | 
| 132 | 
            +
                # Determines if execution should be retried based on retry configuration.
         | 
| 133 | 
            +
                #
         | 
| 134 | 
            +
                # @param exception [Exception] The exception that occurred
         | 
| 135 | 
            +
                #
         | 
| 136 | 
            +
                # @return [Boolean] Whether execution should be retried
         | 
| 137 | 
            +
                #
         | 
| 138 | 
            +
                # @example
         | 
| 139 | 
            +
                #   retry_execution?(standard_error)
         | 
| 140 | 
            +
                #
         | 
| 141 | 
            +
                # @rbs (Exception exception) -> bool
         | 
| 142 | 
            +
                def retry_execution?(exception)
         | 
| 143 | 
            +
                  available_retries = (task.class.settings[:retries] || 0).to_i
         | 
| 144 | 
            +
                  return false unless available_retries.positive?
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                  current_retries = (task.result.metadata[:retries] ||= 0).to_i
         | 
| 147 | 
            +
                  remaining_retries = available_retries - current_retries
         | 
| 148 | 
            +
                  return false unless remaining_retries.positive?
         | 
| 149 | 
            +
             | 
| 150 | 
            +
                  exceptions = Array(task.class.settings[:retry_on] || StandardError)
         | 
| 151 | 
            +
                  return false unless exceptions.any? { |e| exception.class <= e }
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                  task.result.metadata[:retries] += 1
         | 
| 154 | 
            +
             | 
| 155 | 
            +
                  task.logger.warn do
         | 
| 156 | 
            +
                    reason = "[#{exception.class}] #{exception.message}"
         | 
| 157 | 
            +
                    task.to_h.merge!(reason:, remaining_retries:)
         | 
| 158 | 
            +
                  end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                  jitter = task.class.settings[:retry_jitter].to_f * current_retries
         | 
| 161 | 
            +
                  sleep(jitter) if jitter.positive?
         | 
| 162 | 
            +
             | 
| 163 | 
            +
                  true
         | 
| 164 | 
            +
                end
         | 
| 165 | 
            +
             | 
| 111 166 | 
             
                # Raises an exception and clears the chain.
         | 
| 112 167 | 
             
                #
         | 
| 113 168 | 
             
                # @param exception [Exception] The exception to raise
         | 
| @@ -116,8 +171,11 @@ module CMDx | |
| 116 171 | 
             
                #
         | 
| 117 172 | 
             
                # @example
         | 
| 118 173 | 
             
                #   raise_exception(standard_error)
         | 
| 174 | 
            +
                #
         | 
| 175 | 
            +
                # @rbs (Exception exception) -> void
         | 
| 119 176 | 
             
                def raise_exception(exception)
         | 
| 120 177 | 
             
                  Chain.clear
         | 
| 178 | 
            +
             | 
| 121 179 | 
             
                  raise(exception)
         | 
| 122 180 | 
             
                end
         | 
| 123 181 |  | 
| @@ -129,14 +187,27 @@ module CMDx | |
| 129 187 | 
             
                #
         | 
| 130 188 | 
             
                # @example
         | 
| 131 189 | 
             
                #   invoke_callbacks(:before_execution)
         | 
| 190 | 
            +
                #
         | 
| 191 | 
            +
                # @rbs (Symbol type) -> void
         | 
| 132 192 | 
             
                def invoke_callbacks(type)
         | 
| 133 193 | 
             
                  task.class.settings[:callbacks].invoke(type, task)
         | 
| 134 194 | 
             
                end
         | 
| 135 195 |  | 
| 136 196 | 
             
                private
         | 
| 137 197 |  | 
| 198 | 
            +
                # Lazy loaded repeator instance to handle retries.
         | 
| 199 | 
            +
                #
         | 
| 200 | 
            +
                # @rbs () -> untyped
         | 
| 201 | 
            +
                def repeator
         | 
| 202 | 
            +
                  @repeator ||= Repeator.new(task)
         | 
| 203 | 
            +
                end
         | 
| 204 | 
            +
             | 
| 138 205 | 
             
                # Performs pre-execution tasks including validation and attribute verification.
         | 
| 206 | 
            +
                #
         | 
| 207 | 
            +
                # @rbs () -> void
         | 
| 139 208 | 
             
                def pre_execution!
         | 
| 209 | 
            +
                  @pre_execution = true
         | 
| 210 | 
            +
             | 
| 140 211 | 
             
                  invoke_callbacks(:before_validation)
         | 
| 141 212 |  | 
| 142 213 | 
             
                  task.class.settings[:attributes].define_and_verify(task)
         | 
| @@ -152,6 +223,8 @@ module CMDx | |
| 152 223 | 
             
                end
         | 
| 153 224 |  | 
| 154 225 | 
             
                # Executes the main task logic.
         | 
| 226 | 
            +
                #
         | 
| 227 | 
            +
                # @rbs () -> void
         | 
| 155 228 | 
             
                def execution!
         | 
| 156 229 | 
             
                  invoke_callbacks(:before_execution)
         | 
| 157 230 |  | 
| @@ -160,6 +233,8 @@ module CMDx | |
| 160 233 | 
             
                end
         | 
| 161 234 |  | 
| 162 235 | 
             
                # Performs post-execution tasks including callback invocation.
         | 
| 236 | 
            +
                #
         | 
| 237 | 
            +
                # @rbs () -> void
         | 
| 163 238 | 
             
                def post_execution!
         | 
| 164 239 | 
             
                  invoke_callbacks(:"on_#{task.result.state}")
         | 
| 165 240 | 
             
                  invoke_callbacks(:on_executed) if task.result.executed?
         | 
| @@ -170,14 +245,68 @@ module CMDx | |
| 170 245 | 
             
                end
         | 
| 171 246 |  | 
| 172 247 | 
             
                # Finalizes execution by freezing the task and logging results.
         | 
| 248 | 
            +
                #
         | 
| 249 | 
            +
                # @rbs () -> Result
         | 
| 173 250 | 
             
                def finalize_execution!
         | 
| 174 | 
            -
                   | 
| 175 | 
            -
             | 
| 176 | 
            -
             | 
| 177 | 
            -
             | 
| 251 | 
            +
                  log_execution!
         | 
| 252 | 
            +
                  log_backtrace! if task.class.settings[:backtrace]
         | 
| 253 | 
            +
             | 
| 254 | 
            +
                  freeze_execution!
         | 
| 255 | 
            +
                  clear_chain!
         | 
| 256 | 
            +
                end
         | 
| 257 | 
            +
             | 
| 258 | 
            +
                # Logs the execution result at the configured log level.
         | 
| 259 | 
            +
                #
         | 
| 260 | 
            +
                # @rbs () -> void
         | 
| 261 | 
            +
                def log_execution!
         | 
| 262 | 
            +
                  task.logger.info { task.result.to_h }
         | 
| 263 | 
            +
                end
         | 
| 264 | 
            +
             | 
| 265 | 
            +
                # Logs the backtrace of the exception if the task failed.
         | 
| 266 | 
            +
                #
         | 
| 267 | 
            +
                # @rbs () -> void
         | 
| 268 | 
            +
                def log_backtrace!
         | 
| 269 | 
            +
                  return unless task.result.failed?
         | 
| 270 | 
            +
             | 
| 271 | 
            +
                  exception = task.result.caused_failure.cause
         | 
| 272 | 
            +
                  return if exception.is_a?(Fault)
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                  task.logger.error do
         | 
| 275 | 
            +
                    "[#{exception.class}] #{exception.message}\n" <<
         | 
| 276 | 
            +
                      if (cleaner = task.class.settings[:backtrace_cleaner])
         | 
| 277 | 
            +
                        cleaner.call(exception.backtrace).join("\n\t")
         | 
| 278 | 
            +
                      else
         | 
| 279 | 
            +
                        exception.full_message(highlight: false)
         | 
| 280 | 
            +
                      end
         | 
| 178 281 | 
             
                  end
         | 
| 282 | 
            +
                end
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                # Freezes the task and its associated objects to prevent modifications.
         | 
| 285 | 
            +
                #
         | 
| 286 | 
            +
                # @rbs () -> void
         | 
| 287 | 
            +
                def freeze_execution!
         | 
| 288 | 
            +
                  # Stubbing on frozen objects is not allowed in most test environments.
         | 
| 289 | 
            +
                  skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
         | 
| 290 | 
            +
                  return if Coercions::Boolean.call(skip_freezing)
         | 
| 291 | 
            +
             | 
| 292 | 
            +
                  task.freeze
         | 
| 293 | 
            +
                  task.result.freeze
         | 
| 179 294 |  | 
| 180 | 
            -
                   | 
| 295 | 
            +
                  # Freezing the context and chain can only be done
         | 
| 296 | 
            +
                  # once the outer-most task has completed.
         | 
| 297 | 
            +
                  return unless task.result.index.zero?
         | 
| 298 | 
            +
             | 
| 299 | 
            +
                  task.context.freeze
         | 
| 300 | 
            +
                  task.chain.freeze
         | 
| 301 | 
            +
                end
         | 
| 302 | 
            +
             | 
| 303 | 
            +
                # Clears the chain if the task is the outermost (top-level) task.
         | 
| 304 | 
            +
                #
         | 
| 305 | 
            +
                # @rbs () -> void
         | 
| 306 | 
            +
                def clear_chain!
         | 
| 307 | 
            +
                  return unless task.result.index.zero?
         | 
| 308 | 
            +
             | 
| 309 | 
            +
                  Chain.clear
         | 
| 181 310 | 
             
                end
         | 
| 182 311 |  | 
| 183 312 | 
             
              end
         |