cmdx 1.7.5 → 1.9.0
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/rspec.md +1 -1
- data/.irbrc +14 -2
- data/CHANGELOG.md +62 -29
- data/LLM.md +203 -78
- data/README.md +23 -85
- 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 +19 -29
- data/docs/attributes/defaults.md +3 -16
- data/docs/attributes/definitions.md +29 -39
- data/docs/attributes/naming.md +3 -13
- data/docs/attributes/transformations.md +63 -0
- data/docs/attributes/validations.md +23 -40
- 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 +101 -77
- data/docs/index.md +120 -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 +31 -25
- 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 +9 -2
- data/lib/cmdx/attribute_value.rb +31 -10
- data/lib/cmdx/callback_registry.rb +12 -2
- data/lib/cmdx/coercions/hash.rb +6 -1
- data/lib/cmdx/configuration.rb +10 -2
- data/lib/cmdx/deprecator.rb +3 -3
- data/lib/cmdx/errors.rb +1 -1
- data/lib/cmdx/executor.rb +97 -9
- data/lib/cmdx/log_formatters/logstash.rb +4 -4
- data/lib/cmdx/pipeline.rb +4 -4
- data/lib/cmdx/railtie.rb +9 -0
- data/lib/cmdx/result.rb +10 -1
- data/lib/cmdx/task.rb +12 -7
- data/lib/cmdx/version.rb +1 -1
- data/lib/cmdx.rb +1 -0
- data/lib/generators/cmdx/templates/install.rb +9 -0
- data/lib/locales/af.yml +2 -2
- data/lib/locales/ar.yml +2 -2
- data/lib/locales/az.yml +2 -2
- data/lib/locales/be.yml +2 -2
- data/lib/locales/bg.yml +2 -2
- data/lib/locales/bn.yml +2 -2
- data/lib/locales/bs.yml +2 -2
- data/lib/locales/ca.yml +2 -2
- data/lib/locales/cnr.yml +2 -2
- data/lib/locales/cs.yml +2 -2
- data/lib/locales/cy.yml +2 -2
- data/lib/locales/da.yml +2 -2
- data/lib/locales/de.yml +2 -2
- data/lib/locales/dz.yml +2 -2
- data/lib/locales/el.yml +2 -2
- data/lib/locales/en.yml +2 -2
- data/lib/locales/eo.yml +2 -2
- data/lib/locales/es.yml +2 -2
- data/lib/locales/et.yml +2 -2
- data/lib/locales/eu.yml +2 -2
- data/lib/locales/fa.yml +2 -2
- data/lib/locales/fi.yml +2 -2
- data/lib/locales/fr.yml +2 -2
- data/lib/locales/fy.yml +2 -2
- data/lib/locales/gd.yml +2 -2
- data/lib/locales/gl.yml +2 -2
- data/lib/locales/he.yml +2 -2
- data/lib/locales/hi.yml +2 -2
- data/lib/locales/hr.yml +2 -2
- data/lib/locales/hu.yml +2 -2
- data/lib/locales/hy.yml +2 -2
- data/lib/locales/id.yml +2 -2
- data/lib/locales/is.yml +2 -2
- data/lib/locales/it.yml +2 -2
- data/lib/locales/ja.yml +2 -2
- data/lib/locales/ka.yml +2 -2
- data/lib/locales/kk.yml +2 -2
- data/lib/locales/km.yml +2 -2
- data/lib/locales/kn.yml +2 -2
- data/lib/locales/ko.yml +2 -2
- data/lib/locales/lb.yml +2 -2
- data/lib/locales/lo.yml +2 -2
- data/lib/locales/lt.yml +2 -2
- data/lib/locales/lv.yml +2 -2
- data/lib/locales/mg.yml +2 -2
- data/lib/locales/mk.yml +2 -2
- data/lib/locales/ml.yml +2 -2
- data/lib/locales/mn.yml +2 -2
- data/lib/locales/mr-IN.yml +2 -2
- data/lib/locales/ms.yml +2 -2
- data/lib/locales/nb.yml +2 -2
- data/lib/locales/ne.yml +2 -2
- data/lib/locales/nl.yml +2 -2
- data/lib/locales/nn.yml +2 -2
- data/lib/locales/oc.yml +2 -2
- data/lib/locales/or.yml +2 -2
- data/lib/locales/pa.yml +2 -2
- data/lib/locales/pl.yml +2 -2
- data/lib/locales/pt.yml +2 -2
- data/lib/locales/rm.yml +2 -2
- data/lib/locales/ro.yml +2 -2
- data/lib/locales/ru.yml +2 -2
- data/lib/locales/sc.yml +2 -2
- data/lib/locales/sk.yml +2 -2
- data/lib/locales/sl.yml +2 -2
- data/lib/locales/sq.yml +2 -2
- data/lib/locales/sr.yml +2 -2
- data/lib/locales/st.yml +2 -2
- data/lib/locales/sv.yml +2 -2
- data/lib/locales/sw.yml +2 -2
- data/lib/locales/ta.yml +2 -2
- data/lib/locales/te.yml +2 -2
- data/lib/locales/th.yml +2 -2
- data/lib/locales/tl.yml +2 -2
- data/lib/locales/tr.yml +2 -2
- data/lib/locales/tt.yml +2 -2
- data/lib/locales/ug.yml +2 -2
- data/lib/locales/uk.yml +2 -2
- data/lib/locales/ur.yml +2 -2
- data/lib/locales/uz.yml +2 -2
- data/lib/locales/vi.yml +2 -2
- data/lib/locales/wo.yml +2 -2
- data/lib/locales/zh-CN.yml +2 -2
- data/lib/locales/zh-HK.yml +2 -2
- data/lib/locales/zh-TW.yml +2 -2
- data/lib/locales/zh-YUE.yml +2 -2
- 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 +15 -4
- data/lib/cmdx/freezer.rb +0 -51
- data/src/cmdx-logo.png +0 -0
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            # Paper Trail Whatdunnit
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Tag paper trail version records with which service made a change with a custom `whatdunnit` attribute.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            <https://github.com/paper-trail-gem/paper_trail?tab=readme-ov-file#4c-storing-metadata>
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            ### Setup
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ```ruby
         | 
| 10 | 
            +
            # lib/cmdx_paper_trail_middleware.rb
         | 
| 11 | 
            +
            class CmdxPaperTrailMiddleware
         | 
| 12 | 
            +
              def self.call(task, **options, &)
         | 
| 13 | 
            +
                # This makes sure to reset the whatdunnit value to the previous
         | 
| 14 | 
            +
                # value for nested task calls
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                begin
         | 
| 17 | 
            +
                  PaperTrail.request.controller_info ||= {}
         | 
| 18 | 
            +
                  old_whatdunnit = PaperTrail.request.controller_info[:whatdunnit]
         | 
| 19 | 
            +
                  PaperTrail.request.controller_info[:whatdunnit] = task.class.name
         | 
| 20 | 
            +
                  yield
         | 
| 21 | 
            +
                ensure
         | 
| 22 | 
            +
                  PaperTrail.request.controller_info[:whatdunnit] = old_whatdunnit
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
            end
         | 
| 26 | 
            +
            ```
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            ### Usage
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            ```ruby
         | 
| 31 | 
            +
            class MyTask < CMDx::Task
         | 
| 32 | 
            +
              register :middleware, CmdxPaperTrailMiddleware
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              def work
         | 
| 35 | 
            +
                # Do work...
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
            end
         | 
| 39 | 
            +
            ```
         | 
    
        data/lib/cmdx/attribute.rb
    CHANGED
    
    | @@ -66,7 +66,7 @@ module CMDx | |
