cmdx 1.0.1 → 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 (157) 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 +2 -0
  5. data/CHANGELOG.md +17 -2
  6. data/README.md +1 -1
  7. data/docs/basics/call.md +2 -2
  8. data/docs/basics/chain.md +1 -1
  9. data/docs/callbacks.md +3 -36
  10. data/docs/configuration.md +58 -12
  11. data/docs/interruptions/exceptions.md +1 -1
  12. data/docs/interruptions/faults.md +2 -2
  13. data/docs/logging.md +4 -4
  14. data/docs/middlewares.md +43 -43
  15. data/docs/parameters/coercions.md +49 -38
  16. data/docs/parameters/defaults.md +1 -1
  17. data/docs/parameters/validations.md +0 -39
  18. data/docs/testing.md +11 -12
  19. data/docs/workflows.md +4 -4
  20. data/lib/cmdx/.DS_Store +0 -0
  21. data/lib/cmdx/callback.rb +36 -56
  22. data/lib/cmdx/callback_registry.rb +82 -73
  23. data/lib/cmdx/chain.rb +65 -122
  24. data/lib/cmdx/chain_inspector.rb +22 -115
  25. data/lib/cmdx/chain_serializer.rb +17 -148
  26. data/lib/cmdx/coercion.rb +49 -0
  27. data/lib/cmdx/coercion_registry.rb +94 -0
  28. data/lib/cmdx/coercions/array.rb +18 -36
  29. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  30. data/lib/cmdx/coercions/boolean.rb +21 -40
  31. data/lib/cmdx/coercions/complex.rb +18 -31
  32. data/lib/cmdx/coercions/date.rb +20 -39
  33. data/lib/cmdx/coercions/date_time.rb +22 -39
  34. data/lib/cmdx/coercions/float.rb +19 -32
  35. data/lib/cmdx/coercions/hash.rb +22 -41
  36. data/lib/cmdx/coercions/integer.rb +20 -33
  37. data/lib/cmdx/coercions/rational.rb +20 -32
  38. data/lib/cmdx/coercions/string.rb +23 -31
  39. data/lib/cmdx/coercions/time.rb +24 -40
  40. data/lib/cmdx/coercions/virtual.rb +14 -31
  41. data/lib/cmdx/configuration.rb +57 -171
  42. data/lib/cmdx/context.rb +22 -165
  43. data/lib/cmdx/core_ext/hash.rb +42 -67
  44. data/lib/cmdx/core_ext/module.rb +35 -79
  45. data/lib/cmdx/core_ext/object.rb +63 -98
  46. data/lib/cmdx/correlator.rb +40 -156
  47. data/lib/cmdx/error.rb +37 -202
  48. data/lib/cmdx/errors.rb +165 -202
  49. data/lib/cmdx/fault.rb +55 -158
  50. data/lib/cmdx/faults.rb +26 -137
  51. data/lib/cmdx/immutator.rb +22 -109
  52. data/lib/cmdx/lazy_struct.rb +103 -187
  53. data/lib/cmdx/log_formatters/json.rb +14 -40
  54. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  55. data/lib/cmdx/log_formatters/line.rb +14 -48
  56. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  57. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  58. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  59. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  60. data/lib/cmdx/log_formatters/raw.rb +19 -49
  61. data/lib/cmdx/logger.rb +20 -82
  62. data/lib/cmdx/logger_ansi.rb +18 -75
  63. data/lib/cmdx/logger_serializer.rb +24 -114
  64. data/lib/cmdx/middleware.rb +38 -60
  65. data/lib/cmdx/middleware_registry.rb +81 -77
  66. data/lib/cmdx/middlewares/correlate.rb +41 -226
  67. data/lib/cmdx/middlewares/timeout.rb +46 -185
  68. data/lib/cmdx/parameter.rb +120 -198
  69. data/lib/cmdx/parameter_evaluator.rb +231 -0
  70. data/lib/cmdx/parameter_inspector.rb +25 -56
  71. data/lib/cmdx/parameter_registry.rb +59 -84
  72. data/lib/cmdx/parameter_serializer.rb +23 -74
  73. data/lib/cmdx/railtie.rb +24 -107
  74. data/lib/cmdx/result.rb +254 -260
  75. data/lib/cmdx/result_ansi.rb +19 -85
  76. data/lib/cmdx/result_inspector.rb +27 -68
  77. data/lib/cmdx/result_logger.rb +18 -81
  78. data/lib/cmdx/result_serializer.rb +28 -132
  79. data/lib/cmdx/rspec/matchers.rb +28 -0
  80. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  81. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  82. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  83. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  84. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  85. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  86. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  87. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  88. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  89. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  90. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  91. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  92. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  93. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  94. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  95. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  96. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  97. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  98. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  99. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  100. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  101. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  102. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  103. data/lib/cmdx/task.rb +213 -425
  104. data/lib/cmdx/task_deprecator.rb +55 -0
  105. data/lib/cmdx/task_processor.rb +245 -0
  106. data/lib/cmdx/task_serializer.rb +22 -70
  107. data/lib/cmdx/utils/ansi_color.rb +13 -89
  108. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  109. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  110. data/lib/cmdx/utils/name_affix.rb +21 -71
  111. data/lib/cmdx/validator.rb +48 -0
  112. data/lib/cmdx/validator_registry.rb +86 -0
  113. data/lib/cmdx/validators/exclusion.rb +55 -94
  114. data/lib/cmdx/validators/format.rb +31 -85
  115. data/lib/cmdx/validators/inclusion.rb +65 -110
  116. data/lib/cmdx/validators/length.rb +117 -133
  117. data/lib/cmdx/validators/numeric.rb +123 -130
  118. data/lib/cmdx/validators/presence.rb +38 -79
  119. data/lib/cmdx/version.rb +1 -7
  120. data/lib/cmdx/workflow.rb +46 -339
  121. data/lib/cmdx.rb +1 -1
  122. data/lib/generators/cmdx/install_generator.rb +14 -31
  123. data/lib/generators/cmdx/task_generator.rb +39 -55
  124. data/lib/generators/cmdx/templates/install.rb +24 -6
  125. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  126. data/lib/locales/ar.yml +0 -1
  127. data/lib/locales/cs.yml +0 -1
  128. data/lib/locales/da.yml +0 -1
  129. data/lib/locales/de.yml +0 -1
  130. data/lib/locales/el.yml +0 -1
  131. data/lib/locales/en.yml +0 -1
  132. data/lib/locales/es.yml +0 -1
  133. data/lib/locales/fi.yml +0 -1
  134. data/lib/locales/fr.yml +0 -1
  135. data/lib/locales/he.yml +0 -1
  136. data/lib/locales/hi.yml +0 -1
  137. data/lib/locales/it.yml +0 -1
  138. data/lib/locales/ja.yml +0 -1
  139. data/lib/locales/ko.yml +0 -1
  140. data/lib/locales/nl.yml +0 -1
  141. data/lib/locales/no.yml +0 -1
  142. data/lib/locales/pl.yml +0 -1
  143. data/lib/locales/pt.yml +0 -1
  144. data/lib/locales/ru.yml +0 -1
  145. data/lib/locales/sv.yml +0 -1
  146. data/lib/locales/th.yml +0 -1
  147. data/lib/locales/tr.yml +0 -1
  148. data/lib/locales/vi.yml +0 -1
  149. data/lib/locales/zh.yml +0 -1
  150. metadata +34 -8
  151. data/lib/cmdx/parameter_validator.rb +0 -81
  152. data/lib/cmdx/parameter_value.rb +0 -244
  153. data/lib/cmdx/parameters_inspector.rb +0 -72
  154. data/lib/cmdx/parameters_serializer.rb +0 -115
  155. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  156. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  157. data/lib/cmdx/validators/custom.rb +0 -102
