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
| @@ -6,6 +6,14 @@ module CMDx | |
| 6 6 | 
             
              # in a hierarchical structure, supporting nested attribute definitions.
         | 
| 7 7 | 
             
              class AttributeRegistry
         | 
| 8 8 |  | 
| 9 | 
            +
                # Returns the collection of registered attributes.
         | 
| 10 | 
            +
                #
         | 
| 11 | 
            +
                # @return [Array<Attribute>] Array of registered attributes
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # @example
         | 
| 14 | 
            +
                #   registry.registry # => [#<Attribute @name=:name>, #<Attribute @name=:email>]
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                # @rbs @registry: Array[Attribute]
         | 
| 9 17 | 
             
                attr_reader :registry
         | 
| 10 18 | 
             
                alias to_a registry
         | 
| 11 19 |  | 
| @@ -18,6 +26,8 @@ module CMDx | |
| 18 26 | 
             
                # @example
         | 
| 19 27 | 
             
                #   registry = AttributeRegistry.new
         | 
| 20 28 | 
             
                #   registry = AttributeRegistry.new([attr1, attr2])
         | 
| 29 | 
            +
                #
         | 
| 30 | 
            +
                # @rbs (?Array[Attribute] registry) -> void
         | 
| 21 31 | 
             
                def initialize(registry = [])
         | 
| 22 32 | 
             
                  @registry = registry
         | 
| 23 33 | 
             
                end
         | 
| @@ -28,6 +38,8 @@ module CMDx | |
| 28 38 | 
             
                #
         | 
| 29 39 | 
             
                # @example
         | 
| 30 40 | 
             
                #   new_registry = registry.dup
         | 
| 41 | 
            +
                #
         | 
| 42 | 
            +
                # @rbs () -> AttributeRegistry
         | 
| 31 43 | 
             
                def dup
         | 
| 32 44 | 
             
                  self.class.new(registry.dup)
         | 
| 33 45 | 
             
                end
         | 
| @@ -41,6 +53,8 @@ module CMDx | |
| 41 53 | 
             
                # @example
         | 
| 42 54 | 
             
                #   registry.register(attribute)
         | 
| 43 55 | 
             
                #   registry.register([attr1, attr2])
         | 
| 56 | 
            +
                #
         | 
| 57 | 
            +
                # @rbs (Attribute | Array[Attribute] attributes) -> self
         | 
| 44 58 | 
             
                def register(attributes)
         | 
| 45 59 | 
             
                  @registry.concat(Array(attributes))
         | 
| 46 60 | 
             
                  self
         | 
| @@ -56,6 +70,8 @@ module CMDx | |
| 56 70 | 
             
                # @example
         | 
| 57 71 | 
             
                #   registry.deregister(:name)
         | 
| 58 72 | 
             
                #   registry.deregister(['name1', 'name2'])
         | 
| 73 | 
            +
                #
         | 
| 74 | 
            +
                # @rbs ((Symbol | String | Array[Symbol | String]) names) -> self
         | 
| 59 75 | 
             
                def deregister(names)
         | 
| 60 76 | 
             
                  Array(names).each do |name|
         | 
| 61 77 | 
             
                    @registry.reject! { |attribute| matches_attribute_tree?(attribute, name.to_sym) }
         | 
| @@ -69,6 +85,8 @@ module CMDx | |
| 69 85 | 
             
                # and validate the attribute hierarchy.
         | 
| 70 86 | 
             
                #
         | 
| 71 87 | 
             
                # @param task [Task] The task to associate with all attributes
         | 
| 88 | 
            +
                #
         | 
| 89 | 
            +
                # @rbs (Task task) -> void
         | 
| 72 90 | 
             
                def define_and_verify(task)
         | 
| 73 91 | 
             
                  registry.each do |attribute|
         | 
| 74 92 | 
             
                    attribute.task = task
         | 
| @@ -84,6 +102,8 @@ module CMDx | |
| 84 102 | 
             
                # @param name [Symbol] The name to match against
         | 
| 85 103 | 
             
                #
         | 
| 86 104 | 
             
                # @return [Boolean] True if the attribute or any child matches the name
         | 
| 105 | 
            +
                #
         | 
| 106 | 
            +
                # @rbs (Attribute attribute, Symbol name) -> bool
         | 
| 87 107 | 
             
                def matches_attribute_tree?(attribute, name)
         | 
| 88 108 | 
             
                  return true if attribute.method_name == name
         | 
| 89 109 |  | 
    
        data/lib/cmdx/attribute_value.rb
    CHANGED
    
    | @@ -8,6 +8,14 @@ module CMDx | |
| 8 8 |  | 
| 9 9 | 
             
                extend Forwardable
         | 
| 10 10 |  | 
| 11 | 
            +
                # Returns the attribute managed by this value handler.
         | 
| 12 | 
            +
                #
         | 
| 13 | 
            +
                # @return [Attribute] The attribute instance
         | 
| 14 | 
            +
                #
         | 
| 15 | 
            +
                # @example
         | 
| 16 | 
            +
                #   attr_value.attribute.name # => :user_id
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @rbs @attribute: Attribute
         | 
