cmdx 1.0.0 → 1.1.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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +5 -0
  5. data/CHANGELOG.md +101 -49
  6. data/README.md +2 -1
  7. data/docs/ai_prompts.md +10 -0
  8. data/docs/basics/call.md +11 -2
  9. data/docs/basics/chain.md +10 -1
  10. data/docs/basics/context.md +9 -0
  11. data/docs/basics/setup.md +9 -0
  12. data/docs/callbacks.md +14 -37
  13. data/docs/configuration.md +68 -27
  14. data/docs/getting_started.md +11 -0
  15. data/docs/internationalization.md +148 -0
  16. data/docs/interruptions/exceptions.md +10 -1
  17. data/docs/interruptions/faults.md +11 -2
  18. data/docs/interruptions/halt.md +9 -0
  19. data/docs/logging.md +14 -4
  20. data/docs/middlewares.md +53 -43
  21. data/docs/outcomes/result.md +9 -0
  22. data/docs/outcomes/states.md +9 -0
  23. data/docs/outcomes/statuses.md +9 -0
  24. data/docs/parameters/coercions.md +58 -38
  25. data/docs/parameters/defaults.md +10 -1
  26. data/docs/parameters/definitions.md +9 -0
  27. data/docs/parameters/namespacing.md +9 -0
  28. data/docs/parameters/validations.md +8 -67
  29. data/docs/testing.md +22 -13
  30. data/docs/tips_and_tricks.md +9 -0
  31. data/docs/workflows.md +14 -4
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +36 -56
  34. data/lib/cmdx/callback_registry.rb +82 -73
  35. data/lib/cmdx/chain.rb +65 -122
  36. data/lib/cmdx/chain_inspector.rb +22 -115
  37. data/lib/cmdx/chain_serializer.rb +17 -148
  38. data/lib/cmdx/coercion.rb +49 -0
  39. data/lib/cmdx/coercion_registry.rb +94 -0
  40. data/lib/cmdx/coercions/array.rb +18 -36
  41. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  42. data/lib/cmdx/coercions/boolean.rb +21 -40
  43. data/lib/cmdx/coercions/complex.rb +18 -31
  44. data/lib/cmdx/coercions/date.rb +20 -39
  45. data/lib/cmdx/coercions/date_time.rb +22 -39
  46. data/lib/cmdx/coercions/float.rb +19 -32
  47. data/lib/cmdx/coercions/hash.rb +22 -41
  48. data/lib/cmdx/coercions/integer.rb +20 -33
  49. data/lib/cmdx/coercions/rational.rb +20 -32
  50. data/lib/cmdx/coercions/string.rb +23 -31
  51. data/lib/cmdx/coercions/time.rb +24 -40
  52. data/lib/cmdx/coercions/virtual.rb +14 -31
  53. data/lib/cmdx/configuration.rb +57 -171
  54. data/lib/cmdx/context.rb +22 -165
  55. data/lib/cmdx/core_ext/hash.rb +42 -67
  56. data/lib/cmdx/core_ext/module.rb +35 -79
  57. data/lib/cmdx/core_ext/object.rb +63 -98
  58. data/lib/cmdx/correlator.rb +40 -156
  59. data/lib/cmdx/error.rb +37 -202
  60. data/lib/cmdx/errors.rb +165 -202
  61. data/lib/cmdx/fault.rb +55 -158
  62. data/lib/cmdx/faults.rb +26 -137
  63. data/lib/cmdx/immutator.rb +22 -109
  64. data/lib/cmdx/lazy_struct.rb +103 -187
  65. data/lib/cmdx/log_formatters/json.rb +14 -40
  66. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  67. data/lib/cmdx/log_formatters/line.rb +14 -48
  68. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  69. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  70. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  71. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  72. data/lib/cmdx/log_formatters/raw.rb +19 -49
  73. data/lib/cmdx/logger.rb +20 -82
  74. data/lib/cmdx/logger_ansi.rb +18 -75
  75. data/lib/cmdx/logger_serializer.rb +24 -114
  76. data/lib/cmdx/middleware.rb +38 -60
  77. data/lib/cmdx/middleware_registry.rb +81 -77
  78. data/lib/cmdx/middlewares/correlate.rb +41 -226
  79. data/lib/cmdx/middlewares/timeout.rb +46 -185
  80. data/lib/cmdx/parameter.rb +120 -198
  81. data/lib/cmdx/parameter_evaluator.rb +231 -0
  82. data/lib/cmdx/parameter_inspector.rb +25 -56
  83. data/lib/cmdx/parameter_registry.rb +59 -84
  84. data/lib/cmdx/parameter_serializer.rb +23 -74
  85. data/lib/cmdx/railtie.rb +24 -107
  86. data/lib/cmdx/result.rb +254 -260
  87. data/lib/cmdx/result_ansi.rb +19 -85
  88. data/lib/cmdx/result_inspector.rb +27 -68
  89. data/lib/cmdx/result_logger.rb +18 -81
  90. data/lib/cmdx/result_serializer.rb +28 -132
  91. data/lib/cmdx/rspec/matchers.rb +28 -0
  92. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  93. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  94. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  96. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  97. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  98. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  99. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  100. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  101. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  102. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  103. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  104. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  105. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  106. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  107. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  108. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  109. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  110. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  111. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  112. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  113. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  114. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  115. data/lib/cmdx/task.rb +213 -425
  116. data/lib/cmdx/task_deprecator.rb +55 -0
  117. data/lib/cmdx/task_processor.rb +245 -0
  118. data/lib/cmdx/task_serializer.rb +22 -70
  119. data/lib/cmdx/utils/ansi_color.rb +13 -89
  120. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  121. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  122. data/lib/cmdx/utils/name_affix.rb +21 -71
  123. data/lib/cmdx/validator.rb +48 -0
  124. data/lib/cmdx/validator_registry.rb +86 -0
  125. data/lib/cmdx/validators/exclusion.rb +55 -94
  126. data/lib/cmdx/validators/format.rb +31 -85
  127. data/lib/cmdx/validators/inclusion.rb +65 -110
  128. data/lib/cmdx/validators/length.rb +117 -133
  129. data/lib/cmdx/validators/numeric.rb +123 -130
  130. data/lib/cmdx/validators/presence.rb +38 -79
  131. data/lib/cmdx/version.rb +1 -7
  132. data/lib/cmdx/workflow.rb +46 -339
  133. data/lib/cmdx.rb +1 -1
  134. data/lib/generators/cmdx/install_generator.rb +14 -31
  135. data/lib/generators/cmdx/task_generator.rb +39 -55
  136. data/lib/generators/cmdx/templates/install.rb +61 -11
  137. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  138. data/lib/locales/ar.yml +35 -0
  139. data/lib/locales/cs.yml +35 -0
  140. data/lib/locales/da.yml +35 -0
  141. data/lib/locales/de.yml +35 -0
  142. data/lib/locales/el.yml +35 -0
  143. data/lib/locales/en.yml +19 -20
  144. data/lib/locales/es.yml +19 -20
  145. data/lib/locales/fi.yml +35 -0
  146. data/lib/locales/fr.yml +35 -0
  147. data/lib/locales/he.yml +35 -0
  148. data/lib/locales/hi.yml +35 -0
  149. data/lib/locales/it.yml +35 -0
  150. data/lib/locales/ja.yml +35 -0
  151. data/lib/locales/ko.yml +35 -0
  152. data/lib/locales/nl.yml +35 -0
  153. data/lib/locales/no.yml +35 -0
  154. data/lib/locales/pl.yml +35 -0
  155. data/lib/locales/pt.yml +35 -0
  156. data/lib/locales/ru.yml +35 -0
  157. data/lib/locales/sv.yml +35 -0
  158. data/lib/locales/th.yml +35 -0
  159. data/lib/locales/tr.yml +35 -0
  160. data/lib/locales/vi.yml +35 -0
  161. data/lib/locales/zh.yml +35 -0
  162. metadata +57 -8
  163. data/lib/cmdx/parameter_validator.rb +0 -81
  164. data/lib/cmdx/parameter_value.rb +0 -244
  165. data/lib/cmdx/parameters_inspector.rb +0 -72
  166. data/lib/cmdx/parameters_serializer.rb +0 -115
  167. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  168. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  169. data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,79 +2,29 @@