@@ -2,108 +2,58 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
- # Length validator for parameter validation based on size constraints.
5
+ # Validator class for validating the length of values.
6
6
  #
7
- # The Length validator validates the length/size of parameter values such as
8
- # strings, arrays, and other objects that respond to #length. It supports
9
- # various constraint types including ranges, boundaries, and exact lengths.
10
- #
11
- # @example Range-based length validation
12
- # class ProcessUserTask < CMDx::Task
13
- # required :username, length: { within: 3..20 }
14
- # required :password, length: { in: 8..128 }
15
- # required :bio, length: { not_within: 500..1000 } # Avoid medium length
16
- # end
17
- #
18
- # @example Boundary length validation
19
- # class ProcessContentTask < CMDx::Task
20
- # required :title, length: { min: 5 }
21
- # required :description, length: { max: 500 }
22
- # required :slug, length: { min: 3, max: 50 } # Combined min/max
23
- # end
24
- #
25
- # @example Exact length validation
26
- # class ProcessCodeTask < CMDx::Task
27
- # required :country_code, length: { is: 2 } # ISO country codes
28
- # required :postal_code, length: { is_not: 4 } # Avoid 4-digit codes
29
- # end
30
- #
31
- # @example Custom error messages
32
- # class ProcessUserTask < CMDx::Task
33
- # required :username, length: {
34
- # within: 3..20,
35
- # within_message: "must be between %{min} and %{max} characters"
36
- # }
37
- # required :password, length: {
38
- # min: 8,
39
- # min_message: "must be at least %{min} characters for security"
40
- # }
41
- # end
42
- #
43
- # @example Length validation behavior
44
- # # String length validation
45
- # Length.call("hello", length: { min: 3 }) # passes (length: 5)
46
- # Length.call("hi", length: { min: 3 }) # raises ValidationError
47
- #
48
- # # Array length validation
49
- # Length.call([1, 2, 3], length: { is: 3 }) # passes
50
- # Length.call([1, 2], length: { is: 3 }) # raises ValidationError
51
- #
52
- # @see CMDx::Validators::Numeric For numeric value validation
53
- # @see CMDx::Parameter Parameter validation integration
54
- # @see CMDx::ValidationError Raised when validation fails
55
- module Length
56
-
57
- extend self
58
-
59
- # Validates that a parameter value meets the specified length constraints.
60
- #
61
- # Validates the length of the value using the specified constraint type.
62
- # Only one constraint option can be used at a time, except for :min and :max
63
- # which can be combined together.
64
- #
65
- # @param value [#length] The parameter value to validate (must respond to #length)
66
- # @param options [Hash] Validation configuration options
67
- # @option options [Hash] :length Length validation configuration
68
- # @option options [Range] :length.within Allowed length range
69
- # @option options [Range] :length.not_within Forbidden length range
70
- # @option options [Range] :length.in Alias for :within
71
- # @option options [Range] :length.not_in Alias for :not_within
72
- # @option options [Integer] :length.min Minimum allowed length
73
- # @option options [Integer] :length.max Maximum allowed length
74
- # @option options [Integer] :length.is Exact required length
75
- # @option options [Integer] :length.is_not Forbidden exact length
76
- # @option options [String] :length.*_message Custom error messages for each constraint
7
+ # This validator ensures that a value's length meets specified criteria.
8
+ # It supports various length validation options including exact length,
9
+ # minimum/maximum bounds, range validation, and exclusion patterns.
10
+ class Length < Validator
11
+
12
+ # Validates that the given value's length meets the specified criteria.
77
13
  #