| 11 19 | 
             
                attr_reader :attribute
         | 
| 12 20 |  | 
| 13 21 | 
             
                def_delegators :attribute, :task, :parent, :name, :options, :types, :source, :method_name, :required?
         | 
| @@ -20,6 +28,8 @@ module CMDx | |
| 20 28 | 
             
                # @example
         | 
| 21 29 | 
             
                #   attr = Attribute.new(:user_id, required: true)
         | 
| 22 30 | 
             
                #   attr_value = AttributeValue.new(attr)
         | 
| 31 | 
            +
                #
         | 
| 32 | 
            +
                # @rbs (Attribute attribute) -> void
         | 
| 23 33 | 
             
                def initialize(attribute)
         | 
| 24 34 | 
             
                  @attribute = attribute
         | 
| 25 35 | 
             
                end
         | 
| @@ -30,6 +40,8 @@ module CMDx | |
| 30 40 | 
             
                #
         | 
| 31 41 | 
             
                # @example
         | 
| 32 42 | 
             
                #   attr_value.value # => "john_doe"
         | 
| 43 | 
            +
                #
         | 
| 44 | 
            +
                # @rbs () -> untyped
         | 
| 33 45 | 
             
                def value
         | 
| 34 46 | 
             
                  attributes[method_name]
         | 
| 35 47 | 
             
                end
         | 
| @@ -41,6 +53,8 @@ module CMDx | |
| 41 53 | 
             
                #
         | 
| 42 54 | 
             
                # @example
         | 
| 43 55 | 
             
                #   attr_value.generate # => 42
         | 
| 56 | 
            +
                #
         | 
| 57 | 
            +
                # @rbs () -> untyped
         | 
| 44 58 | 
             
                def generate
         | 
| 45 59 | 
             
                  return value if attributes.key?(method_name)
         | 
| 46 60 |  | 
| @@ -51,9 +65,10 @@ module CMDx | |
| 51 65 | 
             
                  return if errors.for?(method_name)
         | 
| 52 66 |  | 
| 53 67 | 
             
                  coerced_value = coerce_value(derived_value)
         | 
| 68 | 
            +
                  transformed_value = transform_value(coerced_value)
         | 
| 54 69 | 
             
                  return if errors.for?(method_name)
         | 
| 55 70 |  | 
| 56 | 
            -
                  attributes[method_name] =  | 
| 71 | 
            +
                  attributes[method_name] = transformed_value
         | 
| 57 72 | 
             
                end
         | 
| 58 73 |  | 
| 59 74 | 
             
                # Validates the current attribute value against configured validators.
         | 
| @@ -63,6 +78,8 @@ module CMDx | |
| 63 78 | 
             
                # @example
         | 
| 64 79 | 
             
                #   attr_value.validate
         | 
| 65 80 | 
             
                #   # Validates value against :presence, :format, etc.
         | 
| 81 | 
            +
                #
         | 
| 82 | 
            +
                # @rbs () -> void
         | 
| 66 83 | 
             
                def validate
         | 
| 67 84 | 
             
                  registry = task.class.settings[:validators]
         | 
| 68 85 |  | 
| @@ -85,6 +102,7 @@ module CMDx | |
| 85 102 | 
             
                # @example
         | 
| 86 103 | 
             
                #   # Sources from task method, proc, or direct value
         | 
| 87 104 | 
             
                #   source_value # => "raw_value"
         | 
| 105 | 
            +
                # @rbs () -> untyped
         | 
| 88 106 | 
             
                def source_value
         | 
| 89 107 | 
             
                  sourced_value =
         | 
| 90 108 | 
             
                    case source
         | 
| @@ -113,7 +131,9 @@ module CMDx | |
| 113 131 | 
             
                #
         | 
| 114 132 | 
             
                # @example
         | 
| 115 133 | 
             
                #   # Default can be symbol, proc, or direct value
         | 
| 116 | 
            -
                #    | 
| 134 | 
            +
                #   -> { rand(100) } # => 23
         | 
| 135 | 
            +
                #
         | 
| 136 | 
            +
                # @rbs () -> untyped
         | 
| 117 137 | 
             
                def default_value
         | 
| 118 138 | 
             
                  default = options[:default]
         | 
| 119 139 |  | 
| @@ -138,7 +158,9 @@ module CMDx | |
| 138 158 | 
             
                #
         | 
| 139 159 | 
             
                # @example
         | 
| 140 160 | 
             
                #   # Derives from hash key, method call, or proc execution
         | 
| 141 | 
            -
                #    | 
| 161 | 
            +
                #   context.user_id # => 42
         | 
| 162 | 
            +
                #
         | 
| 163 | 
            +
                # @rbs (untyped source_value) -> untyped
         | 
| 142 164 | 
             
                def derive_value(source_value)
         | 
| 143 165 | 
             
                  derived_value =
         | 
| 144 166 | 
             
                    case source_value
         | 
| @@ -154,9 +176,31 @@ module CMDx | |
| 154 176 | 
             
                  nil
         | 
| 155 177 | 
             
                end
         | 
| 156 178 |  | 
| 179 | 
            +
                # Transforms the derived value using the transform option.
         | 