2
2
 
3
3
  module CMDx
4
4
  module Utils
5
- # Utility for measuring execution time using monotonic clock.
5
+ # Utility module for measuring execution time using monotonic clock.
6
6
  #
7
- # MonotonicRuntime provides accurate execution time measurement that is
8
- # unaffected by system clock adjustments, leap seconds, or other time
9
- # synchronization events. Uses Ruby's Process.clock_gettime with
10
- # CLOCK_MONOTONIC for reliable performance measurements.
11
- #
12
- # @example Basic runtime measurement
13
- # runtime = Utils::MonotonicRuntime.call do
14
- # sleep(1.5)
15
- # # ... task execution code ...
16
- # end
17
- # # => 1500 (milliseconds)
18
- #
19
- # @example Task execution timing
20
- # class ProcessOrderTask < CMDx::Task
21
- # def call
22
- # runtime = Utils::MonotonicRuntime.call do
23
- # # Complex business logic
24
- # process_payment
25
- # update_inventory
26
- # send_confirmation
27
- # end
28
- # logger.info "Order processed in #{runtime}ms"
29
- # end
30
- # end
31
- #
32
- # @example Performance benchmarking
33
- # fast_time = Utils::MonotonicRuntime.call { fast_algorithm }
34
- # slow_time = Utils::MonotonicRuntime.call { slow_algorithm }
35
- # puts "Fast algorithm is #{slow_time / fast_time}x faster"
36
- #
37
- # @see CMDx::Task Uses this internally to measure task execution time
38
- # @see CMDx::Result#runtime Contains the measured execution time
7
+ # This module provides functionality to measure the time taken to execute
8
+ # a block of code using the monotonic clock, which is not affected by
9
+ # system clock adjustments and provides more accurate timing measurements.
39
10
  module MonotonicRuntime