78
- # @return [void]
79
- # @raise [ValidationError] If value doesn't meet the length constraints
80
- # @raise [ArgumentError] If no valid length constraint options are provided
14
+ # @param value [Object] the value to validate (must respond to #length)
15
+ # @param options [Hash] validation options containing length configuration
16
+ # @option options [Hash] :length length validation configuration
17
+ # @option options [Range] :length.within acceptable length range
18
+ # @option options [Range] :length.not_within unacceptable length range
19
+ # @option options [Range] :length.in alias for :within
20
+ # @option options [Range] :length.not_in alias for :not_within
21
+ # @option options [Integer] :length.min minimum acceptable length
22
+ # @option options [Integer] :length.max maximum acceptable length
23
+ # @option options [Integer] :length.is exact required length
24
+ # @option options [Integer] :length.is_not exact forbidden length
25
+ # @option options [String] :length.message custom error message
26
+ # @option options [String] :length.within_message custom error message for within validation
27
+ # @option options [String] :length.not_within_message custom error message for not_within validation
28
+ # @option options [String] :length.in_message alias for :within_message
29
+ # @option options [String] :length.not_in_message alias for :not_within_message
30
+ # @option options [String] :length.min_message custom error message for minimum validation
31
+ # @option options [String] :length.max_message custom error message for maximum validation
32
+ # @option options [String] :length.is_message custom error message for exact validation
33
+ # @option options [String] :length.is_not_message custom error message for exact exclusion validation
81
34
  #
82
- # @example Range validation
83
- # Length.call("hello", length: { within: 3..10 })
84
- # # => passes without error
35
+ # @return [void]
85
36
  #
86
- # @example Failed range validation
87
- # Length.call("hi", length: { within: 3..10 })
88
- # # => raises ValidationError: "length must be within 3 and 10"
37
+ # @raise [ValidationError] if the value's length doesn't meet the criteria
38
+ # @raise [ArgumentError] if no known length validator options are provided
89
39
  #
90
- # @example Minimum length validation
91
- # Length.call("password123", length: { min: 8 })
92
- # # => passes without error
40
+ # @example Validating within a range
41
+ # Validators::Length.call("hello", length: { within: 1..10 })
42
+ # # => nil (no error raised)
93
43
  #