| 180 | 
            +
                #
         | 
| 181 | 
            +
                # @param derived_value [Object] The value to transform
         | 
| 182 | 
            +
                #
         | 
| 183 | 
            +
                # @return [Object, nil] The transformed value or nil if transformation failed
         | 
| 184 | 
            +
                #
         | 
| 185 | 
            +
                # @example
         | 
| 186 | 
            +
                #   :downcase # => "hello"
         | 
| 187 | 
            +
                #
         | 
| 188 | 
            +
                # @rbs (untyped derived_value) -> untyped
         | 
| 189 | 
            +
                def transform_value(derived_value)
         | 
| 190 | 
            +
                  transform = options[:transform]
         | 
| 191 | 
            +
             | 
| 192 | 
            +
                  if transform.is_a?(Symbol) && derived_value.respond_to?(transform, true)
         | 
| 193 | 
            +
                    derived_value.send(transform)
         | 
| 194 | 
            +
                  elsif transform.respond_to?(:call)
         | 
| 195 | 
            +
                    transform.call(derived_value)
         | 
| 196 | 
            +
                  else
         | 
| 197 | 
            +
                    derived_value
         | 
| 198 | 
            +
                  end
         | 
| 199 | 
            +
                end
         | 
| 200 | 
            +
             | 
| 157 201 | 
             
                # Coerces the derived value to the expected type(s) using the coercion registry.
         | 
| 158 202 | 
             
                #
         | 
| 159 | 
            -
                # @param  | 
| 203 | 
            +
                # @param transformed_value [Object] The value to coerce
         | 
| 160 204 | 
             
                #
         | 
| 161 205 | 
             
                # @return [Object, nil] The coerced value or nil if coercion failed
         | 
| 162 206 | 
             
                #
         | 
| @@ -165,14 +209,16 @@ module CMDx | |
| 165 209 | 
             
                # @example
         | 
| 166 210 | 
             
                #   # Coerces "42" to Integer, "true" to Boolean, etc.
         | 
| 167 211 | 
             
                #   coerce_value("42") # => 42
         | 
| 168 | 
            -
                 | 
| 169 | 
            -
             | 
| 212 | 
            +
                #
         | 
| 213 | 
            +
                # @rbs (untyped transformed_value) -> untyped
         | 
| 214 | 
            +
                def coerce_value(transformed_value)
         | 
| 215 | 
            +
                  return transformed_value if types.empty?
         | 
| 170 216 |  | 
| 171 217 | 
             
                  registry = task.class.settings[:coercions]
         | 
| 172 | 
            -
                  last_idx =  | 
| 218 | 
            +
                  last_idx = types.size - 1
         | 
| 173 219 |  | 
| 174 | 
            -
                   | 