40
11
 
41
12
  module_function
42
13
 
43
14
  # Measures the execution time of a given block using monotonic clock.
44
15
  #
45
- # Executes the provided block and returns the elapsed time in milliseconds.
46
- # Uses Process.clock_gettime with CLOCK_MONOTONIC to ensure accurate
47
- # timing that is immune to system clock changes.
48
- #
49
- # @yield Block of code to measure execution time for
50
- # @return [Integer] Execution time in milliseconds
51
- #
52
- # @example Simple timing measurement
53
- # time_taken = MonotonicRuntime.call do
54
- # expensive_operation
55
- # end
56
- # puts "Operation took #{time_taken}ms"
16
+ # @param block [Proc] the block of code to measure execution time for
17
+ # @yield executes the provided block while measuring its runtime
57
18
  #
58
- # @example Database query timing
59
- # query_time = MonotonicRuntime.call do
60
- # User.joins(:orders).where(active: true).count
61
- # end
62
- # logger.debug "Query executed in #{query_time}ms"
19
+ # @return [Integer] the execution time in milliseconds
63
20
  #
64
- # @example API call timing with error handling
65
- # api_time = MonotonicRuntime.call do
66
- # begin
67
- # external_api.fetch_data
68
- # rescue => e
69
- # logger.error "API call failed: #{e.message}"
70
- # raise
71
- # end
72
- # end
73
- # # Time is measured even if an exception occurs
21
+ # @example Basic usage
22
+ # runtime = MonotonicRuntime.call { sleep(0.1) }
23
+ # # => 100 (approximately)
74
24
  #
