cmdx 1.0.1 → 1.1.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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,79 +2,27 @@
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
+ # MonotonicRuntime.call { sleep(0.1) } #=> 100 (approximately)
74
23
  #
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
24
+ # @example Measuring database query time
25
+ # MonotonicRuntime.call { User.find(1) } #=> 15 (milliseconds)
78
26
  def call(&)
79
27
  now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
80
28
  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,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Base class for implementing parameter validation functionality in task processing.
5
+ #
6
+ # Validators are used to validate parameter values against specific rules and constraints,
7
+ # supporting both built-in validation types and custom validation logic. All validator
8
+ # implementations 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] additional options for the validation
15
+ #
16
+ # @return [Object] the validated value if validation passes
17
+ #
18
+ # @raise [UndefinedCallError] when the validator subclass doesn't implement call
19
+ # @raise [ValidationError] when validation fails
20
+ #
21
+ # @example Execute a validator on a value
22
+ # PresenceValidator.call("some_value") #=> "some_value"
23
+ #
24
+ # @example Execute with options
25
+ # NumericValidator.call(42, greater_than: 10) #=> 42
26
+ def self.call(value, options = {})
27
+ new.call(value, options)
28
+ end
29
+
30
+ # Abstract method that must be implemented by validator subclasses.
31
+ #
32
+ # This method contains the actual validation logic to verify the input
33
+ # value meets the specified criteria. Subclasses must override this method
34
+ # to provide their specific validation implementation.
35
+ #
36
+ # @param value [Object] the value to be validated
37
+ # @param options [Hash] additional options for the validation
38
+ #
39
+ # @return [Object] the validated value if validation passes
40
+ #
41
+ # @raise [UndefinedCallError] always raised in the base class
42
+ # @raise [ValidationError] when validation fails in subclass implementations
43
+ #
44
+ # @example Implement in a subclass
45
+ # class BlankValidator < CMDx::Validator
46
+ # def call(value, options = {})
47
+ # if value.nil? || value.empty?
48
+ # raise ValidationError, options[:message] || "Value cannot be blank"
49
+ # end
50
+ # end
51
+ # end
52
+ def call(value, options = {}) # rubocop:disable Lint/UnusedMethodArgument
53
+ raise UndefinedCallError, "call method not defined in #{self.class.name}"
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Registry for parameter validation handlers in the CMDx framework.
5
+ #
6
+ # ValidatorRegistry manages the collection of validator implementations
7
+ # that can be used for parameter validation in tasks. It provides a
8
+ # centralized registry where validators can be registered by type and
9
+ # invoked during parameter processing. The registry comes pre-loaded
10
+ # with built-in validators for common validation scenarios.
11
+ class ValidatorRegistry
12
+
13
+ # @return [Hash] internal hash storing validator implementations by type
14
+ attr_reader :registry
15
+
16
+ # Creates a new validator registry with built-in validators.
17
+ #
18
+ # The registry is initialized with standard validators including
19
+ # exclusion, format, inclusion, length, numeric, and presence validation.
20
+ # These built-in validators provide common validation functionality
21
+ # that can be immediately used without additional registration.
22
+ #
23
+ # @return [ValidatorRegistry] a new registry instance with built-in validators
24
+ #
25
+ # @example Create a new validator registry
26
+ # registry = ValidatorRegistry.new
27
+ # registry.registry.keys #=> [:exclusion, :format, :inclusion, :length, :numeric, :presence]
28
+ def initialize
29
+ @registry = {
30
+ exclusion: Validators::Exclusion,
31
+ format: Validators::Format,
32
+ inclusion: Validators::Inclusion,
33
+ length: Validators::Length,
34
+ numeric: Validators::Numeric,
35
+ presence: Validators::Presence
36
+ }
37
+ end
38
+
39
+ # Registers a new validator implementation for the specified type.
40
+ #
41
+ # This method allows custom validators to be added to the registry,
42
+ # enabling extended validation functionality beyond the built-in
43
+ # validators. The validator can be a class, symbol, string, or proc
44
+ # that implements the validation logic.
45
+ #
46
+ # @param type [Symbol] the validator type identifier
47
+ # @param validator [Class, Symbol, String, Proc] the validator implementation
48
+ #
49
+ # @return [ValidatorRegistry] returns self for method chaining
50
+ #
51
+ # @example Register a custom validator class
52
+ # registry.register(:email, EmailValidator)
53
+ #
54
+ # @example Register a symbol validator
55
+ # registry.register(:zipcode, :validate_zipcode)
56
+ #
57
+ # @example Register a proc validator
58
+ # registry.register(:positive, ->(value, options) { value > 0 })
59
+ #
60
+ # @example Method chaining
61
+ # registry.register(:email, EmailValidator)
62
+ # .register(:phone, PhoneValidator)
63
+ def register(type, validator)
64
+ registry[type] = validator
65
+ self
66
+ end
67
+
68
+ # Executes validation for a parameter value using the specified validator type.
69
+ #
70
+ # This method performs validation by looking up the registered validator
71
+ # for the given type and executing it with the provided value and options.
72
+ # The validation is only performed if the task's evaluation of the options
73
+ # returns a truthy value, allowing for conditional validation.
74
+ #
75
+ # @param task [Task] the task instance performing validation
76
+ # @param type [Symbol] the validator type to use
77
+ # @param value [Object] the value to validate
78
+ # @param options [Hash] validation options and configuration
79
+ #
80
+ # @return [Object, nil] the validation result or nil if validation was skipped
81
+ #
82
+ # @raise [UnknownValidatorError] if the specified validator type is not registered
83
+ #
84
+ # @example Validate with a built-in validator
85
+ # registry.call(task, :presence, "", {})
86
+ # #=> may raise ValidationError if value is blank
87
+ #
88
+ # @example Validate with options
89
+ # registry.call(task, :length, "hello", minimum: 3, maximum: 10)
90
+ # #=> validates string length is between 3 and 10 characters
91
+ #
92
+ # @example Conditional validation that gets skipped
93
+ # registry.call(task, :presence, "", if: -> { false })
94
+ # #=> returns nil without performing validation
95
+ def call(task, type, value, options = {})
96
+ raise UnknownValidatorError, "unknown validator #{type}" unless registry.key?(type)
97
+ return unless task.cmdx_eval(options)
98
+
99
+ case validator = registry[type]
100
+ when Symbol, String, Proc
101
+ task.cmdx_try(validator, value, options)
102
+ else
103
+ validator.call(value, options)
104
+ end
105
+ end
106
+
107
+ end
108
+ 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(