| 175 | 
            -
                    break registry.coerce(type, task,  | 
| 220 | 
            +
                  types.find.with_index do |type, i|
         | 
| 221 | 
            +
                    break registry.coerce(type, task, transformed_value, options)
         | 
| 176 222 | 
             
                  rescue CoercionError => e
         | 
| 177 223 | 
             
                    next if i != last_idx
         | 
| 178 224 |  | 
| @@ -180,7 +226,7 @@ module CMDx | |
| 180 226 | 
             
                      if last_idx.zero?
         | 
| 181 227 | 
             
                        e.message
         | 
| 182 228 | 
             
                      else
         | 
| 183 | 
            -
                        tl =  | 
| 229 | 
            +
                        tl = types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
         | 
| 184 230 | 
             
                        Locale.t("cmdx.coercions.into_any", types: tl)
         | 
| 185 231 | 
             
                      end
         | 
| 186 232 |  | 
| @@ -7,6 +7,7 @@ module CMDx | |
| 7 7 | 
             
              # Each callback type represents a specific execution phase or outcome.
         | 
| 8 8 | 
             
              class CallbackRegistry
         | 
| 9 9 |  | 
| 10 | 
            +
                # @rbs TYPES: Array[Symbol]
         | 
| 10 11 | 
             
                TYPES = %i[
         | 
| 11 12 | 
             
                  before_validation
         | 
| 12 13 | 
             
                  before_execution
         | 
| @@ -20,10 +21,20 @@ module CMDx | |
| 20 21 | 
             
                  on_bad
         | 
| 21 22 | 
             
                ].freeze
         | 
| 22 23 |  | 
| 24 | 
            +
                # Returns the internal registry of callbacks organized by type.
         | 
| 25 | 
            +
                #
         | 
| 26 | 
            +
                # @return [Hash{Symbol => Set<Array>}] Hash mapping callback types to their registered callables
         | 
| 27 | 
            +
                #
         | 
| 28 | 
            +
                # @example
         | 
| 29 | 
            +
                #   registry.registry # => { before_execution: #<Set: [[[:validate], {}]]> }
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # @rbs @registry: Hash[Symbol, Set[Array[untyped]]]
         | 
| 23 32 | 
             
                attr_reader :registry
         | 
| 24 33 | 
             
                alias to_h registry
         | 
| 25 34 |  | 
| 26 35 | 
             
                # @param registry [Hash] Initial registry hash, defaults to empty
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                # @rbs (?Hash[Symbol, Set[Array[untyped]]] registry) -> void
         | 
| 27 38 | 
             
                def initialize(registry = {})
         | 
| 28 39 | 
             
                  @registry = registry
         | 
| 29 40 | 
             
                end
         | 
| @@ -31,6 +42,8 @@ module CMDx | |
| 31 42 | 
             
                # Creates a deep copy of the registry with duplicated callable sets
         | 
| 32 43 | 
             
                #
         | 
| 33 44 | 
             
                # @return [CallbackRegistry] A new instance with duplicated registry contents
         | 
| 45 | 
            +
                #
         | 
| 46 | 
            +
                # @rbs () -> CallbackRegistry
         | 
| 34 47 | 
             
                def dup
         | 
| 35 48 | 
             
                  self.class.new(registry.transform_values(&:dup))
         | 
| 36 49 | 
             
                end
         | 
| @@ -54,6 +67,8 @@ module CMDx | |
| 54 67 | 
             
                #   registry.register(:on_success, if: { status: :completed }) do |task|
         | 
| 55 68 | 
             
                #     task.log("Success callback executed")
         | 
| 56 69 | 
             
                #   end
         | 
| 70 | 
            +
                #
         | 
| 71 | 
            +
                # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
         | 
| 57 72 | 
             
                def register(type, *callables, **options, &block)
         | 
| 58 73 | 
             
                  callables << block if block_given?
         | 
| 59 74 |  | 
| @@ -73,6 +88,8 @@ module CMDx | |
| 73 88 | 
             
                #
         | 
| 74 89 | 
             
                # @example Remove a specific callback
         | 
| 75 90 | 
             
                #   registry.deregister(:before_execution, :validate_inputs)
         | 
| 91 | 
            +
                #
         | 
| 92 | 
            +
                # @rbs (Symbol type, *untyped callables, **untyped options) ?{ (Task) -> void } -> self
         | 
| 76 93 | 
             
                def deregister(type, *callables, **options, &block)
         | 
| 77 94 | 
             
                  callables << block if block_given?
         | 
| 78 95 | 
             
                  return self unless registry[type]
         | 
| @@ -91,13 +108,25 @@ module CMDx | |
| 91 108 | 
             
                #
         | 
| 92 109 | 
             
                # @example Invoke all before_execution callbacks
         | 
| 93 110 | 
             
                #   registry.invoke(:before_execution, task)
         | 
| 111 | 
            +
                #
         | 
| 112 | 
            +
                # @rbs (Symbol type, Task task) -> void
         | 
| 94 113 | 
             
                def invoke(type, task)
         | 
| 95 114 | 
             
                  raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type)
         | 
| 96 115 |  | 
| 97 116 | 
             
                  Array(registry[type]).each do |callables, options|
         | 