75
- # @note The block's return value is discarded; only execution time is returned
76
- # @note Uses millisecond precision for practical performance monitoring
77
- # @note Monotonic clock ensures accurate timing regardless of system clock changes
25
+ # @example Measuring database query time
26
+ # query_time = MonotonicRuntime.call { User.find(1) }
27
+ # # => 15 (milliseconds)
78
28
  def call(&)
79
29
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
80
30
  yield
@@ -2,92 +2,42 @@
2
2
 
3
3
  module CMDx
4
4
  module Utils
5
- # Utility for generating method names with prefixes and suffixes.
5
+ # Utility module for generating method names with configurable prefixes and suffixes.
6
6
  #
7
- # NameAffix provides flexible method name generation for dynamic method
8
- # creation, delegation, and metaprogramming scenarios. Supports custom
9
- # prefixes, suffixes, and complete name overrides for method naming
10
- # conventions in CMDx's parameter and delegation systems.
11
- #
12
- # @example Basic prefix and suffix usage
13
- # Utils::NameAffix.call(:name, "user", prefix: true, suffix: true)
14
- # # => :user_name_user
15
- #
16
- # @example Custom prefix
17
- # Utils::NameAffix.call(:email, "admin", prefix: "get_")
18
- # # => :get_email
19
- #
20
- # @example Custom suffix
21
- # Utils::NameAffix.call(:count, "items", suffix: "_total")
22
- # # => :count_total
23
- #
24
- # @example Complete name override
25
- # Utils::NameAffix.call(:original, "source", as: :custom_method)
26
- # # => :custom_method
27
- #
28
- # @example Parameter delegation usage
29
- # class MyTask < CMDx::Task
30
- # required :user_id
31
- #
32
- # # Internally uses NameAffix for method generation
33
- # # Creates methods like user_id, user_id?, etc.
34
- # end
35
- #
36
- # @see CMDx::Parameter Uses this for parameter method name generation
37
- # @see CMDx::CoreExt::Module Uses this for delegation method naming
7
+ # This module provides functionality to dynamically construct method names
8
+ # by applying prefixes and suffixes to a base method name, with support
9
+ # for custom naming through options.
38
10
  module NameAffix
39
11
 
40
- # Proc for handling affix logic with boolean or custom values
41
- # @return [Proc] processor for affix options that handles true/false and custom strings
12
+ # Proc that handles affix logic - returns block result if value is true, otherwise returns value as-is.
42
13
  AFFIX = proc do |o, &block|
43
14
  o == true ? block.call : o
44
15
  end.freeze
45
16
 
46
17
  module_function
47
18
 
48
- # Generates a method name with optional prefix and suffix.
49
- #
50
- # Creates a method name by combining the base method name with optional
51
- # prefixes and suffixes. Supports boolean flags for default affixes or
52
- # custom string values for specific naming patterns.
53
- #
54
- # @param method_name [Symbol, String] Base method name to transform
55
- # @param source [String] Source identifier used for default prefix/suffix generation
56
- # @param options [Hash] Configuration options for name generation
57
- # @option options [Boolean, String] :prefix (false) Add prefix - true for "#{source}_", string for custom
58
- # @option options [Boolean, String] :suffix (false) Add suffix - true for "_#{source}", string for custom
59
- # @option options [Symbol] :as Override the entire generated name
60
- #
61
- # @return [Symbol] Generated method name with applied affixes
62
- #
63
- # @example Default prefix generation
64
- # NameAffix.call(:method, "user", prefix: true)
65
- # # => :user_method
19
+ # Generates a method name with optional prefix and suffix based on source and options.
66
20
  #