94
- # @example Combined min/max validation
95
- # Length.call("username", length: { min: 3, max: 20 })
96
- # # => passes without error
44
+ # @example Validating minimum length
45
+ # Validators::Length.call("hi", length: { min: 5 })
46
+ # # raises ValidationError: "length must be at least 5"
97
47
  #
98
- # @example Exact length validation
99
- # Length.call("US", length: { is: 2 })
100
- # # => passes without error (country code)
48
+ # @example Validating exact length
49
+ # Validators::Length.call("test", length: { is: 4 })
50
+ # # => nil (no error raised)
101
51
  #
102
- # @example Array length validation
103
- # Length.call([1, 2, 3, 4], length: { max: 5 })
104
- # # => passes without error
52
+ # @example Validating with custom message
53
+ # Validators::Length.call("", length: { min: 1, message: "cannot be empty" })
54
+ # # raises ValidationError: "cannot be empty"
105
55
  def call(value, options = {})
106
- case options[:length]
56
+ case options
107
57
  in { within: within }
108
58
  raise_within_validation_error!(within.begin, within.end, options) unless within.cover?(value.length)
109
59
  in { not_within: not_within }
@@ -129,16 +79,21 @@ module CMDx
129
79
 
130
80
  private
131
81
 
132
- # Raises validation error for range-based length violations.
82
+ # Raises a validation error for within/in range validation.
83
+ #
84
+ # @param min [Integer] the minimum acceptable length
85
+ # @param max [Integer] the maximum acceptable length
86
+ # @param options [Hash] validation options
87
+ #
88
+ # @return [void]
133
89
  #
134
- # @param min [Integer] Range minimum length
135
- # @param max [Integer] Range maximum length
136
- # @param options [Hash] Validation options containing error messages
137
- # @raise [ValidationError] With formatted error message
90
+ # @raise [ValidationError] always raised with appropriate message
91
+ #
92
+ # @example
93
+ # raise_within_validation_error!(5, 10, {})
94
+ # # raises ValidationError: "length must be within 5 and 10"
138
95
  def raise_within_validation_error!(min, max, options)
139
- message = options.dig(:length, :within_message) ||
140
- options.dig(:length, :in_message) ||
141
- options.dig(:length, :message)
96
+ message = options[:within_message] || options[:in_message] || options[:message]
142
97
  message %= { min:, max: } unless message.nil?
143
98
 
144
99
  raise ValidationError, message || I18n.t(
@@ -149,16 +104,21 @@ module CMDx
149
104
  )
150
105
  end
151
106
 
152
- # Raises validation error for forbidden range violations.
107
+ # Raises a validation error for not_within/not_in range validation.
108
+ #
109
+ # @param min [Integer] the minimum forbidden length
110
+ # @param max [Integer] the maximum forbidden length
111
+ # @param options [Hash] validation options
112
+ #
113
+ # @return [void]
153
114
  #
154
- # @param min [Integer] Range minimum length
155
- # @param max [Integer] Range maximum length
156
- # @param options [Hash] Validation options containing error messages
157
- # @raise [ValidationError] With formatted error message
115
+ # @raise [ValidationError] always raised with appropriate message
116
+ #
117
+ # @example
118
+ # raise_not_within_validation_error!(5, 10, {})
119
+ # # raises ValidationError: "length must not be within 5 and 10"
158
120
  def raise_not_within_validation_error!(min, max, options)
159
- message = options.dig(:length, :not_within_message) ||
160
- options.dig(:length, :not_in_message) ||
161
- options.dig(:length, :message)
121
+ message = options[:not_within_message] || options[:not_in_message] || options[:message]
162
122
  message %= { min:, max: } unless message.nil?
163
123
 
164
124
  raise ValidationError, message || I18n.t(
@@ -169,14 +129,20 @@ module CMDx
169
129
  )
170
130
  end
171
131
 
172
- # Raises validation error for minimum length violations.
132
+ # Raises a validation error for minimum length validation.
133
+ #
134
+ # @param min [Integer] the minimum acceptable length
135
+ # @param options [Hash] validation options
136
+ #
137
+ # @return [void]
138
+ #
139
+ # @raise [ValidationError] always raised with appropriate message
173
140
  #
174
- # @param min [Integer] Minimum required length
175
- # @param options [Hash] Validation options containing error messages
176
- # @raise [ValidationError] With formatted error message
141
+ # @example
142
+ # raise_min_validation_error!(5, {})
143
+ # # raises ValidationError: "length must be at least 5"
177
144
  def raise_min_validation_error!(min, options)