| 66 66 | 
             
                    if names.none?
         | 
| 67 67 | 
             
                      raise ArgumentError, "no attributes given"
         | 
| 68 68 | 
             
                    elsif (names.size > 1) && options.key?(:as)
         | 
| 69 | 
            -
                      raise ArgumentError, ":as option only supports one attribute per definition"
         | 
| 69 | 
            +
                      raise ArgumentError, "the :as option only supports one attribute per definition"
         | 
| 70 70 | 
             
                    end
         | 
| 71 71 |  | 
| 72 72 | 
             
                    names.filter_map { |name| new(name, **options, &) }
         | 
| @@ -212,7 +212,14 @@ module CMDx | |
| 212 212 | 
             
                #
         | 
| 213 213 | 
             
                # @raise [RuntimeError] When the method name is already defined on the task
         | 
| 214 214 | 
             
                def define_and_verify
         | 
| 215 | 
            -
                   | 
| 215 | 
            +
                  if task.respond_to?(method_name, true)
         | 
| 216 | 
            +
                    raise <<~MESSAGE
         | 
| 217 | 
            +
                      The method #{method_name.inspect} is already defined on the #{task.class.name} task.
         | 
| 218 | 
            +
                      This may be due conflicts with one of the task's user defined or internal methods/attributes.
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                      Use :as, :prefix, and/or :suffix attribute options to avoid conflicts with existing methods.
         | 
| 221 | 
            +
                    MESSAGE
         | 
| 222 | 
            +
                  end
         | 
| 216 223 |  | 
| 217 224 | 
             
                  attribute_value = AttributeValue.new(self)
         | 
| 218 225 | 
             
                  attribute_value.generate
         | 
    
        data/lib/cmdx/attribute_value.rb
    CHANGED
    
    | @@ -51,9 +51,10 @@ module CMDx | |
| 51 51 | 
             
                  return if errors.for?(method_name)
         | 
| 52 52 |  | 
| 53 53 | 
             
                  coerced_value = coerce_value(derived_value)
         | 
| 54 | 
            +
                  transformed_value = transform_value(coerced_value)
         | 
| 54 55 | 
             
                  return if errors.for?(method_name)
         | 
| 55 56 |  | 
| 56 | 
            -
                  attributes[method_name] =  | 
| 57 | 
            +
                  attributes[method_name] = transformed_value
         | 
| 57 58 | 
             
                end
         | 
| 58 59 |  | 
| 59 60 | 
             
                # Validates the current attribute value against configured validators.
         | 
| @@ -113,7 +114,7 @@ module CMDx | |
| 113 114 | 
             
                #
         | 
| 114 115 | 
             
                # @example
         | 
| 115 116 | 
             
                #   # Default can be symbol, proc, or direct value
         | 
| 116 | 
            -
                #    | 
| 117 | 
            +
                #   -> { rand(100) } # => 23
         | 
| 117 118 | 
             
                def default_value
         | 
| 118 119 | 
             
                  default = options[:default]
         | 
| 119 120 |  | 
| @@ -138,7 +139,7 @@ module CMDx | |
| 138 139 | 
             
                #
         | 
| 139 140 | 
             
                # @example
         | 
| 140 141 | 
             
                #   # Derives from hash key, method call, or proc execution
         | 
| 141 | 
            -
                #    | 
| 142 | 
            +
                #   context.user_id # => 42
         | 
| 142 143 | 
             
                def derive_value(source_value)
         | 
| 143 144 | 
             
                  derived_value =
         | 
| 144 145 | 
             
                    case source_value
         | 
| @@ -154,9 +155,29 @@ module CMDx | |
| 154 155 | 
             
                  nil
         | 
| 155 156 | 
             
                end
         | 
| 156 157 |  | 
| 158 | 
            +
                # Transforms the derived value using the transform option.
         | 
| 159 | 
            +
                #
         | 
| 160 | 
            +
                # @param derived_value [Object] The value to transform
         | 
| 161 | 
            +
                #
         | 
| 162 | 
            +
                # @return [Object, nil] The transformed value or nil if transformation failed
         | 
| 163 | 
            +
                #
         | 
| 164 | 
            +
                # @example
         | 