67
- # @example Custom prefix
68
- # NameAffix.call(:method, "user", prefix: "get_")
69
- # # => :get_method
21
+ # @param method_name [String, Symbol] the base method name to be affixed
22
+ # @param source [String, Symbol] the source identifier used for generating default prefixes/suffixes
23
+ # @param options [Hash] configuration options for name generation
24
+ # @option options [String, Symbol, true] :prefix custom prefix or true for default "#{source}_"
25
+ # @option options [String, Symbol, true] :suffix custom suffix or true for default "_#{source}"
26
+ # @option options [String, Symbol] :as override the entire generated name
70
27
  #
71
- # @example Default suffix generation
72
- # NameAffix.call(:method, "user", suffix: true)
73
- # # => :method_user
28
+ # @return [Symbol] the generated method name as a symbol
74
29
  #
75
- # @example Custom suffix
76
- # NameAffix.call(:method, "user", suffix: "_count")
77
- # # => :method_count
30
+ # @example Using default prefix and suffix
31
+ # NameAffix.call("process", "user", prefix: true, suffix: true) #=> :user_process_user
78
32
  #
79
- # @example Combined prefix and suffix
80
- # NameAffix.call(:name, "user", prefix: "get_", suffix: "_value")
81
- # # => :get_name_value
33
+ # @example Using custom prefix
34
+ # NameAffix.call("process", "user", prefix: "handle_") #=> :handle_process
82
35
  #
83
- # @example Complete name override (ignores prefix/suffix)
84
- # NameAffix.call(:original, "user", prefix: true, as: :custom)
85
- # # => :custom
36
+ # @example Using custom suffix
37
+ # NameAffix.call("process", "user", suffix: "_data") #=> :process_data
86
38
  #
87
- # @example Parameter method generation
88
- # # CMDx internally uses this for parameter methods:
89
- # NameAffix.call(:email, "user", suffix: "?") # => :email?
90
- # NameAffix.call(:process, "order", prefix: "can_") # => :can_process
39
+ # @example Overriding with custom name
40
+ # NameAffix.call("process", "user", as: "custom_method") #=> :custom_method
91
41
  def call(method_name, source, options = {})
92
42
  options[:as] || begin