178
- message = options.dig(:length, :min_message) ||
179
- options.dig(:length, :message)
145
+ message = options[:min_message] || options[:message]
180
146
  message %= { min: } unless message.nil?
181
147
 
182
148
  raise ValidationError, message || I18n.t(
@@ -186,14 +152,20 @@ module CMDx
186
152
  )
187
153
  end
188
154
 
189
- # Raises validation error for maximum length violations.
155
+ # Raises a validation error for maximum length validation.
156
+ #
157
+ # @param max [Integer] the maximum acceptable length
158
+ # @param options [Hash] validation options
159
+ #
160
+ # @return [void]
161
+ #
162
+ # @raise [ValidationError] always raised with appropriate message
190
163
  #
191
- # @param max [Integer] Maximum allowed length
192
- # @param options [Hash] Validation options containing error messages
193
- # @raise [ValidationError] With formatted error message
164
+ # @example
165
+ # raise_max_validation_error!(10, {})
166
+ # # raises ValidationError: "length must be at most 10"
194
167
  def raise_max_validation_error!(max, options)
195
- message = options.dig(:length, :max_message) ||
196
- options.dig(:length, :message)
168
+ message = options[:max_message] || options[:message]
197
169
  message %= { max: } unless message.nil?
198
170
 
199
171
  raise ValidationError, message || I18n.t(
@@ -203,14 +175,20 @@ module CMDx
203
175
  )
204
176
  end
205
177
 
206
- # Raises validation error for exact length violations.
178
+ # Raises a validation error for exact length validation.
207
179
  #
208
- # @param is [Integer] Required exact length
209
- # @param options [Hash] Validation options containing error messages
210
- # @raise [ValidationError] With formatted error message
180
+ # @param is [Integer] the exact required length
181
+ # @param options [Hash] validation options
182
+ #
183
+ # @return [void]
184
+ #
185
+ # @raise [ValidationError] always raised with appropriate message
186
+ #
187
+ # @example
188
+ # raise_is_validation_error!(5, {})
189
+ # # raises ValidationError: "length must be 5"
211
190
  def raise_is_validation_error!(is, options)
212
- message = options.dig(:length, :is_message) ||
213
- options.dig(:length, :message)
191
+ message = options[:is_message] || options[:message]
214
192
  message %= { is: } unless message.nil?
215
193
 
216
194
  raise ValidationError, message || I18n.t(
@@ -220,14 +198,20 @@ module CMDx
220
198
  )
221
199
  end
222
200
 
223
- # Raises validation error for forbidden exact length violations.
201
+ # Raises a validation error for exact length exclusion validation.
202
+ #
203
+ # @param is_not [Integer] the exact forbidden length
204
+ # @param options [Hash] validation options
205
+ #
206
+ # @return [void]
207
+ #
208
+ # @raise [ValidationError] always raised with appropriate message
224
209
  #
225
- # @param is_not [Integer] Forbidden exact length
226
- # @param options [Hash] Validation options containing error messages
227
- # @raise [ValidationError] With formatted error message
210
+ # @example
211
+ # raise_is_not_validation_error!(5, {})
212
+ # # raises ValidationError: "length must not be 5"
228
213
  def raise_is_not_validation_error!(is_not, options)
229
- message = options.dig(:length, :is_not_message) ||
230
- options.dig(:length, :message)
214
+ message = options[:is_not_message] || options[:message]
231
215
  message %= { is_not: } unless message.nil?
232
216
 