| 98 | 
            -
                    next unless Utils::Condition.evaluate(task, options | 
| 117 | 
            +
                    next unless Utils::Condition.evaluate(task, options)
         | 
| 99 118 |  | 
| 100 | 
            -
                    Array(callables).each  | 
| 119 | 
            +
                    Array(callables).each do |callable|
         | 
| 120 | 
            +
                      if callable.is_a?(Symbol)
         | 
| 121 | 
            +
                        task.send(callable)
         | 
| 122 | 
            +
                      elsif callable.is_a?(Proc)
         | 
| 123 | 
            +
                        task.instance_exec(&callable)
         | 
| 124 | 
            +
                      elsif callable.respond_to?(:call)
         | 
| 125 | 
            +
                        callable.call(task)
         | 
| 126 | 
            +
                      else
         | 
| 127 | 
            +
                        raise "cannot invoke #{callable}"
         | 
| 128 | 
            +
                      end
         | 
| 129 | 
            +
                    end
         | 
| 101 130 | 
             
                  end
         | 
| 102 131 | 
             
                end
         | 
| 103 132 |  | 
    
        data/lib/cmdx/chain.rb
    CHANGED
    
    | @@ -8,9 +8,28 @@ module CMDx | |
| 8 8 |  | 
| 9 9 | 
             
                extend Forwardable
         | 
| 10 10 |  | 
| 11 | 
            +
                # @rbs THREAD_KEY: Symbol
         | 
| 11 12 | 
             
                THREAD_KEY = :cmdx_chain
         | 
| 12 13 |  | 
| 13 | 
            -
                 | 
| 14 | 
            +
                # Returns the unique identifier for this chain.
         | 
| 15 | 
            +
                #
         | 
| 16 | 
            +
                # @return [String] The chain identifier
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                # @example
         | 
| 19 | 
            +
                #   chain.id # => "abc123xyz"
         | 
| 20 | 
            +
                #
         | 
| 21 | 
            +
                # @rbs @id: String
         | 
| 22 | 
            +
                attr_reader :id
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                # Returns the collection of execution results in this chain.
         | 
| 25 | 
            +
                #
         | 
| 26 | 
            +
                # @return [Array<Result>] Array of task results
         | 
| 27 | 
            +
                #
         | 
| 28 | 
            +
                # @example
         | 
| 29 | 
            +
                #   chain.results # => [#<Result>, #<Result>]
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # @rbs @results: Array[Result]
         | 
| 32 | 
            +
                attr_reader :results
         | 
| 14 33 |  | 
| 15 34 | 
             
                def_delegators :results, :index, :first, :last, :size
         | 
| 16 35 | 
             
                def_delegators :first, :state, :status, :outcome, :runtime
         | 
| @@ -18,6 +37,8 @@ module CMDx | |
| 18 37 | 
             
                # Creates a new chain with a unique identifier and empty results collection.
         | 
| 19 38 | 
             
                #
         | 
| 20 39 | 
             
                # @return [Chain] A new chain instance
         | 
| 40 | 
            +
                #
         | 
| 41 | 
            +
                # @rbs () -> void
         | 
| 21 42 | 
             
                def initialize
         | 
| 22 43 | 
             
                  @id = Identifier.generate
         | 
| 23 44 | 
             
                  @results = []
         | 
| @@ -34,6 +55,8 @@ module CMDx | |
| 34 55 | 
             
                  #   if chain
         | 
| 35 56 | 
             
                  #     puts "Current chain: #{chain.id}"
         | 
| 36 57 | 
             
                  #   end
         | 
| 58 | 
            +
                  #
         | 
| 59 | 
            +
                  # @rbs () -> Chain?
         | 
| 37 60 | 
             
                  def current
         | 
| 38 61 | 
             
                    Thread.current[THREAD_KEY]
         | 
| 39 62 | 
             
                  end
         | 
| @@ -46,6 +69,8 @@ module CMDx | |
| 46 69 | 
             
                  #
         | 
| 47 70 | 
             
                  # @example
         | 
| 48 71 | 
             
                  #   Chain.current = my_chain
         | 
| 72 | 
            +
                  #
         | 
| 73 | 
            +
                  # @rbs (Chain chain) -> Chain
         | 
| 49 74 | 
             
                  def current=(chain)
         | 
| 50 75 | 
             
                    Thread.current[THREAD_KEY] = chain
         | 
| 51 76 | 
             
                  end
         | 
| @@ -56,6 +81,8 @@ module CMDx | |
| 56 81 | 
             
                  #
         | 
| 57 82 | 
             
                  # @example
         | 
| 58 83 | 
             
                  #   Chain.clear
         | 
| 84 | 
            +
                  #
         | 
| 85 | 
            +
                  # @rbs () -> nil
         | 
| 59 86 | 
             
                  def clear
         | 
| 60 87 | 
             
                    Thread.current[THREAD_KEY] = nil
         | 
| 61 88 | 
             
                  end
         | 
| @@ -73,6 +100,8 @@ module CMDx | |
| 73 100 | 
             
                  #   result = task.execute
         | 
| 74 101 | 
             
                  #   chain = Chain.build(result)
         | 
| 75 102 | 
             
                  #   puts "Chain size: #{chain.size}"
         | 
| 103 | 
            +
                  #
         | 
| 104 | 
            +
                  # @rbs (Result result) -> Chain
         | 
| 76 105 | 
             
                  def build(result)
         | 
| 77 106 | 
             
                    raise TypeError, "must be a CMDx::Result" unless result.is_a?(Result)
         | 
| 78 107 |  | 
| @@ -95,6 +124,8 @@ module CMDx | |
| 95 124 | 
             
                #   chain_hash = chain.to_h
         | 
| 96 125 | 
             
                #   puts chain_hash[:id]
         | 
| 97 126 | 
             
                #   puts chain_hash[:results].size
         | 
| 127 | 
            +
                #
         | 
| 128 | 
            +
                # @rbs () -> Hash[Symbol, untyped]
         | 
| 98 129 | 
             
                def to_h
         | 
| 99 130 | 
             
                  {
         | 
| 100 131 | 
             
                    id: id,
         | 
| @@ -108,6 +139,8 @@ module CMDx | |
| 108 139 | 
             
                #
         | 
| 109 140 | 
             
                # @example
         | 
| 110 141 | 
             
                #   puts chain.to_s
         | 
| 142 | 
            +
                #
         | 
| 143 | 
            +
                # @rbs () -> String
         | 
| 111 144 | 
             
                def to_s
         | 
| 112 145 | 
             
                  Utils::Format.to_str(to_h)
         | 
| 113 146 | 
             
                end
         | 
| @@ -7,6 +7,14 @@ module CMDx | |
| 7 7 | 
             
              # for various data types including arrays, numbers, dates, and other primitives.
         | 
| 8 8 | 
             
              class CoercionRegistry
         | 
| 9 9 |  | 
| 10 | 
            +
                # Returns the internal registry mapping coercion types to handler classes.
         | 
| 11 | 
            +
                #
         | 
| 12 | 
            +
                # @return [Hash{Symbol => Class}] Hash of coercion type names to coercion classes
         | 
| 13 | 
            +
                #
         | 
| 14 | 
            +
                # @example
         | 
| 15 | 
            +
                #   registry.registry # => { integer: Coercions::Integer, boolean: Coercions::Boolean }
         | 
| 16 | 
            +
                #
         | 
| 17 | 
            +
                # @rbs @registry: Hash[Symbol, Class]
         | 
| 10 18 | 
             
                attr_reader :registry
         | 
| 11 19 | 
             
                alias to_h registry
         | 
| 12 20 |  | 
| @@ -17,6 +25,8 @@ module CMDx | |
| 17 25 | 
             
                # @example
         | 
| 18 26 | 
             
                #   registry = CoercionRegistry.new
         | 
| 19 27 | 
             
                #   registry = CoercionRegistry.new(custom: CustomCoercion)
         | 
| 28 | 
            +
                #
         | 
| 29 | 
            +
                # @rbs (?Hash[Symbol, Class]? registry) -> void
         | 
| 20 30 | 
             
                def initialize(registry = nil)
         | 
| 21 31 | 
             
                  @registry = registry || {
         | 
| 22 32 | 
             
                    array: Coercions::Array,
         | 
| @@ -40,6 +50,8 @@ module CMDx | |
| 40 50 | 
             
                #
         | 
| 41 51 | 
             
                # @example
         | 
| 42 52 | 
             
                #   new_registry = registry.dup
         | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
                # @rbs () -> CoercionRegistry
         | 
| 43 55 | 
             
                def dup
         | 
| 44 56 | 
             
                  self.class.new(registry.dup)
         | 
| 45 57 | 
             
                end
         | 
| @@ -54,6 +66,8 @@ module CMDx | |
| 54 66 | 
             
                # @example
         | 
| 55 67 | 
             
                #   registry.register(:custom_type, CustomCoercion)
         | 
| 56 68 | 
             
                #   registry.register("another_type", AnotherCoercion)
         | 
| 69 | 
            +
                #
         | 
| 70 | 
            +
                # @rbs ((Symbol | String) name, Class coercion) -> self
         | 
| 57 71 | 
             
                def register(name, coercion)
         | 
| 58 72 | 
             
                  registry[name.to_sym] = coercion
         | 
| 59 73 | 
             
                  self
         | 
| @@ -68,6 +82,8 @@ module CMDx | |
| 68 82 | 
             
                # @example
         | 
| 69 83 | 
             
                #   registry.deregister(:custom_type)
         | 
| 70 84 | 
             
                #   registry.deregister("another_type")
         | 
| 85 | 
            +
                #
         | 
| 86 | 
            +
                # @rbs ((Symbol | String) name) -> self
         | 
| 71 87 | 
             
                def deregister(name)
         | 
| 72 88 | 
             
                  registry.delete(name.to_sym)
         | 
| 73 89 | 
             
                  self
         | 
| @@ -87,6 +103,8 @@ module CMDx | |
| 87 103 | 
             
                # @example
         | 
| 88 104 | 
             
                #   result = registry.coerce(:integer, task, "42")
         | 
| 89 105 | 
             
                #   result = registry.coerce(:boolean, task, "true", strict: true)
         | 
| 106 | 
            +
                #
         | 
| 107 | 
            +
                # @rbs (Symbol type, untyped task, untyped value, ?Hash[Symbol, untyped] options) -> untyped
         | 
| 90 108 | 
             
                def coerce(type, task, value, options = {})
         | 
| 91 109 | 
             
                  raise TypeError, "unknown coercion type #{type.inspect}" unless registry.key?(type)
         | 
| 92 110 |  | 
    
        data/lib/cmdx/coercions/array.rb
    CHANGED
    
    | @@ -26,6 +26,8 @@ module CMDx | |
| 26 26 | 
             
                  #   Array.call("hello")     # => ["hello"]
         | 
| 27 27 | 
             
                  #   Array.call(42)          # => [42]
         | 
| 28 28 | 
             
                  #   Array.call(nil)         # => []
         | 
| 29 | 
            +
                  #
         | 
| 30 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Array[untyped]
         | 
| 29 31 | 
             
                  def call(value, options = {})
         | 
| 30 32 | 
             
                    if value.is_a?(::String) && value.start_with?("[")
         | 
| 31 33 | 
             
                      JSON.parse(value)
         | 
| @@ -10,6 +10,7 @@ module CMDx | |
| 10 10 |  | 
| 11 11 | 
             
                  extend self
         | 
| 12 12 |  | 
| 13 | 
            +
                  # @rbs DEFAULT_PRECISION: Integer
         | 
| 13 14 | 
             
                  DEFAULT_PRECISION = 14
         | 
| 14 15 |  | 
| 15 16 | 
             
                  # Converts a value to a BigDecimal
         | 
| @@ -28,6 +29,8 @@ module CMDx | |
| 28 29 | 
             
                  # @example Convert other numeric types
         | 
| 29 30 | 
             
                  #   BigDecimal.call(42)                         # => #<BigDecimal:7f8b8c0d8e0f '0.42E2',9(18)>
         | 
| 30 31 | 
             
                  #   BigDecimal.call(3.14159)                    # => #<BigDecimal:7f8b8c0d8e0f '0.314159E1',9(18)>
         | 
| 32 | 
            +
                  #
         | 
| 33 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> BigDecimal
         | 
| 31 34 | 
             
                  def call(value, options = {})
         | 
| 32 35 | 
             
                    BigDecimal(value, options[:precision] || DEFAULT_PRECISION)
         | 
| 33 36 | 
             
                  rescue ArgumentError, TypeError
         | 
| @@ -10,7 +10,10 @@ module CMDx | |
| 10 10 |  | 
| 11 11 | 
             
                  extend self
         | 
| 12 12 |  | 
| 13 | 
            +
                  # @rbs FALSEY: Regexp
         | 
| 13 14 | 
             
                  FALSEY = /^(false|f|no|n|0)$/i
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  # @rbs TRUTHY: Regexp
         | 
| 14 17 | 
             
                  TRUTHY = /^(true|t|yes|y|1)$/i
         | 
| 15 18 |  | 
| 16 19 | 
             
                  # Converts a value to a Boolean
         | 
| @@ -34,6 +37,8 @@ module CMDx | |
| 34 37 | 
             
                  # @example Handle case-insensitive input
         | 
| 35 38 | 
             
                  #   Boolean.call("TRUE")   # => true
         | 
| 36 39 | 
             
                  #   Boolean.call("False")  # => false
         | 
| 40 | 
            +
                  #
         | 
| 41 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> bool
         | 
| 37 42 | 
             
                  def call(value, options = {})
         | 
| 38 43 | 
             
                    case value.to_s.downcase
         | 
| 39 44 | 
             
                    when FALSEY then false
         | 
| @@ -26,6 +26,8 @@ module CMDx | |
| 26 26 | 
             
                  #   Complex.call(5)                          # => (5+0i)
         | 
| 27 27 | 
             
                  #   Complex.call(3.14)                       # => (3.14+0i)
         | 
| 28 28 | 
             
                  #   Complex.call(Complex(1, 2))              # => (1+2i)
         | 
| 29 | 
            +
                  #
         | 
| 30 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Complex
         | 
| 29 31 | 
             
                  def call(value, options = {})
         | 
| 30 32 | 
             
                    Complex(value)
         | 
| 31 33 | 
             
                  rescue ArgumentError, TypeError
         | 
    
        data/lib/cmdx/coercions/date.rb
    CHANGED
    
    | @@ -12,6 +12,8 @@ module CMDx | |
| 12 12 | 
             
                  extend self
         | 
| 13 13 |  | 
| 14 14 | 
             
                  # Types that are already date-like and don't need conversion
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  # @rbs ANALOG_TYPES: Array[String]
         | 
| 15 17 | 
             
                  ANALOG_TYPES = %w[Date DateTime Time].freeze
         | 
| 16 18 |  | 
| 17 19 | 
             
                  # Converts a value to a Date object
         | 
| @@ -33,6 +35,8 @@ module CMDx | |
| 33 35 | 
             
                  # @example Return existing Date objects unchanged
         | 
| 34 36 | 
             
                  #   Date.call(Date.new(2023, 12, 25)) # => #<Date: 2023-12-25>
         | 
| 35 37 | 
             
                  #   Date.call(DateTime.new(2023, 12, 25)) # => #<Date: 2023-12-25>
         | 
| 38 | 
            +
                  #
         | 
| 39 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Date
         | 
| 36 40 | 
             
                  def call(value, options = {})
         | 
| 37 41 | 
             
                    return value if ANALOG_TYPES.include?(value.class.name)
         | 
| 38 42 | 
             
                    return ::Date.strptime(value, options[:strptime]) if options[:strptime]
         | 
| @@ -11,6 +11,9 @@ module CMDx | |
| 11 11 |  | 
| 12 12 | 
             
                  extend self
         | 
| 13 13 |  | 
| 14 | 
            +
                  # Types that are already date-time-like and don't need conversion
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  # @rbs ANALOG_TYPES: Array[String]
         | 
| 14 17 | 
             
                  ANALOG_TYPES = %w[Date DateTime Time].freeze
         | 
| 15 18 |  | 
| 16 19 | 
             
                  # Converts a value to a DateTime
         | 
| @@ -32,6 +35,8 @@ module CMDx | |
| 32 35 | 
             
                  # @example Convert existing date objects
         | 
| 33 36 | 
             
                  #   DateTime.call(Date.new(2023, 12, 25))     # => #<DateTime: 2023-12-25T00:00:00+00:00>
         | 
| 34 37 | 
             
                  #   DateTime.call(Time.new(2023, 12, 25))     # => #<DateTime: 2023-12-25T00:00:00+00:00>
         | 
| 38 | 
            +
                  #
         | 
| 39 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> DateTime
         | 
| 35 40 | 
             
                  def call(value, options = {})
         | 
| 36 41 | 
             
                    return value if ANALOG_TYPES.include?(value.class.name)
         | 
| 37 42 | 
             
                    return ::DateTime.strptime(value, options[:strptime]) if options[:strptime]
         | 
    
        data/lib/cmdx/coercions/float.rb
    CHANGED
    
    | @@ -30,6 +30,8 @@ module CMDx | |
| 30 30 | 
             
                  #   Float.call(BigDecimal("123.456")) # => 123.456
         | 
| 31 31 | 
             
                  #   Float.call(Rational(3, 4))       # => 0.75
         | 
| 32 32 | 
             
                  #   Float.call(Complex(5.0, 0))      # => 5.0
         | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Float
         | 
| 33 35 | 
             
                  def call(value, options = {})
         | 
| 34 36 | 
             
                    Float(value)
         | 
| 35 37 | 
             
                  rescue ArgumentError, RangeError, TypeError
         | 
    
        data/lib/cmdx/coercions/hash.rb
    CHANGED
    
    | @@ -30,6 +30,8 @@ module CMDx | |
| 30 30 | 
             
                  #   Hash.call([:a, 1, :b, 2]) # => {a: 1, b: 2}
         | 
| 31 31 | 
             
                  # @example Coerce from JSON string
         | 
| 32 32 | 
             
                  #   Hash.call('{"key": "value"}') # => {"key" => "value"}
         | 
| 33 | 
            +
                  #
         | 
| 34 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Hash[untyped, untyped]
         | 
| 33 35 | 
             
                  def call(value, options = {})
         | 
| 34 36 | 
             
                    if value.nil?
         | 
| 35 37 | 
             
                      {}
         | 
| @@ -39,6 +41,8 @@ module CMDx | |
| 39 41 | 
             
                      ::Hash[*value]
         | 
| 40 42 | 
             
                    elsif value.is_a?(::String) && value.start_with?("{")
         | 
| 41 43 | 
             
                      JSON.parse(value)
         | 
| 44 | 
            +
                    elsif value.respond_to?(:to_h)
         | 
| 45 | 
            +
                      value.to_h
         | 
| 42 46 | 
             
                    else
         | 
| 43 47 | 
             
                      raise_coercion_error!
         | 
| 44 48 | 
             
                    end
         | 
| @@ -34,6 +34,8 @@ module CMDx | |
| 34 34 | 
             
                  #   Integer.call(nil)       # => 0
         | 
| 35 35 | 
             
                  #   Integer.call(false)     # => 0
         | 
| 36 36 | 
             
                  #   Integer.call(true)      # => 1
         | 
| 37 | 
            +
                  #
         | 
| 38 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Integer
         | 
| 37 39 | 
             
                  def call(value, options = {})
         | 
| 38 40 | 
             
                    Integer(value)
         | 
| 39 41 | 
             
                  rescue ArgumentError, FloatDomainError, RangeError, TypeError
         | 
| @@ -33,6 +33,8 @@ module CMDx | |
| 33 33 | 
             
                  #   Rational.call("")        # => (0/1)
         | 
| 34 34 | 
             
                  #   Rational.call(nil)       # => (0/1)
         | 
| 35 35 | 
             
                  #   Rational.call(0)         # => (0/1)
         | 
| 36 | 
            +
                  #
         | 
| 37 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Rational
         | 
| 36 38 | 
             
                  def call(value, options = {})
         | 
| 37 39 | 
             
                    Rational(value)
         | 
| 38 40 | 
             
                  rescue ArgumentError, FloatDomainError, RangeError, TypeError, ZeroDivisionError
         | 
| @@ -25,6 +25,8 @@ module CMDx | |
| 25 25 | 
             
                  #   Symbol.call("user_id")         # => :user_id
         | 
| 26 26 | 
             
                  #   Symbol.call("")                # => :""
         | 
| 27 27 | 
             
                  #   Symbol.call(:existing)         # => :existing
         | 
| 28 | 
            +
                  #
         | 
| 29 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Symbol
         | 
| 28 30 | 
             
                  def call(value, options = {})
         | 
| 29 31 | 
             
                    value.to_sym
         | 
| 30 32 | 
             
                  rescue NoMethodError
         | 
    
        data/lib/cmdx/coercions/time.rb
    CHANGED
    
    | @@ -11,6 +11,9 @@ module CMDx | |
| 11 11 |  | 
| 12 12 | 
             
                  extend self
         | 
| 13 13 |  | 
| 14 | 
            +
                  # Types that are already time-like and don't need conversion
         | 
| 15 | 
            +
                  #
         | 
| 16 | 
            +
                  # @rbs ANALOG_TYPES: Array[String]
         | 
| 14 17 | 
             
                  ANALOG_TYPES = %w[DateTime Time].freeze
         | 
| 15 18 |  | 
| 16 19 | 
             
                  # Converts a value to a Time object
         | 
| @@ -34,6 +37,8 @@ module CMDx | |
| 34 37 | 
             
                  # @example Convert strings with custom format
         | 
| 35 38 | 
             
                  #   Time.call("25/12/2023", strptime: "%d/%m/%Y")  # => Time object
         | 
| 36 39 | 
             
                  #   Time.call("12-25-2023", strptime: "%m-%d-%Y")  # => Time object
         | 
| 40 | 
            +
                  #
         | 
| 41 | 
            +
                  # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> Time
         | 
| 37 42 | 
             
                  def call(value, options = {})
         | 
| 38 43 | 
             
                    return value if ANALOG_TYPES.include?(value.class.name)
         | 
| 39 44 | 
             
                    return value.to_time if value.respond_to?(:to_time)
         |