93
43
  prefix = AFFIX.call(options[:prefix]) { "#{source}_" }
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Base class for implementing validation functionality in parameter processing.
5
+ #
6
+ # Validators are used to validate parameter values during task execution to
7
+ # ensure data integrity and business rule compliance. All validator implementations
8
+ # must inherit from this class and implement the abstract call method.
9
+ class Validator
10
+
11
+ # Executes a validator by creating a new instance and calling it.
12
+ #
13
+ # @param value [Object] the value to be validated
14
+ # @param options [Hash] optional validation configuration
15
+ #
16
+ # @return [Object] the result of the validation execution
17
+ #
18
+ # @raise [UndefinedCallError] when the validator subclass doesn't implement call
19
+ #
20
+ # @example Execute a validator on a value
21
+ # MyValidator.call("example", { min_length: 5 })
22
+ def self.call(value, options = {})
23
+ new.call(value, options)
24
+ end
25
+
26
+ # Abstract method that must be implemented by validator subclasses.
27
+ #
28
+ # This method contains the actual validation logic to be executed.
29
+ # Subclasses must override this method to provide their specific
30
+ # validation implementation.
31
+ #
32
+ # @param _value [Object] the value to be validated
33
+ # @param _options [Hash] optional validation configuration
34
+ #
35
+ # @return [Object] the result of the validation execution
36
+ #
37
+ # @raise [UndefinedCallError] always raised in the base class
38
+ #
39
+ # @example Implement in a subclass
40
+ # def call(value, options)
41
+ # raise ValidationError, "Value too short" if value.length < options[:min_length]
42
+ # end
43
+ def call(_value, _options = {})
44
+ raise UndefinedCallError, "call method not defined in #{self.class.name}"
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Registry for managing validator definitions and execution within tasks.
5
+ #
6
+ # This registry handles the registration and execution of validators for
7
+ # parameter validation, including built-in validators and custom validators
8
+ # that can be registered at runtime.
9
+ class ValidatorRegistry
10
+
11
+ # The internal hash storing validator definitions.
12
+ #
13
+ # @return [Hash] hash containing validator type keys and validator class values
14
+ attr_reader :registry
15
+
16
+ # Initializes a new validator registry with built-in validators.
17
+ #
18
+ # @return [ValidatorRegistry] a new validator registry instance
19
+ #
20
+ # @example Creating a validator registry
21
+ # ValidatorRegistry.new
22
+ def initialize
23
+ @registry = {
24
+ exclusion: Validators::Exclusion,
25
+ format: Validators::Format,
26
+ inclusion: Validators::Inclusion,
27
+ length: Validators::Length,
28
+ numeric: Validators::Numeric,
29
+ presence: Validators::Presence
30
+ }
31
+ end
32
+
33
+ # Registers a custom validator for a specific type.
34
+ #
35
+ # @param type [Symbol] the validator type to register
36
+ # @param validator [Class, Module, Symbol, Proc] the validator to register
37
+ #
38
+ # @return [ValidatorRegistry] returns self for method chaining
39
+ #
40
+ # @example Registering a custom validator class
41
+ # registry.register(:email, EmailValidator)
42
+ #
43
+ # @example Registering a Proc validator
44
+ # registry.register(:custom, ->(value, opts) { value.length > 3 })
45
+ #
46
+ # @example Registering a Symbol validator
47
+ # registry.register(:password, :validate_password_strength)
48
+ #
49
+ # @example Chaining validator registrations
50
+ # registry.register(:phone, PhoneValidator)
51
+ # .register(:zipcode, ZipcodeValidator)
52
+ def register(type, validator)
53
+ registry[type] = validator
54
+ self
55
+ end
56
+
57
+ # Executes validation for a specific type on a given value.
58
+ #
59
+ # @param task [Task] the task instance to execute validation on
60
+ # @param type [Symbol] the validator type to execute
61
+ # @param value [Object] the value to validate
62
+ # @param options [Hash] options for conditional validation execution
63
+ #
64
+ # @return [Object, nil] returns the validation result or nil if skipped
65
+ #
66
+ # @raise [UnknownValidatorError] when the validator type is not registered
67
+ #
68
+ # @example Validating with a built-in validator
69
+ # registry.call(task, :presence, "", {})
70
+ #
71
+ # @example Validating with options
72
+ # registry.call(task, :length, "test", { minimum: 5 })
73
+ def call(task, type, value, options = {})
74
+ raise UnknownValidatorError, "unknown validator #{type}" unless registry.key?(type)
75
+ return unless task.cmdx_eval(options)
76
+
77
+ case validator = registry[type]
78
+ when Symbol, String, Proc
79
+ task.cmdx_try(validator, value, options)
80
+ else
81
+ validator.call(value, options)
82
+ end
83
+ end
84
+
85
+ end
86
+ end
@@ -2,96 +2,46 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
- # Exclusion validator for parameter validation against forbidden values.
5
+ # Validator class for excluding values from a specified set.
6
6
  #