233
217
  raise ValidationError, message || I18n.t(
@@ -2,108 +2,67 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
- # Numeric validator for parameter validation based on numeric value constraints.
5
+ # Validator class for validating numeric values with various constraints.
6
6
  #
7
- # The Numeric validator validates numeric parameter values against various
8
- # constraints including ranges, boundaries, and exact values. It works with
9
- # any numeric type including integers, floats, decimals, and other numeric objects.
10
- #
11
- # @example Range-based numeric validation
12
- # class ProcessOrderTask < CMDx::Task
13
- # required :quantity, numeric: { within: 1..100 }
14
- # required :price, numeric: { in: 0.01..999.99 }
15
- # required :discount, numeric: { not_within: 90..100 } # Avoid excessive discounts
16
- # end
17
- #
18
- # @example Boundary numeric validation
19
- # class ProcessUserTask < CMDx::Task
20
- # required :age, numeric: { min: 18 }
21
- # required :score, numeric: { max: 100 }
22
- # required :rating, numeric: { min: 1, max: 5 } # Combined min/max
23
- # end
24
- #
25
- # @example Exact numeric validation
26
- # class ProcessConfigTask < CMDx::Task
27
- # required :version, numeric: { is: 2 } # Specific version required
28
- # required :legacy_flag, numeric: { is_not: 0 } # Must not be zero
29
- # end
30
- #
31
- # @example Custom error messages
32
- # class ProcessOrderTask < CMDx::Task
33
- # required :quantity, numeric: {
34
- # within: 1..100,
35
- # within_message: "must be between %{min} and %{max} items"
36
- # }
37
- # required :age, numeric: {
38
- # min: 18,
39
- # min_message: "must be at least %{min} years old"
40
- # }
41
- # end
42
- #
43
- # @example Numeric validation behavior
44
- # # Integer validation
45
- # Numeric.call(25, numeric: { min: 18 }) # passes
46
- # Numeric.call(15, numeric: { min: 18 }) # raises ValidationError
47
- #
48
- # # Float validation
49
- # Numeric.call(99.99, numeric: { max: 100.0 }) # passes
50
- # Numeric.call(101.5, numeric: { max: 100.0 }) # raises ValidationError
51
- #
52
- # @see CMDx::Validators::Length For length/size validation
53
- # @see CMDx::Parameter Parameter validation integration
54
- # @see CMDx::ValidationError Raised when validation fails
55
- module Numeric
56
-
57
- extend self
58
-
59
- # Validates that a parameter value meets the specified numeric constraints.
60
- #
61
- # Validates the numeric value using the specified constraint type.
62
- # Only one constraint option can be used at a time, except for :min and :max
63
- # which can be combined together.
64
- #
65
- # @param value [Numeric] The parameter value to validate (must be numeric)
66
- # @param options [Hash] Validation configuration options
67
- # @option options [Hash] :numeric Numeric validation configuration
68
- # @option options [Range] :numeric.within Allowed value range
69
- # @option options [Range] :numeric.not_within Forbidden value range
70
- # @option options [Range] :numeric.in Alias for :within
71
- # @option options [Range] :numeric.not_in Alias for :not_within
72
- # @option options [Numeric] :numeric.min Minimum allowed value
73
- # @option options [Numeric] :numeric.max Maximum allowed value
74
- # @option options [Numeric] :numeric.is Exact required value
75
- # @option options [Numeric] :numeric.is_not Forbidden exact value
76
- # @option options [String] :numeric.*_message Custom error messages for each constraint
7
+ # This validator ensures that numeric values meet specified criteria such as
8
+ # being within a range, having minimum/maximum values, or matching exact values.
9
+ # It supports both inclusive and exclusive range validation, as well as discrete
10
+ # value matching and rejection.
11
+ class Numeric < Validator
12
+
13
+ # Validates that the given numeric value meets the specified constraints.
14
+ #
15
+ # @param value [Numeric] the numeric value to validate
16
+ # @param options [Hash] validation options containing numeric configuration
17
+ # @option options [Hash] :numeric numeric validation configuration
18
+ # @option options [Range] :numeric.within the range the value must be within
19
+ # @option options [Range] :numeric.not_within the range the value must not be within
20
+ # @option options [Range] :numeric.in alias for :within
21
+ # @option options [Range] :numeric.not_in alias for :not_within
22
+ # @option options [Numeric] :numeric.min the minimum allowed value (can be combined with :max)
23
+ # @option options [Numeric] :numeric.max the maximum allowed value (can be combined with :min)
24
+ # @option options [Numeric] :numeric.is the exact value required
25
+ # @option options [Numeric] :numeric.is_not the exact value that is not allowed
26
+ # @option options [String] :numeric.message custom error message for any validation
27
+ # @option options [String] :numeric.within_message custom error message for within validation
28
+ # @option options [String] :numeric.in_message alias for :within_message
29
+ # @option options [String] :numeric.not_within_message custom error message for not_within validation
30
+ # @option options [String] :numeric.not_in_message alias for :not_within_message
31
+ # @option options [String] :numeric.min_message custom error message for min validation
32
+ # @option options [String] :numeric.max_message custom error message for max validation
33
+ # @option options [String] :numeric.is_message custom error message for is validation
34
+ # @option options [String] :numeric.is_not_message custom error message for is_not validation
77
35
  #
78
36
  # @return [void]
79
- # @raise [ValidationError] If value doesn't meet the numeric constraints
80
- # @raise [ArgumentError] If no valid numeric constraint options are provided
37
+ #
38
+ # @raise [ValidationError] if the value doesn't meet the specified constraints
39
+ # @raise [ArgumentError] if no known numeric validator options are provided
81
40
  #
82
41
  # @example Range validation
83
- # Numeric.call(50, numeric: { within: 1..100 })
84
- # # => passes without error
42
+ # Validators::Numeric.call(5, numeric: { within: 1..10 })
43
+ # # => nil (no error raised)
85
44
  #
86
- # @example Failed range validation
87
- # Numeric.call(150, numeric: { within: 1..100 })
88
- # # => raises ValidationError: "must be within 1 and 100"
45
+ # @example Range exclusion
46
+ # Validators::Numeric.call(5, numeric: { not_within: 1..10 })
47
+ # # raises ValidationError: "must not be within 1 and 10"
89
48
  #
90
- # @example Minimum value validation
91
- # Numeric.call(25, numeric: { min: 18 })
92
- # # => passes without error
49
+ # @example Min/max validation
50
+ # Validators::Numeric.call(15, numeric: { min: 10, max: 20 })
51
+ # # => nil (no error raised)
93
52
  #
94
- # @example Combined min/max validation
95
- # Numeric.call(3.5, numeric: { min: 1.0, max: 5.0 })
96
- # # => passes without error
53
+ # @example Minimum value validation
54
+ # Validators::Numeric.call(5, numeric: { min: 10 })
55
+ # # raises ValidationError: "must be at least 10"
97
56
  #
98
57
  # @example Exact value validation
99
- # Numeric.call(42, numeric: { is: 42 })
100
- # # => passes without error
58
+ # Validators::Numeric.call(42, numeric: { is: 42 })
59
+ # # => nil (no error raised)
101
60
  #
102
- # @example Float validation
103
- # Numeric.call(19.99, numeric: { max: 20.0 })
104
- # # => passes without error
61
+ # @example Custom error message
62
+ # Validators::Numeric.call(5, numeric: { min: 10, message: "Age must be at least %{min}" })
63
+ # # raises ValidationError: "Age must be at least 10"
105
64
  def call(value, options = {})
106
- case options[:numeric]
65
+ case options
107
66
  in { within: within }
108
67
  raise_within_validation_error!(within.begin, within.end, options) unless within.cover?(value)
109
68
  in { not_within: not_within }
@@ -129,16 +88,21 @@ module CMDx
129
88
 
130
89
  private
131
90
 
132
- # Raises validation error for range-based numeric violations.
91
+ # Raises a validation error for within/range validation.
133
92
  #
134
- # @param min [Numeric] Range minimum value
135
- # @param max [Numeric] Range maximum value
136
- # @param options [Hash] Validation options containing error messages
137
- # @raise [ValidationError] With formatted error message
93
+ # @param min [Numeric] the minimum value of the range
94
+ # @param max [Numeric] the maximum value of the range
95
+ # @param options [Hash] validation options
96
+ #
97
+ # @return [void]
98
+ #
99
+ # @raise [ValidationError] always raised with appropriate message
100
+ #
101
+ # @example
102
+ # raise_within_validation_error!(1, 10, {})
103
+ # # raises ValidationError: "must be within 1 and 10"
138
104
  def raise_within_validation_error!(min, max, options)
139
- message = options.dig(:numeric, :within_message) ||
140
- options.dig(:numeric, :in_message) ||
141
- options.dig(:numeric, :message)
105
+ message = options[:within_message] || options[:in_message] || options[:message]
142
106
  message %= { min:, max: } unless message.nil?
143
107
 
144
108
  raise ValidationError, message || I18n.t(
@@ -149,16 +113,21 @@ module CMDx
149
113
  )
150
114
  end
151
115
 
152
- # Raises validation error for forbidden range violations.
116
+ # Raises a validation error for not_within/range exclusion validation.
117
+ #
118
+ # @param min [Numeric] the minimum value of the excluded range
119
+ # @param max [Numeric] the maximum value of the excluded range
120
+ # @param options [Hash] validation options
121
+ #
122
+ # @return [void]
123
+ #
124
+ # @raise [ValidationError] always raised with appropriate message
153
125
  #
154
- # @param min [Numeric] Range minimum value
155
- # @param max [Numeric] Range maximum value
156
- # @param options [Hash] Validation options containing error messages
157
- # @raise [ValidationError] With formatted error message
126
+ # @example
127
+ # raise_not_within_validation_error!(1, 10, {})
128
+ # # raises ValidationError: "must not be within 1 and 10"
158
129
  def raise_not_within_validation_error!(min, max, options)
159
- message = options.dig(:numeric, :not_within_message) ||
160
- options.dig(:numeric, :not_in_message) ||
161
- options.dig(:numeric, :message)
130
+ message = options[:not_within_message] || options[:not_in_message] || options[:message]
162
131
  message %= { min:, max: } unless message.nil?
163
132
 
164
133
  raise ValidationError, message || I18n.t(
@@ -169,14 +138,20 @@ module CMDx
169
138
  )
170
139
  end
171
140
 
172
- # Raises validation error for minimum value violations.
141
+ # Raises a validation error for minimum value validation.
142
+ #
143
+ # @param min [Numeric] the minimum allowed value
144
+ # @param options [Hash] validation options
145
+ #
146
+ # @return [void]
173
147
  #
174
- # @param min [Numeric] Minimum required value
175
- # @param options [Hash] Validation options containing error messages
176
- # @raise [ValidationError] With formatted error message
148
+ # @raise [ValidationError] always raised with appropriate message
149
+ #
150
+ # @example
151
+ # raise_min_validation_error!(10, {})
152
+ # # raises ValidationError: "must be at least 10"
177
153
  def raise_min_validation_error!(min, options)
178
- message = options.dig(:numeric, :min_message) ||
179
- options.dig(:numeric, :message)
154
+ message = options[:min_message] || options[:message]
180
155
  message %= { min: } unless message.nil?
181
156
 
182
157
  raise ValidationError, message || I18n.t(
@@ -186,14 +161,20 @@ module CMDx
186
161
  )
187
162
  end
188
163
 
189
- # Raises validation error for maximum value violations.
164
+ # Raises a validation error for maximum value validation.
165
+ #
166
+ # @param max [Numeric] the maximum allowed value
167
+ # @param options [Hash] validation options
168
+ #
169
+ # @return [void]
190
170
  #
191
- # @param max [Numeric] Maximum allowed value
192
- # @param options [Hash] Validation options containing error messages
193
- # @raise [ValidationError] With formatted error message
171
+ # @raise [ValidationError] always raised with appropriate message
172
+ #
173
+ # @example
174
+ # raise_max_validation_error!(100, {})
175
+ # # raises ValidationError: "must be at most 100"
194
176
  def raise_max_validation_error!(max, options)
195
- message = options.dig(:numeric, :max_message) ||
196
- options.dig(:numeric, :message)
177
+ message = options[:max_message] || options[:message]
197
178
  message %= { max: } unless message.nil?
198
179
 
199
180
  raise ValidationError, message || I18n.t(
@@ -203,14 +184,20 @@ module CMDx
203
184
  )
204
185
  end
205
186
 
206
- # Raises validation error for exact value violations.
187
+ # Raises a validation error for exact value validation.
188
+ #
189
+ # @param is [Numeric] the exact value required
190
+ # @param options [Hash] validation options
191
+ #
192
+ # @return [void]
193
+ #
194
+ # @raise [ValidationError] always raised with appropriate message
207
195
  #
208
- # @param is [Numeric] Required exact value
209
- # @param options [Hash] Validation options containing error messages
210
- # @raise [ValidationError] With formatted error message
196
+ # @example
197
+ # raise_is_validation_error!(42, {})
198
+ # # raises ValidationError: "must be 42"
211
199
  def raise_is_validation_error!(is, options)
212
- message = options.dig(:numeric, :is_message) ||
213
- options.dig(:numeric, :message)
200
+ message = options[:is_message] || options[:message]
214
201
  message %= { is: } unless message.nil?
215
202
 
216
203
  raise ValidationError, message || I18n.t(
@@ -220,14 +207,20 @@ module CMDx
220
207
  )
221
208
  end
222
209
 
223
- # Raises validation error for forbidden exact value violations.
210
+ # Raises a validation error for exact value exclusion validation.
211
+ #
212
+ # @param is_not [Numeric] the exact value that is not allowed
213
+ # @param options [Hash] validation options
214
+ #
215
+ # @return [void]
216
+ #
217
+ # @raise [ValidationError] always raised with appropriate message
224
218
  #
225
- # @param is_not [Numeric] Forbidden exact value
226
- # @param options [Hash] Validation options containing error messages
227
- # @raise [ValidationError] With formatted error message
219
+ # @example
220
+ # raise_is_not_validation_error!(0, {})
221
+ # # raises ValidationError: "must not be 0"
228
222
  def raise_is_not_validation_error!(is_not, options)
229
- message = options.dig(:numeric, :is_not_message) ||
230
- options.dig(:numeric, :message)
223
+ message = options[:is_not_message] || options[:message]
231
224
  message %= { is_not: } unless message.nil?
232
225
 
233
226
  raise ValidationError, message || I18n.t(