| 165 | 
            +
                #   :downcase # => "hello"
         | 
| 166 | 
            +
                def transform_value(derived_value)
         | 
| 167 | 
            +
                  transform = options[:transform]
         | 
| 168 | 
            +
             | 
| 169 | 
            +
                  if transform.is_a?(Symbol) && derived_value.respond_to?(transform, true)
         | 
| 170 | 
            +
                    derived_value.send(transform)
         | 
| 171 | 
            +
                  elsif transform.respond_to?(:call)
         | 
| 172 | 
            +
                    transform.call(derived_value)
         | 
| 173 | 
            +
                  else
         | 
| 174 | 
            +
                    derived_value
         | 
| 175 | 
            +
                  end
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
             | 
| 157 178 | 
             
                # Coerces the derived value to the expected type(s) using the coercion registry.
         | 
| 158 179 | 
             
                #
         | 
| 159 | 
            -
                # @param  | 
| 180 | 
            +
                # @param transformed_value [Object] The value to coerce
         | 
| 160 181 | 
             
                #
         | 
| 161 182 | 
             
                # @return [Object, nil] The coerced value or nil if coercion failed
         | 
| 162 183 | 
             
                #
         | 
| @@ -165,14 +186,14 @@ module CMDx | |
| 165 186 | 
             
                # @example
         | 
| 166 187 | 
             
                #   # Coerces "42" to Integer, "true" to Boolean, etc.
         | 
| 167 188 | 
             
                #   coerce_value("42") # => 42
         | 