7
- # The Exclusion validator ensures that parameter values are NOT within a
8
- # specified set of forbidden values. It supports both array-based exclusion
9
- # (specific values) and range-based exclusion (value ranges).
10
- #
11
- # @example Basic exclusion validation with array
12
- # class ProcessOrderTask < CMDx::Task
13
- # required :status, exclusion: { in: ['cancelled', 'refunded'] }
14
- # required :priority, exclusion: { in: [0, -1] }
15
- # end
16
- #
17
- # @example Range-based exclusion
18
- # class ProcessUserTask < CMDx::Task
19
- # required :age, exclusion: { in: 0..17 } # Must be 18 or older
20
- # required :score, exclusion: { within: 90..100 } # Cannot be in top 10%
21
- # end
22
- #
23
- # @example Custom error messages
24
- # class ProcessOrderTask < CMDx::Task
25
- # required :status, exclusion: {
26
- # in: ['cancelled', 'refunded'],
27
- # of_message: "cannot be cancelled or refunded"
28
- # }
29
- # required :age, exclusion: {
30
- # in: 0..17,
31
- # in_message: "must be %{min} or older"
32
- # }
33
- # end
34
- #
35
- # @example Exclusion validation behavior
36
- # # Array exclusion
37
- # Exclusion.call("active", exclusion: { in: ['cancelled'] }) # passes
38
- # Exclusion.call("cancelled", exclusion: { in: ['cancelled'] }) # raises ValidationError
39
- #
40
- # # Range exclusion
41
- # Exclusion.call(25, exclusion: { in: 0..17 }) # passes
42
- # Exclusion.call(15, exclusion: { in: 0..17 }) # raises ValidationError
43
- #
44
- # @see CMDx::Validators::Inclusion For validating values must be in a set
45
- # @see CMDx::Parameter Parameter validation integration
46
- # @see CMDx::ValidationError Raised when validation fails
47
- module Exclusion
7
+ # This validator ensures that a value is not included in a given array or range
8
+ # of forbidden values. It supports both discrete value exclusion and range-based
9
+ # exclusion validation.
10
+ class Exclusion < Validator
48
11
 
49
- extend self
50
-
51
- # Validates that a parameter value is not in the excluded set.
52
- #
53
- # Checks that the value is not present in the specified array or range
54
- # of forbidden values. Raises ValidationError if the value is found
55
- # in the exclusion set.
12
+ # Validates that the given value is not included in the exclusion set.
56
13
  #
57
- # @param value [Object] The parameter value to validate
58
- # @param options [Hash] Validation configuration options
59
- # @option options [Hash] :exclusion Exclusion validation configuration
60
- # @option options [Array, Range] :exclusion.in Values/range to exclude
61
- # @option options [Array, Range] :exclusion.within Alias for :in
62
- # @option options [String] :exclusion.of_message Error message for array exclusion
63
- # @option options [String] :exclusion.in_message Error message for range exclusion
64
- # @option options [String] :exclusion.within_message Alias for :in_message
65
- # @option options [String] :exclusion.message General error message override
14
+ # @param value [Object] the value to validate
15
+ # @param options [Hash] validation options containing exclusion configuration
16
+ # @option options [Hash] :exclusion exclusion validation configuration
17
+ # @option options [Array, Range] :exclusion.in the values to exclude
18
+ # @option options [Array, Range] :exclusion.within alias for :in
19
+ # @option options [String] :exclusion.message custom error message
20
+ # @option options [String] :exclusion.of_message custom error message for array exclusion
21
+ # @option options [String] :exclusion.in_message custom error message for range exclusion
22
+ # @option options [String] :exclusion.within_message alias for :in_message
66
23
  #
67
24
  # @return [void]
68
- # @raise [ValidationError] If value is found in the exclusion set
69
25
  #
70
- # @example Array exclusion validation
71
- # Exclusion.call("pending", exclusion: { in: ['cancelled', 'failed'] })
72
- # # => passes without error
26
+ # @raise [ValidationError] if the value is found in the exclusion set
73
27
  #
74
- # @example Failed array exclusion
75
- # Exclusion.call("cancelled", exclusion: { in: ['cancelled', 'failed'] })
76
- # # => raises ValidationError: "must not be one of: \"cancelled\", \"failed\""
28
+ # @example Excluding from an array
29
+ # Validators::Exclusion.call("admin", exclusion: { in: ["admin", "root"] })
30
+ # # raises ValidationError: "must not be one of: \"admin\", \"root\""
77
31
  #
78
- # @example Range exclusion validation
79
- # Exclusion.call(25, exclusion: { in: 0..17 })
80
- # # => passes without error
32
+ # @example Excluding from a range
33
+ # Validators::Exclusion.call(5, exclusion: { in: 1..10 })
34
+ # # raises ValidationError: "must not be within 1 and 10"
81
35
  #
82
- # @example Failed range exclusion
83
- # Exclusion.call(15, exclusion: { in: 0..17 })
84
- # # => raises ValidationError: "must not be within 0 and 17"
36
+ # @example Valid exclusion
37
+ # Validators::Exclusion.call("user", exclusion: { in: ["admin", "root"] })
38
+ # # => nil (no error raised)
85
39
  #
86
- # @example Custom error messages
87
- # Exclusion.call("admin", exclusion: {
88
- # in: ['admin', 'root'],
89
- # of_message: "role is restricted"
90
- # })
91
- # # => raises ValidationError: "role is restricted"
40
+ # @example Using a custom message
41
+ # Validators::Exclusion.call("admin", exclusion: { in: ["admin", "root"], message: "Reserved username not allowed" })
42
+ # # raises ValidationError: "Reserved username not allowed"
92
43
  def call(value, options = {})
93
- values = options.dig(:exclusion, :in) ||
94
- options.dig(:exclusion, :within)
44
+ values = options[:in] || options[:within]
95
45
 
96
46
  if values.is_a?(Range)
97
47
  raise_within_validation_error!(values.begin, values.end, options) if values.cover?(value)
@@ -102,15 +52,21 @@ module CMDx
102
52
 
103
53
  private
104
54
 
105
- # Raises validation error for array-based exclusion violations.
55
+ # Raises a validation error for array-based exclusion.
106
56
  #
107
- # @param values [Array] The excluded values array
108
- # @param options [Hash] Validation options containing error messages
109
- # @raise [ValidationError] With formatted error message
57
+ # @param values [Array] the excluded values
58
+ # @param options [Hash] validation options
59
+ #
60
+ # @return [void]
61
+ #
62
+ # @raise [ValidationError] always raised with appropriate message
63
+ #
64
+ # @example
65
+ # raise_of_validation_error!(["admin", "root"], {})
66
+ # # raises ValidationError: "must not be one of: \"admin\", \"root\""
110
67
  def raise_of_validation_error!(values, options)
111
- values = values.map(&:inspect).join(", ")
112
- message = options.dig(:exclusion, :of_message) ||
113
- options.dig(:exclusion, :message)
68
+ values = values.map(&:inspect).join(", ") unless values.nil?
69
+ message = options[:of_message] || options[:message]
114
70
  message %= { values: } unless message.nil?
115
71
 
116
72
  raise ValidationError, message || I18n.t(
@@ -120,16 +76,21 @@ module CMDx
120
76
  )
121
77
  end
122
78
 
123
- # Raises validation error for range-based exclusion violations.
79
+ # Raises a validation error for range-based exclusion.
80
+ #
81
+ # @param min [Object] the minimum value of the range
82
+ # @param max [Object] the maximum value of the range
83
+ # @param options [Hash] validation options
84
+ #
85
+ # @return [void]
86
+ #
87
+ # @raise [ValidationError] always raised with appropriate message
124
88
  #
125
- # @param min [Object] Range minimum value
126
- # @param max [Object] Range maximum value
127
- # @param options [Hash] Validation options containing error messages
128
- # @raise [ValidationError] With formatted error message
89
+ # @example
90
+ # raise_within_validation_error!(1, 10, {})
91
+ # # raises ValidationError: "must not be within 1 and 10"
129
92
  def raise_within_validation_error!(min, max, options)
130
- message = options.dig(:exclusion, :in_message) ||
131
- options.dig(:exclusion, :within_message) ||
132
- options.dig(:exclusion, :message)
93
+ message = options[:in_message] || options[:within_message] || options[:message]
133
94
  message %= { min:, max: } unless message.nil?
134
95
 
135
96
  raise ValidationError, message || I18n.t(