| 168 | 
            -
                def coerce_value( | 
| 169 | 
            -
                  return  | 
| 189 | 
            +
                def coerce_value(transformed_value)
         | 
| 190 | 
            +
                  return transformed_value if types.empty?
         | 
| 170 191 |  | 
| 171 192 | 
             
                  registry = task.class.settings[:coercions]
         | 
| 172 | 
            -
                  last_idx =  | 
| 193 | 
            +
                  last_idx = types.size - 1
         | 
| 173 194 |  | 
| 174 | 
            -
                   | 
| 175 | 
            -
                    break registry.coerce(type, task,  | 
| 195 | 
            +
                  types.find.with_index do |type, i|
         | 
| 196 | 
            +
                    break registry.coerce(type, task, transformed_value, options)
         | 
| 176 197 | 
             
                  rescue CoercionError => e
         | 
| 177 198 | 
             
                    next if i != last_idx
         | 
| 178 199 |  | 
| @@ -180,7 +201,7 @@ module CMDx | |
| 180 201 | 
             
                      if last_idx.zero?
         | 
| 181 202 | 
             
                        e.message
         | 
| 182 203 | 
             
                      else
         | 
| 183 | 
            -
                        tl =  | 
| 204 | 
            +
                        tl = types.map { |t| Locale.t("cmdx.types.#{t}") }.join(", ")
         | 
| 184 205 | 
             
                        Locale.t("cmdx.coercions.into_any", types: tl)
         | 
| 185 206 | 
             
                      end
         | 
| 186 207 |  | 
| @@ -95,9 +95,19 @@ module CMDx | |
| 95 95 | 
             
                  raise TypeError, "unknown callback type #{type.inspect}" unless TYPES.include?(type)
         | 
| 96 96 |  | 
| 97 97 | 
             
                  Array(registry[type]).each do |callables, options|
         | 
| 98 | 
            -
                    next unless Utils::Condition.evaluate(task, options | 
| 98 | 
            +
                    next unless Utils::Condition.evaluate(task, options)
         | 
| 99 99 |  | 
| 100 | 
            -
                    Array(callables).each  | 
| 100 | 
            +
                    Array(callables).each do |callable|
         | 
| 101 | 
            +
                      if callable.is_a?(Symbol)
         | 
| 102 | 
            +
                        task.send(callable)
         | 
| 103 | 
            +
                      elsif callable.is_a?(Proc)
         | 
| 104 | 
            +
                        task.instance_exec(&callable)
         | 
| 105 | 
            +
                      elsif callable.respond_to?(:call)
         | 
| 106 | 
            +
                        callable.call(task)
         | 
| 107 | 
            +
                      else
         | 
| 108 | 
            +
                        raise "cannot invoke #{callable}"
         | 
| 109 | 
            +
                      end
         | 
| 110 | 
            +
                    end
         | 
| 101 111 | 
             
                  end
         | 
| 102 112 | 
             
                end
         | 
| 103 113 |  | 
    
        data/lib/cmdx/coercions/hash.rb
    CHANGED
    
    | @@ -5,6 +5,7 @@ module CMDx | |
| 5 5 | 
             
                # Coerces various input types into Hash objects
         | 
| 6 6 | 
             
                #
         | 
| 7 7 | 
             
                # Supports conversion from:
         | 
| 8 | 
            +
                # - Nil values (converted to empty Hash)
         | 
| 8 9 | 
             
                # - Hash objects (returned as-is)
         | 
| 9 10 | 
             
                # - Array objects (converted using Hash[*array])
         | 
| 10 11 | 
             
                # - JSON strings starting with "{" (parsed into Hash)
         | 
| @@ -30,12 +31,16 @@ module CMDx | |
| 30 31 | 
             
                  # @example Coerce from JSON string
         | 
| 31 32 | 
             
                  #   Hash.call('{"key": "value"}') # => {"key" => "value"}
         | 
| 32 33 | 
             
                  def call(value, options = {})
         | 
| 33 | 
            -
                    if value. | 
| 34 | 
            +
                    if value.nil?
         | 
| 35 | 
            +
                      {}
         | 
| 36 | 
            +
                    elsif value.is_a?(::Hash)
         | 
| 34 37 | 
             
                      value
         | 
| 35 38 | 
             
                    elsif value.is_a?(::Array)
         | 
| 36 39 | 
             
                      ::Hash[*value]
         | 
| 37 40 | 
             
                    elsif value.is_a?(::String) && value.start_with?("{")
         | 
| 38 41 | 
             
                      JSON.parse(value)
         | 
| 42 | 
            +
                    elsif value.respond_to?(:to_h)
         | 
| 43 | 
            +
                      value.to_h
         | 
| 39 44 | 
             
                    else
         | 
| 40 45 | 
             
                      raise_coercion_error!
         | 
| 41 46 | 
             
                    end
         | 
    
        data/lib/cmdx/configuration.rb
    CHANGED
    
    | @@ -3,13 +3,14 @@ | |
| 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 9 | 
             
                DEFAULT_BREAKPOINTS = %w[failed].freeze
         | 
| 10 10 |  | 
| 11 11 | 
             
                attr_accessor :middlewares, :callbacks, :coercions, :validators,
         | 
| 12 | 
            -
                              :task_breakpoints, :workflow_breakpoints, :logger
         | 
| 12 | 
            +
                              :task_breakpoints, :workflow_breakpoints, :logger,
         | 
| 13 | 
            +
                              :backtrace, :backtrace_cleaner, :exception_handler
         | 
| 13 14 |  | 
| 14 15 | 
             
                # Initializes a new Configuration instance with default values.
         | 
| 15 16 | 
             
                #
         | 
| @@ -31,6 +32,10 @@ module CMDx | |
| 31 32 | 
             
                  @task_breakpoints = DEFAULT_BREAKPOINTS
         | 
| 32 33 | 
             
                  @workflow_breakpoints = DEFAULT_BREAKPOINTS
         | 
| 33 34 |  | 
| 35 | 
            +
                  @backtrace = false
         | 
| 36 | 
            +
                  @backtrace_cleaner = nil
         | 
| 37 | 
            +
                  @exception_handler = nil
         | 
| 38 | 
            +
             | 
| 34 39 | 
             
                  @logger = Logger.new(
         | 
| 35 40 | 
             
                    $stdout,
         | 
| 36 41 | 
             
                    progname: "cmdx",
         | 
| @@ -55,6 +60,9 @@ module CMDx | |
| 55 60 | 
             
                    validators: @validators,
         | 
| 56 61 | 
             
                    task_breakpoints: @task_breakpoints,
         | 
| 57 62 | 
             
                    workflow_breakpoints: @workflow_breakpoints,
         | 
| 63 | 
            +
                    backtrace: @backtrace,
         | 
| 64 | 
            +
                    backtrace_cleaner: @backtrace_cleaner,
         | 
| 65 | 
            +
                    exception_handler: @exception_handler,
         | 
| 58 66 | 
             
                    logger: @logger
         | 
| 59 67 | 
             
                  }
         | 
| 60 68 | 
             
                end
         | 
    
        data/lib/cmdx/deprecator.rb
    CHANGED
    
    | @@ -44,15 +44,15 @@ module CMDx | |
| 44 44 | 
             
                #     settings(deprecate: :warn)
         | 
| 45 45 | 
             
                #   end
         | 
| 46 46 | 
             
                #
         | 
| 47 | 
            -
                #   MyTask.new # => [MyTask] DEPRECATED: migrate to replacement or discontinue use
         | 
| 47 | 
            +
                #   MyTask.new # => [MyTask] DEPRECATED: migrate to a replacement or discontinue use
         | 
| 48 48 | 
             
                def restrict(task)
         | 
| 49 49 | 
             
                  type = EVAL.call(task, task.class.settings[:deprecate])
         | 
| 50 50 |  | 
| 51 51 | 
             
                  case type
         | 
| 52 52 | 
             
                  when NilClass, FalseClass # Do nothing
         | 
| 53 53 | 
             
                  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)
         | 
| 54 | 
            +
                  when /log/ then task.logger.warn { "DEPRECATED: migrate to a replacement or discontinue use" }
         | 
| 55 | 
            +
                  when /warn/ then warn("[#{task.class.name}] DEPRECATED: migrate to a replacement or discontinue use", category: :deprecated)
         | 
| 56 56 | 
             
                  else raise "unknown deprecation type #{type.inspect}"
         | 
| 57 57 | 
             
                  end
         | 
| 58 58 | 
             
                end
         | 
    
        data/lib/cmdx/errors.rb
    CHANGED
    
    | @@ -48,7 +48,7 @@ module CMDx | |
| 48 48 | 
             
                  !messages[attribute].empty?
         | 
| 49 49 | 
             
                end
         | 
| 50 50 |  | 
| 51 | 
            -
                # Convert errors to a hash format with arrays of  | 
| 51 | 
            +
                # Convert errors to a hash format with arrays of full messages.
         | 
| 52 52 | 
             
                #
         | 
| 53 53 | 
             
                # @return [Hash{Symbol => Array<String>}] Hash with attribute keys and message arrays
         | 
| 54 54 | 
             
                #
         | 
    
        data/lib/cmdx/executor.rb
    CHANGED
    
    | @@ -46,14 +46,16 @@ module CMDx | |
| 46 46 | 
             
                #   result = executor.execute
         | 
| 47 47 | 
             
                def execute
         | 
| 48 48 | 
             
                  task.class.settings[:middlewares].call!(task) do
         | 
| 49 | 
            -
                    pre_execution!
         | 
| 49 | 
            +
                    pre_execution! unless @pre_execution
         | 
| 50 50 | 
             
                    execution!
         | 
| 51 51 | 
             
                  rescue UndefinedMethodError => e
         | 
| 52 52 | 
             
                    raise(e) # No need to clear the Chain since exception is not being re-raised
         | 
| 53 53 | 
             
                  rescue Fault => e
         | 
| 54 54 | 
             
                    task.result.throw!(e.result, halt: false, cause: e)
         | 
| 55 55 | 
             
                  rescue StandardError => e
         | 
| 56 | 
            +
                    retry if retry_execution?(e)
         | 
| 56 57 | 
             
                    task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
         | 
| 58 | 
            +
                    task.class.settings[:exception_handler]&.call(task, e)
         | 
| 57 59 | 
             
                  ensure
         | 
| 58 60 | 
             
                    task.result.executed!
         | 
| 59 61 | 
             
                    post_execution!
         | 
| @@ -73,7 +75,7 @@ module CMDx | |
| 73 75 | 
             
                #   result = executor.execute!
         | 
| 74 76 | 
             
                def execute!
         | 
| 75 77 | 
             
                  task.class.settings[:middlewares].call!(task) do
         | 
| 76 | 
            -
                    pre_execution!
         | 
| 78 | 
            +
                    pre_execution! unless @pre_execution
         | 
| 77 79 | 
             
                    execution!
         | 
| 78 80 | 
             
                  rescue UndefinedMethodError => e
         | 
| 79 81 | 
             
                    raise_exception(e)
         | 
| @@ -81,6 +83,7 @@ module CMDx | |
| 81 83 | 
             
                    task.result.throw!(e.result, halt: false, cause: e)
         | 
| 82 84 | 
             
                    halt_execution?(e) ? raise_exception(e) : post_execution!
         | 
| 83 85 | 
             
                  rescue StandardError => e
         | 
| 86 | 
            +
                    retry if retry_execution?(e)
         | 
| 84 87 | 
             
                    task.result.fail!("[#{e.class}] #{e.message}", halt: false, cause: e)
         | 
| 85 88 | 
             
                    raise_exception(e)
         | 
| 86 89 | 
             
                  else
         | 
| @@ -108,6 +111,38 @@ module CMDx | |
| 108 111 | 
             
                  breakpoints.include?(exception.result.status)
         | 
| 109 112 | 
             
                end
         | 
| 110 113 |  | 
| 114 | 
            +
                # Determines if execution should be retried based on retry configuration.
         | 
| 115 | 
            +
                #
         | 
| 116 | 
            +
                # @param exception [Exception] The exception that occurred
         | 
| 117 | 
            +
                #
         | 
| 118 | 
            +
                # @return [Boolean] Whether execution should be retried
         | 
| 119 | 
            +
                #
         | 
| 120 | 
            +
                # @example
         | 
| 121 | 
            +
                #   retry_execution?(standard_error)
         | 
| 122 | 
            +
                def retry_execution?(exception)
         | 
| 123 | 
            +
                  available_retries = (task.class.settings[:retries] || 0).to_i
         | 
| 124 | 
            +
                  return false unless available_retries.positive?
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                  current_retries = (task.result.metadata[:retries] ||= 0).to_i
         | 
| 127 | 
            +
                  remaining_retries = available_retries - current_retries
         | 
| 128 | 
            +
                  return false unless remaining_retries.positive?
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                  exceptions = Array(task.class.settings[:retry_on] || StandardError)
         | 
| 131 | 
            +
                  return false unless exceptions.any? { |e| exception.class <= e }
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  task.result.metadata[:retries] += 1
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                  task.logger.warn do
         | 
| 136 | 
            +
                    reason = "[#{exception.class}] #{exception.message}"
         | 
| 137 | 
            +
                    task.to_h.merge!(reason:, remaining_retries:)
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                  jitter = task.class.settings[:retry_jitter].to_f * current_retries
         | 
| 141 | 
            +
                  sleep(jitter) if jitter.positive?
         | 
| 142 | 
            +
             | 
| 143 | 
            +
                  true
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 111 146 | 
             
                # Raises an exception and clears the chain.
         | 
| 112 147 | 
             
                #
         | 
| 113 148 | 
             
                # @param exception [Exception] The exception to raise
         | 
| @@ -118,6 +153,7 @@ module CMDx | |
| 118 153 | 
             
                #   raise_exception(standard_error)
         | 
| 119 154 | 
             
                def raise_exception(exception)
         | 
| 120 155 | 
             
                  Chain.clear
         | 
| 156 | 
            +
             | 
| 121 157 | 
             
                  raise(exception)
         | 
| 122 158 | 
             
                end
         | 
| 123 159 |  | 
| @@ -135,8 +171,15 @@ module CMDx | |
| 135 171 |  | 
| 136 172 | 
             
                private
         | 
| 137 173 |  | 
| 174 | 
            +
                # Lazy loaded repeator instance to handle retries.
         | 
| 175 | 
            +
                def repeator
         | 
| 176 | 
            +
                  @repeator ||= Repeator.new(task)
         | 
| 177 | 
            +
                end
         | 
| 178 | 
            +
             | 
| 138 179 | 
             
                # Performs pre-execution tasks including validation and attribute verification.
         | 
| 139 180 | 
             
                def pre_execution!
         | 
| 181 | 
            +
                  @pre_execution = true
         | 
| 182 | 
            +
             | 
| 140 183 | 
             
                  invoke_callbacks(:before_validation)
         | 
| 141 184 |  | 
| 142 185 | 
             
                  task.class.settings[:attributes].define_and_verify(task)
         | 
| @@ -144,8 +187,10 @@ module CMDx | |
| 144 187 |  | 
| 145 188 | 
             
                  task.result.fail!(
         | 
| 146 189 | 
             
                    Locale.t("cmdx.faults.invalid"),
         | 
| 147 | 
            -
                     | 
| 148 | 
            -
             | 
| 190 | 
            +
                    errors: {
         | 
| 191 | 
            +
                      full_message: task.errors.to_s,
         | 
| 192 | 
            +
                      messages: task.errors.to_h
         | 
| 193 | 
            +
                    }
         | 
| 149 194 | 
             
                  )
         | 
| 150 195 | 
             
                end
         | 
| 151 196 |  | 
| @@ -169,13 +214,56 @@ module CMDx | |
| 169 214 |  | 
| 170 215 | 
             
                # Finalizes execution by freezing the task and logging results.
         | 
| 171 216 | 
             
                def finalize_execution!
         | 
| 172 | 
            -
                   | 
| 173 | 
            -
             | 
| 174 | 
            -
             | 
| 175 | 
            -
             | 
| 217 | 
            +
                  log_execution!
         | 
| 218 | 
            +
                  log_backtrace! if task.class.settings[:backtrace]
         | 
| 219 | 
            +
             | 
| 220 | 
            +
                  freeze_execution!
         | 
| 221 | 
            +
                  clear_chain!
         | 
| 222 | 
            +
                end
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                # Logs the execution result at the configured log level.
         | 
| 225 | 
            +
                def log_execution!
         | 
| 226 | 
            +
                  task.logger.info { task.result.to_h }
         | 
| 227 | 
            +
                end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                # Logs the backtrace of the exception if the task failed.
         | 
| 230 | 
            +
                def log_backtrace!
         | 
| 231 | 
            +
                  return unless task.result.failed?
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  exception = task.result.caused_failure.cause
         | 
| 234 | 
            +
                  return if exception.is_a?(Fault)
         | 
| 235 | 
            +
             | 
| 236 | 
            +
                  task.logger.error do
         | 
| 237 | 
            +
                    "[#{exception.class}] #{exception.message}\n" <<
         | 
| 238 | 
            +
                      if (cleaner = task.class.settings[:backtrace_cleaner])
         | 
| 239 | 
            +
                        cleaner.call(exception.backtrace).join("\n\t")
         | 
| 240 | 
            +
                      else
         | 
| 241 | 
            +
                        exception.full_message(highlight: false)
         | 
| 242 | 
            +
                      end
         | 
| 176 243 | 
             
                  end
         | 
| 244 | 
            +
                end
         | 
| 245 | 
            +
             | 
| 246 | 
            +
                # Freezes the task and its associated objects to prevent modifications.
         | 
| 247 | 
            +
                def freeze_execution!
         | 
| 248 | 
            +
                  # Stubbing on frozen objects is not allowed in most test environments.
         | 
| 249 | 
            +
                  skip_freezing = ENV.fetch("SKIP_CMDX_FREEZING", false)
         | 
| 250 | 
            +
                  return if Coercions::Boolean.call(skip_freezing)
         | 
| 251 | 
            +
             | 
| 252 | 
            +
                  task.freeze
         | 
| 253 | 
            +
                  task.result.freeze
         | 
| 254 | 
            +
             | 
| 255 | 
            +
                  # Freezing the context and chain can only be done
         | 
| 256 | 
            +
                  # once the outer-most task has completed.
         | 
| 257 | 
            +
                  return unless task.result.index.zero?
         | 
| 177 258 |  | 
| 178 | 
            -
                   | 
| 259 | 
            +
                  task.context.freeze
         | 
| 260 | 
            +
                  task.chain.freeze
         | 
| 261 | 
            +
                end
         | 
| 262 | 
            +
             | 
| 263 | 
            +
                def clear_chain!
         | 
| 264 | 
            +
                  return unless task.result.index.zero?
         | 
| 265 | 
            +
             | 
| 266 | 
            +
                  Chain.clear
         | 
| 179 267 | 
             
                end
         | 
| 180 268 |  | 
| 181 269 | 
             
              end
         | 
| @@ -21,15 +21,15 @@ module CMDx | |
| 21 21 | 
             
                  #
         | 
| 22 22 | 
             
                  # @example Basic usage
         | 
| 23 23 | 
             
                  #   logger_formatter.call("INFO", Time.now, "MyApp", "User logged in")
         | 
| 24 | 
            -
                  #   # => '{" | 
| 24 | 
            +
                  #   # => '{"severity":"INFO","progname":"MyApp","pid":12345,"message":"User logged in","@version":"1","@timestamp":"2024-01-15T10:30:45.123456Z"}\n'
         | 
| 25 25 | 
             
                  def call(severity, time, progname, message)
         | 
| 26 26 | 
             
                    hash = {
         | 
| 27 | 
            -
                      "@version" => "1",
         | 
| 28 | 
            -
                      "@timestamp" => time.utc.iso8601(6),
         | 
| 29 27 | 
             
                      severity:,
         | 
| 30 28 | 
             
                      progname:,
         | 
| 31 29 | 
             
                      pid: Process.pid,
         | 
| 32 | 
            -
                      message: Utils::Format.to_log(message)
         | 
| 30 | 
            +
                      message: Utils::Format.to_log(message),
         | 
| 31 | 
            +
                      "@version" => "1",
         | 
| 32 | 
            +
                      "@timestamp" => time.utc.iso8601(6)
         | 
| 33 33 | 
             
                    }
         | 
| 34 34 |  | 
| 35 35 | 
             
                    ::JSON.dump(hash) << "\n"
         | 
    
        data/lib/cmdx/pipeline.rb
    CHANGED
    
    | @@ -42,7 +42,7 @@ module CMDx | |
| 42 42 | 
             
                #   pipeline.execute
         | 
| 43 43 | 
             
                def execute
         | 
| 44 44 | 
             
                  workflow.class.pipeline.each do |group|
         | 
| 45 | 
            -
                    next unless Utils::Condition.evaluate(workflow, group.options | 
| 45 | 
            +
                    next unless Utils::Condition.evaluate(workflow, group.options)
         | 
| 46 46 |  | 
| 47 47 | 
             
                    breakpoints = group.options[:breakpoints] ||
         | 
| 48 48 | 
             
                                  workflow.class.settings[:breakpoints] ||
         | 
| @@ -108,18 +108,18 @@ module CMDx | |
| 108 108 | 
             
                # @example
         | 
| 109 109 | 
             
                #   execute_tasks_in_parallel(group, ["failed"])
         | 
| 110 110 | 
             
                def execute_tasks_in_parallel(group, breakpoints)
         | 
| 111 | 
            -
                  raise "install the `parallel` gem to use this feature" unless defined?( | 
| 111 | 
            +
                  raise "install the `parallel` gem to use this feature" unless defined?(Parallel)
         | 
| 112 112 |  | 
| 113 113 | 
             
                  parallel_options = group.options.slice(:in_threads, :in_processes)
         | 
| 114 114 | 
             
                  throwable_result = nil
         | 
| 115 115 |  | 
| 116 | 
            -
                   | 
| 116 | 
            +
                  Parallel.each(group.tasks, **parallel_options) do |task|
         | 
| 117 117 | 
             
                    Chain.current = workflow.chain
         | 
| 118 118 |  | 
| 119 119 | 
             
                    task_result = task.execute(workflow.context)
         | 
| 120 120 | 
             
                    next unless breakpoints.include?(task_result.status)
         | 
| 121 121 |  | 
| 122 | 
            -
                    raise  | 
| 122 | 
            +
                    raise Parallel::Break, throwable_result = task_result
         | 
| 123 123 | 
             
                  end
         | 
| 124 124 |  | 
| 125 125 | 
             
                  return if throwable_result.nil?
         | 
    
        data/lib/cmdx/railtie.rb
    CHANGED
    
    | @@ -32,5 +32,14 @@ module CMDx | |
| 32 32 | 
             
                  ::I18n.reload!
         | 
| 33 33 | 
             
                end
         | 
| 34 34 |  | 
| 35 | 
            +
                # Configures the backtrace cleaner for CMDx in a Rails environment.
         | 
| 36 | 
            +
                #
         | 
| 37 | 
            +
                # Sets the backtrace cleaner to the Rails backtrace cleaner.
         | 
| 38 | 
            +
                initializer("cmdx.backtrace_cleaner") do
         | 
| 39 | 
            +
                  CMDx.configuration.backtrace_cleaner = lambda do |backtrace|
         | 
| 40 | 
            +
                    Rails.backtrace_cleaner.clean(backtrace)
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 35 44 | 
             
              end
         | 
| 36 45 | 
             
            end
         | 
    
        data/lib/cmdx/result.rb
    CHANGED
    
    | @@ -291,7 +291,16 @@ module CMDx | |
| 291 291 |  | 
| 292 292 | 
             
                  # Strip the first two frames (this method and the delegator)
         | 
| 293 293 | 
             
                  frames = caller_locations(3..-1)
         | 
| 294 | 
            -
             | 
| 294 | 
            +
             | 
| 295 | 
            +
                  unless frames.empty?
         | 
| 296 | 
            +
                    frames = frames.map(&:to_s)
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                    if (cleaner = task.class.settings[:backtrace_cleaner])
         | 
| 299 | 
            +
                      cleaner.call(frames)
         | 
| 300 | 
            +
                    end
         | 
| 301 | 
            +
             | 
| 302 | 
            +
                    fault.set_backtrace(frames)
         | 
| 303 | 
            +
                  end
         | 
| 295 304 |  | 
| 296 305 | 
             
                  raise(fault)
         | 
| 297 306 | 
             
                end
         | 
    
        data/lib/cmdx/task.rb
    CHANGED
    
    | @@ -40,9 +40,6 @@ module CMDx | |
| 40 40 | 
             
                class << self
         | 
| 41 41 |  | 
| 42 42 | 
             
                  # @param options [Hash] Configuration options to merge with existing settings
         | 
| 43 | 
            -
                  # @option options [AttributeRegistry] :attributes Registry for task attributes
         | 
| 44 | 
            -
                  # @option options [Boolean] :deprecate Whether the task is deprecated
         | 
| 45 | 
            -
                  # @option options [Array<Symbol>] :tags Tags associated with the task
         | 
| 46 43 | 
             
                  #
         | 
| 47 44 | 
             
                  # @return [Hash] The merged settings hash
         | 
| 48 45 | 
             
                  #
         | 
| @@ -54,13 +51,21 @@ module CMDx | |
| 54 51 | 
             
                    @settings ||= begin
         | 
| 55 52 | 
             
                      hash =
         | 
| 56 53 | 
             
                        if superclass.respond_to?(:settings)
         | 
| 57 | 
            -
                          superclass.settings
         | 
| 54 | 
            +
                          parent = superclass.settings
         | 
| 55 | 
            +
                          parent
         | 
| 56 | 
            +
                            .except(:backtrace_cleaner, :exception_handler, :logger, :deprecate)
         | 
| 57 | 
            +
                            .transform_values!(&:dup)
         | 
| 58 | 
            +
                            .merge!(
         | 
| 59 | 
            +
                              backtrace_cleaner: parent[:backtrace_cleaner] || CMDx.configuration.backtrace_cleaner,
         | 
| 60 | 
            +
                              exception_handler: parent[:exception_handler] || CMDx.configuration.exception_handler,
         | 
| 61 | 
            +
                              logger: parent[:logger] || CMDx.configuration.logger,
         | 
| 62 | 
            +
                              deprecate: parent[:deprecate]
         | 
| 63 | 
            +
                            )
         | 
| 58 64 | 
             
                        else
         | 
| 59 | 
            -
                          CMDx.configuration.to_h | 
| 60 | 
            -
                        end | 
| 65 | 
            +
                          CMDx.configuration.to_h
         | 
| 66 | 
            +
                        end
         | 
| 61 67 |  | 
| 62 68 | 
             
                      hash[:attributes] ||= AttributeRegistry.new
         | 
| 63 | 
            -
                      hash[:deprecate] ||= false
         | 
| 64 69 | 
             
                      hash[:tags] ||= []
         | 
| 65 70 |  | 
| 66 71 | 
             
                      hash.merge!(options)
         | 
    
        data/lib/cmdx/version.rb
    CHANGED
    
    
    
        data/lib/cmdx.rb
    CHANGED
    
    
| @@ -32,6 +32,15 @@ CMDx.configure do |config| | |
| 32 32 | 
             
                level: Logger::INFO
         | 
| 33 33 | 
             
              )
         | 
| 34 34 |  | 
| 35 | 
            +
              # Backtrace configuration - controls whether to log backtraces on faults and exceptions
         | 
| 36 | 
            +
              # https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#backtraces
         | 
| 37 | 
            +
              # config.backtrace = false
         | 
| 38 | 
            +
              # config.backtrace_cleaner = nil
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              # Exception handler configuration - called when non-fault exceptions are raised
         | 
| 41 | 
            +
              # https://github.com/drexed/cmdx/blob/main/docs/getting_started.md#exception-handler
         | 
| 42 | 
            +
              # config.exception_handler = nil
         | 
| 43 | 
            +
             | 
| 35 44 | 
             
              # Additional global configurations - automatically applied to all tasks
         | 
| 36 45 | 
             
              #
         | 
| 37 46 | 
             
              # Middlewares - https://github.com/drexed/cmdx/blob/main/docs/middlewares.md
         | 
    
        data/lib/locales/af.yml
    CHANGED
    
    | @@ -9,8 +9,8 @@ af: | |
| 9 9 | 
             
                  into_any: "kon nie na een van: %{types} omskep word nie"
         | 
| 10 10 | 
             
                  unknown: "onbekende %{type} omskep tipe"
         | 
| 11 11 | 
             
                faults:
         | 
| 12 | 
            -
                  invalid: " | 
| 13 | 
            -
                  unspecified: " | 
| 12 | 
            +
                  invalid: "Ongeldig"
         | 
| 13 | 
            +
                  unspecified: "Ongespesifiseer"
         | 
| 14 14 | 
             
                types:
         | 
| 15 15 | 
             
                  array: "skikking"
         | 
| 16 16 | 
             
                  big_decimal: "groot desimale"
         | 
    
        data/lib/locales/ar.yml
    CHANGED
    
    | @@ -9,8 +9,8 @@ ar: | |
| 9 9 | 
             
                  into_any: "لا يمكن تحويله إلى واحد من: %{types}"
         | 
| 10 10 | 
             
                  unknown: "نوع تحويل %{type} غير معروف"
         | 
| 11 11 | 
             
                faults:
         | 
| 12 | 
            -
                  invalid: " | 
| 13 | 
            -
                  unspecified: " | 
| 12 | 
            +
                  invalid: "غير صالح"
         | 
| 13 | 
            +
                  unspecified: "غير محدد"
         | 
| 14 14 | 
             
                types:
         | 
| 15 15 | 
             
                  array: "مصفوفة"
         | 
| 16 16 | 
             
                  big_decimal: "عدد عشري كبير"
         | 
    
        data/lib/locales/az.yml
    CHANGED
    
    | @@ -9,8 +9,8 @@ az: | |
| 9 9 | 
             
                  into_any: "aşağıdakılardan birinə çevrilə bilmədi: %{types}"
         | 
| 10 10 | 
             
                  unknown: "naməlum %{type} çevrilmə tipi"
         | 
| 11 11 | 
             
                faults:
         | 
| 12 | 
            -
                  invalid: "Etibarsız | 
| 13 | 
            -
                  unspecified: " | 
| 12 | 
            +
                  invalid: "Etibarsız"
         | 
| 13 | 
            +
                  unspecified: "Göstərilməyib"
         | 
| 14 14 | 
             
                types:
         | 
| 15 15 | 
             
                  array: "massiv"
         | 
| 16 16 | 
             
                  big_decimal: "böyük onluq"
         | 
    
        data/lib/locales/be.yml
    CHANGED
    
    | @@ -9,8 +9,8 @@ be: | |
| 9 9 | 
             
                  into_any: "не ўдалося пераўтварыць у адзін з: %{types}"
         | 
| 10 10 | 
             
                  unknown: "невядомы тып пераўтварэння %{type}"
         | 
| 11 11 | 
             
                faults:
         | 
| 12 | 
            -
                  invalid: "Няправільныя | 
| 13 | 
            -
                  unspecified: " | 
| 12 | 
            +
                  invalid: "Няправільныя"
         | 
| 13 | 
            +
                  unspecified: "Не паказана"
         | 
| 14 14 | 
             
                types:
         | 
| 15 15 | 
             
                  array: "масіў"
         | 
| 16 16 | 
             
                  big_decimal: "вялікае дзесятковае лік"
         | 
    
        data/lib/locales/bg.yml
    CHANGED
    
    | @@ -9,8 +9,8 @@ bg: | |
| 9 9 | 
             
                  into_any: "не може да бъде преобразуван в един от: %{types}"
         | 
| 10 10 | 
             
                  unknown: "неизвестен тип преобразуване %{type}"
         | 
| 11 11 | 
             
                faults:
         | 
| 12 | 
            -
                  invalid: "Невалидни | 
| 13 | 
            -
                  unspecified: "Не е посочена | 
| 12 | 
            +
                  invalid: "Невалидни"
         | 
| 13 | 
            +
                  unspecified: "Не е посочена"
         | 
| 14 14 | 
             
                types:
         | 
| 15 15 | 
             
                  array: "масив"
         | 
| 16 16 | 
             
                  big_decimal: "голямо десетично число"
         | 
    
        data/lib/locales/bn.yml
    CHANGED
    
    | @@ -9,8 +9,8 @@ bn: | |
| 9 9 | 
             
                  into_any: "নিম্নলিখিতগুলির মধ্যে একটিতে রূপান্তর করা যায়নি: %{types}"
         | 
| 10 10 | 
             
                  unknown: "অজানা %{type} রূপান্তর প্রকার"
         | 
| 11 11 | 
             
                faults:
         | 
| 12 | 
            -
                  invalid: "অবৈধ | 
| 13 | 
            -
                  unspecified: " | 
| 12 | 
            +
                  invalid: "অবৈধ"
         | 
| 13 | 
            +
                  unspecified: "অনির্দিষ্ট"
         | 
| 14 14 | 
             
                types:
         | 
| 15 15 | 
             
                  array: "অ্যারে"
         | 
| 16 16 | 
             
                  big_decimal: "বড় দশমিক"
         |