cmdx 0.5.0 → 1.0.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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +16 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +31 -1
  7. data/README.md +72 -25
  8. data/docs/ai_prompts.md +309 -0
  9. data/docs/basics/call.md +225 -14
  10. data/docs/basics/chain.md +271 -0
  11. data/docs/basics/context.md +232 -33
  12. data/docs/basics/setup.md +76 -12
  13. data/docs/callbacks.md +273 -0
  14. data/docs/configuration.md +158 -28
  15. data/docs/getting_started.md +134 -22
  16. data/docs/interruptions/exceptions.md +189 -11
  17. data/docs/interruptions/faults.md +187 -44
  18. data/docs/interruptions/halt.md +179 -35
  19. data/docs/logging.md +194 -53
  20. data/docs/middlewares.md +735 -0
  21. data/docs/outcomes/result.md +296 -10
  22. data/docs/outcomes/states.md +203 -31
  23. data/docs/outcomes/statuses.md +275 -30
  24. data/docs/parameters/coercions.md +402 -29
  25. data/docs/parameters/defaults.md +249 -25
  26. data/docs/parameters/definitions.md +238 -72
  27. data/docs/parameters/namespacing.md +250 -27
  28. data/docs/parameters/validations.md +193 -168
  29. data/docs/testing.md +550 -0
  30. data/docs/tips_and_tricks.md +95 -43
  31. data/docs/workflows.md +319 -0
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +69 -0
  34. data/lib/cmdx/callback_registry.rb +106 -0
  35. data/lib/cmdx/chain.rb +190 -0
  36. data/lib/cmdx/chain_inspector.rb +149 -0
  37. data/lib/cmdx/chain_serializer.rb +175 -0
  38. data/lib/cmdx/coercions/array.rb +37 -0
  39. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  40. data/lib/cmdx/coercions/boolean.rb +41 -1
  41. data/lib/cmdx/coercions/complex.rb +31 -0
  42. data/lib/cmdx/coercions/date.rb +39 -0
  43. data/lib/cmdx/coercions/date_time.rb +39 -0
  44. data/lib/cmdx/coercions/float.rb +31 -0
  45. data/lib/cmdx/coercions/hash.rb +42 -0
  46. data/lib/cmdx/coercions/integer.rb +32 -0
  47. data/lib/cmdx/coercions/rational.rb +31 -0
  48. data/lib/cmdx/coercions/string.rb +31 -0
  49. data/lib/cmdx/coercions/time.rb +39 -0
  50. data/lib/cmdx/coercions/virtual.rb +31 -0
  51. data/lib/cmdx/configuration.rb +217 -9
  52. data/lib/cmdx/context.rb +173 -2
  53. data/lib/cmdx/core_ext/hash.rb +72 -0
  54. data/lib/cmdx/core_ext/module.rb +94 -0
  55. data/lib/cmdx/core_ext/object.rb +105 -0
  56. data/lib/cmdx/correlator.rb +217 -0
  57. data/lib/cmdx/error.rb +210 -8
  58. data/lib/cmdx/errors.rb +256 -1
  59. data/lib/cmdx/fault.rb +177 -2
  60. data/lib/cmdx/faults.rb +158 -2
  61. data/lib/cmdx/immutator.rb +121 -2
  62. data/lib/cmdx/lazy_struct.rb +261 -18
  63. data/lib/cmdx/log_formatters/json.rb +46 -0
  64. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  65. data/lib/cmdx/log_formatters/line.rb +54 -0
  66. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  67. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  68. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  69. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  70. data/lib/cmdx/log_formatters/raw.rb +54 -0
  71. data/lib/cmdx/logger.rb +85 -0
  72. data/lib/cmdx/logger_ansi.rb +93 -7
  73. data/lib/cmdx/logger_serializer.rb +116 -0
  74. data/lib/cmdx/middleware.rb +74 -0
  75. data/lib/cmdx/middleware_registry.rb +106 -0
  76. data/lib/cmdx/middlewares/correlate.rb +266 -0
  77. data/lib/cmdx/middlewares/timeout.rb +232 -0
  78. data/lib/cmdx/parameter.rb +228 -1
  79. data/lib/cmdx/parameter_inspector.rb +61 -0
  80. data/lib/cmdx/parameter_registry.rb +125 -0
  81. data/lib/cmdx/parameter_serializer.rb +83 -0
  82. data/lib/cmdx/parameter_validator.rb +62 -0
  83. data/lib/cmdx/parameter_value.rb +109 -1
  84. data/lib/cmdx/parameters_inspector.rb +59 -0
  85. data/lib/cmdx/parameters_serializer.rb +102 -0
  86. data/lib/cmdx/railtie.rb +123 -3
  87. data/lib/cmdx/result.rb +367 -25
  88. data/lib/cmdx/result_ansi.rb +105 -9
  89. data/lib/cmdx/result_inspector.rb +76 -0
  90. data/lib/cmdx/result_logger.rb +90 -3
  91. data/lib/cmdx/result_serializer.rb +137 -0
  92. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  93. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  94. data/lib/cmdx/task.rb +405 -37
  95. data/lib/cmdx/task_serializer.rb +74 -2
  96. data/lib/cmdx/utils/ansi_color.rb +95 -0
  97. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  98. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  99. data/lib/cmdx/utils/name_affix.rb +78 -0
  100. data/lib/cmdx/validators/custom.rb +82 -0
  101. data/lib/cmdx/validators/exclusion.rb +94 -0
  102. data/lib/cmdx/validators/format.rb +102 -8
  103. data/lib/cmdx/validators/inclusion.rb +104 -0
  104. data/lib/cmdx/validators/length.rb +128 -0
  105. data/lib/cmdx/validators/numeric.rb +128 -0
  106. data/lib/cmdx/validators/presence.rb +93 -7
  107. data/lib/cmdx/version.rb +7 -1
  108. data/lib/cmdx/workflow.rb +394 -0
  109. data/lib/cmdx.rb +25 -64
  110. data/lib/generators/cmdx/install_generator.rb +37 -1
  111. data/lib/generators/cmdx/task_generator.rb +69 -1
  112. data/lib/generators/cmdx/templates/install.rb +8 -12
  113. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  114. metadata +54 -15
  115. data/docs/basics/run.md +0 -34
  116. data/docs/batch.md +0 -53
  117. data/docs/example.md +0 -82
  118. data/docs/hooks.md +0 -62
  119. data/lib/cmdx/batch.rb +0 -43
  120. data/lib/cmdx/parameters.rb +0 -35
  121. data/lib/cmdx/run.rb +0 -39
  122. data/lib/cmdx/run_inspector.rb +0 -26
  123. data/lib/cmdx/run_serializer.rb +0 -20
  124. data/lib/cmdx/task_hook.rb +0 -18
  125. data/lib/generators/cmdx/batch_generator.rb +0 -30
  126. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
@@ -2,19 +2,113 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
+ # Format validator for parameter validation using regular expressions.
6
+ #
7
+ # The Format validator validates parameter values against regular expression
8
+ # patterns. It supports both positive matching (with) and negative matching
9
+ # (without) patterns, and can combine both for complex format validation.
10
+ #
11
+ # @example Basic format validation with positive pattern
12
+ # class ProcessUserTask < CMDx::Task
13
+ # required :email, format: { with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i }
14
+ # required :phone, format: { with: /\A\d{3}-\d{3}-\d{4}\z/ }
15
+ # end
16
+ #
17
+ # @example Format validation with negative pattern
18
+ # class ProcessContentTask < CMDx::Task
19
+ # required :username, format: { without: /\A(admin|root|system)\z/i }
20
+ # required :content, format: { without: /spam|viagra/i }
21
+ # end
22
+ #
23
+ # @example Combined positive and negative patterns
24
+ # class ProcessUserTask < CMDx::Task
25
+ # required :password, format: {
26
+ # with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}\z/, # Strong password
27
+ # without: /password|123456/i # Common weak patterns
28
+ # }
29
+ # end
30
+ #
31
+ # @example Custom error message
32
+ # class ProcessUserTask < CMDx::Task
33
+ # required :email, format: {
34
+ # with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
35
+ # message: "must be a valid email address"
36
+ # }
37
+ # end
38
+ #
39
+ # @example Format validation behavior
40
+ # # Positive pattern matching
41
+ # Format.call("user@example.com", format: { with: /@/ }) # passes
42
+ # Format.call("invalid-email", format: { with: /@/ }) # raises ValidationError
43
+ #
44
+ # # Negative pattern matching
45
+ # Format.call("username", format: { without: /admin/ }) # passes
46
+ # Format.call("admin", format: { without: /admin/ }) # raises ValidationError
47
+ #
48
+ # @see CMDx::Parameter Parameter validation integration
49
+ # @see CMDx::ValidationError Raised when validation fails
5
50
  module Format
6
51
 
7
52
  module_function
8
53
 
54
+ # Validates that a parameter value matches the specified format patterns.
55
+ #
56
+ # Validates the value against the provided regular expression patterns.
57
+ # Supports positive matching (with), negative matching (without), or both.
58
+ # The value must match all specified conditions to pass validation.
59
+ #
60
+ # @param value [String] The parameter value to validate
61
+ # @param options [Hash] Validation configuration options
62
+ # @option options [Hash] :format Format validation configuration
63
+ # @option options [Regexp] :format.with Pattern the value must match
64
+ # @option options [Regexp] :format.without Pattern the value must not match
65
+ # @option options [String] :format.message Custom error message
66
+ #
67
+ # @return [void]
68
+ # @raise [ValidationError] If value doesn't match the format requirements
69
+ #
70
+ # @example Successful positive pattern validation
71
+ # Format.call("user@example.com", format: { with: /@/ })
72
+ # # => passes without error
73
+ #
74
+ # @example Failed positive pattern validation
75
+ # Format.call("invalid-email", format: { with: /@/ })
76
+ # # => raises ValidationError: "is an invalid format"
77
+ #
78
+ # @example Successful negative pattern validation
79
+ # Format.call("username", format: { without: /admin/ })
80
+ # # => passes without error
81
+ #
82
+ # @example Failed negative pattern validation
83
+ # Format.call("admin", format: { without: /admin/ })
84
+ # # => raises ValidationError: "is an invalid format"
85
+ #
86
+ # @example Combined pattern validation
87
+ # Format.call("StrongPass123", format: {
88
+ # with: /\A(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}\z/,
89
+ # without: /password/i
90
+ # })
91
+ # # => passes without error
92
+ #
93
+ # @example Custom error message
94
+ # Format.call("weak", format: {
95
+ # with: /\A.{8,}\z/,
96
+ # message: "must be at least 8 characters"
97
+ # })
98
+ # # => raises ValidationError: "must be at least 8 characters"
9
99
  def call(value, options = {})
10
- return if case options[:format]
11
- in { with: with, without: without }
12
- value.match?(with) && !value.match?(without)
13
- in { with: with }
14
- value.match?(with)
15
- in { without: without }
16
- !value.match?(without)
17
- end
100
+ valid = case options[:format]
101
+ in { with: with, without: without }
102
+ value.match?(with) && !value.match?(without)
103
+ in { with: with }
104
+ value.match?(with)
105
+ in { without: without }
106
+ !value.match?(without)
107
+ else
108
+ false
109
+ end
110
+
111
+ return if valid
18
112
 
19
113
  raise ValidationError, options.dig(:format, :message) || I18n.t(
20
114
  "cmdx.validators.format",
@@ -2,10 +2,103 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
+ # Inclusion validator for parameter validation against allowed values.
6
+ #
7
+ # The Inclusion validator ensures that parameter values ARE within a
8
+ # specified set of allowed values. It supports both array-based inclusion
9
+ # (specific values) and range-based inclusion (value ranges).
10
+ #
11
+ # @example Basic inclusion validation with array
12
+ # class ProcessOrderTask < CMDx::Task
13
+ # required :status, inclusion: { in: ['pending', 'processing', 'completed'] }
14
+ # required :priority, inclusion: { in: [1, 2, 3, 4, 5] }
15
+ # end
16
+ #
17
+ # @example Range-based inclusion
18
+ # class ProcessUserTask < CMDx::Task
19
+ # required :age, inclusion: { in: 18..120 } # Valid age range
20
+ # required :score, inclusion: { within: 0..100 } # Percentage score
21
+ # end
22
+ #
23
+ # @example Custom error messages
24
+ # class ProcessOrderTask < CMDx::Task
25
+ # required :status, inclusion: {
26
+ # in: ['pending', 'processing', 'completed'],
27
+ # of_message: "must be a valid order status"
28
+ # }
29
+ # required :age, inclusion: {
30
+ # in: 18..120,
31
+ # in_message: "must be between %{min} and %{max} years old"
32
+ # }
33
+ # end
34
+ #
35
+ # @example Boolean field validation
36
+ # class ProcessUserTask < CMDx::Task
37
+ # required :active, inclusion: { in: [true, false] } # Proper boolean validation
38
+ # required :role, inclusion: { in: ['admin', 'user', 'guest'] }
39
+ # end
40
+ #
41
+ # @example Inclusion validation behavior
42
+ # # Array inclusion
43
+ # Inclusion.call("pending", inclusion: { in: ['pending', 'active'] }) # passes
44
+ # Inclusion.call("cancelled", inclusion: { in: ['pending', 'active'] }) # raises ValidationError
45
+ #
46
+ # # Range inclusion
47
+ # Inclusion.call(25, inclusion: { in: 18..65 }) # passes
48
+ # Inclusion.call(15, inclusion: { in: 18..65 }) # raises ValidationError
49
+ #
50
+ # @see CMDx::Validators::Exclusion For validating values must not be in a set
51
+ # @see CMDx::Parameter Parameter validation integration
52
+ # @see CMDx::ValidationError Raised when validation fails
5
53
  module Inclusion
6
54
 
7
55
  extend self
8
56
 
57
+ # Validates that a parameter value is in the allowed set.
58
+ #
59
+ # Checks that the value is present in the specified array or range
60
+ # of allowed values. Raises ValidationError if the value is not found
61
+ # in the inclusion set.
62
+ #
63
+ # @param value [Object] The parameter value to validate
64
+ # @param options [Hash] Validation configuration options
65
+ # @option options [Hash] :inclusion Inclusion validation configuration
66
+ # @option options [Array, Range] :inclusion.in Values/range to include
67
+ # @option options [Array, Range] :inclusion.within Alias for :in
68
+ # @option options [String] :inclusion.of_message Error message for array inclusion
69
+ # @option options [String] :inclusion.in_message Error message for range inclusion
70
+ # @option options [String] :inclusion.within_message Alias for :in_message
71
+ # @option options [String] :inclusion.message General error message override
72
+ #
73
+ # @return [void]
74
+ # @raise [ValidationError] If value is not found in the inclusion set
75
+ #
76
+ # @example Array inclusion validation
77
+ # Inclusion.call("active", inclusion: { in: ['active', 'pending'] })
78
+ # # => passes without error
79
+ #
80
+ # @example Failed array inclusion
81
+ # Inclusion.call("cancelled", inclusion: { in: ['active', 'pending'] })
82
+ # # => raises ValidationError: "must be one of: \"active\", \"pending\""
83
+ #
84
+ # @example Range inclusion validation
85
+ # Inclusion.call(25, inclusion: { in: 18..65 })
86
+ # # => passes without error
87
+ #
88
+ # @example Failed range inclusion
89
+ # Inclusion.call(15, inclusion: { in: 18..65 })
90
+ # # => raises ValidationError: "must be within 18 and 65"
91
+ #
92
+ # @example Boolean validation
93
+ # Inclusion.call(true, inclusion: { in: [true, false] })
94
+ # # => passes without error
95
+ #
96
+ # @example Custom error messages
97
+ # Inclusion.call("invalid", inclusion: {
98
+ # in: ['valid', 'pending'],
99
+ # of_message: "status must be valid or pending"
100
+ # })
101
+ # # => raises ValidationError: "status must be valid or pending"
9
102
  def call(value, options = {})
10
103
  values = options.dig(:inclusion, :in) ||
11
104
  options.dig(:inclusion, :within)
@@ -19,6 +112,11 @@ module CMDx
19
112
 
20
113
  private
21
114
 
115
+ # Raises validation error for array-based inclusion violations.
116
+ #
117
+ # @param values [Array] The allowed values array
118
+ # @param options [Hash] Validation options containing error messages
119
+ # @raise [ValidationError] With formatted error message
22
120
  def raise_of_validation_error!(values, options)
23
121
  values = values.map(&:inspect).join(", ")
24
122
  message = options.dig(:inclusion, :of_message) ||
@@ -32,6 +130,12 @@ module CMDx
32
130
  )
33
131
  end
34
132
 
133
+ # Raises validation error for range-based inclusion violations.
134
+ #
135
+ # @param min [Object] Range minimum value
136
+ # @param max [Object] Range maximum value
137
+ # @param options [Hash] Validation options containing error messages
138
+ # @raise [ValidationError] With formatted error message
35
139
  def raise_within_validation_error!(min, max, options)
36
140
  message = options.dig(:inclusion, :in_message) ||
37
141
  options.dig(:inclusion, :within_message) ||
@@ -2,10 +2,106 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
+ # Length validator for parameter validation based on size constraints.
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
5
55
  module Length
6
56
 
7
57
  extend self
8
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
77
+ #
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
81
+ #
82
+ # @example Range validation
83
+ # Length.call("hello", length: { within: 3..10 })
84
+ # # => passes without error
85
+ #
86
+ # @example Failed range validation
87
+ # Length.call("hi", length: { within: 3..10 })
88
+ # # => raises ValidationError: "length must be within 3 and 10"
89
+ #
90
+ # @example Minimum length validation
91
+ # Length.call("password123", length: { min: 8 })
92
+ # # => passes without error
93
+ #
94
+ # @example Combined min/max validation
95
+ # Length.call("username", length: { min: 3, max: 20 })
96
+ # # => passes without error
97
+ #
98
+ # @example Exact length validation
99
+ # Length.call("US", length: { is: 2 })
100
+ # # => passes without error (country code)
101
+ #
102
+ # @example Array length validation
103
+ # Length.call([1, 2, 3, 4], length: { max: 5 })
104
+ # # => passes without error
9
105
  def call(value, options = {})
10
106
  case options[:length]
11
107
  in { within: within }
@@ -33,6 +129,12 @@ module CMDx
33
129
 
34
130
  private
35
131
 
132
+ # Raises validation error for range-based length violations.
133
+ #
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
36
138
  def raise_within_validation_error!(min, max, options)
37
139
  message = options.dig(:length, :within_message) ||
38
140
  options.dig(:length, :in_message) ||
@@ -47,6 +149,12 @@ module CMDx
47
149
  )
48
150
  end
49
151
 
152
+ # Raises validation error for forbidden range violations.
153
+ #
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
50
158
  def raise_not_within_validation_error!(min, max, options)
51
159
  message = options.dig(:length, :not_within_message) ||
52
160
  options.dig(:length, :not_in_message) ||
@@ -61,6 +169,11 @@ module CMDx
61
169
  )
62
170
  end
63
171
 
172
+ # Raises validation error for minimum length violations.
173
+ #
174
+ # @param min [Integer] Minimum required length
175
+ # @param options [Hash] Validation options containing error messages
176
+ # @raise [ValidationError] With formatted error message
64
177
  def raise_min_validation_error!(min, options)
65
178
  message = options.dig(:length, :min_message) ||
66
179
  options.dig(:length, :message)
@@ -73,6 +186,11 @@ module CMDx
73
186
  )
74
187
  end
75
188
 
189
+ # Raises validation error for maximum length violations.
190
+ #
191
+ # @param max [Integer] Maximum allowed length
192
+ # @param options [Hash] Validation options containing error messages
193
+ # @raise [ValidationError] With formatted error message
76
194
  def raise_max_validation_error!(max, options)
77
195
  message = options.dig(:length, :max_message) ||
78
196
  options.dig(:length, :message)
@@ -85,6 +203,11 @@ module CMDx
85
203
  )
86
204
  end
87
205
 
206
+ # Raises validation error for exact length violations.
207
+ #
208
+ # @param is [Integer] Required exact length
209
+ # @param options [Hash] Validation options containing error messages
210
+ # @raise [ValidationError] With formatted error message
88
211
  def raise_is_validation_error!(is, options)
89
212
  message = options.dig(:length, :is_message) ||
90
213
  options.dig(:length, :message)
@@ -97,6 +220,11 @@ module CMDx
97
220
  )
98
221
  end
99
222
 
223
+ # Raises validation error for forbidden exact length violations.
224
+ #
225
+ # @param is_not [Integer] Forbidden exact length
226
+ # @param options [Hash] Validation options containing error messages
227
+ # @raise [ValidationError] With formatted error message
100
228
  def raise_is_not_validation_error!(is_not, options)
101
229
  message = options.dig(:length, :is_not_message) ||
102
230
  options.dig(:length, :message)
@@ -2,10 +2,106 @@
2
2
 
3
3
  module CMDx
4
4
  module Validators
5
+ # Numeric validator for parameter validation based on numeric value constraints.
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
5
55
  module Numeric
6
56
 
7
57
  extend self
8
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
77
+ #
78
+ # @return [void]
79
+ # @raise [ValidationError] If value doesn't meet the numeric constraints
80
+ # @raise [ArgumentError] If no valid numeric constraint options are provided
81
+ #
82
+ # @example Range validation
83
+ # Numeric.call(50, numeric: { within: 1..100 })
84
+ # # => passes without error
85
+ #
86
+ # @example Failed range validation
87
+ # Numeric.call(150, numeric: { within: 1..100 })
88
+ # # => raises ValidationError: "must be within 1 and 100"
89
+ #
90
+ # @example Minimum value validation
91
+ # Numeric.call(25, numeric: { min: 18 })
92
+ # # => passes without error
93
+ #
94
+ # @example Combined min/max validation
95
+ # Numeric.call(3.5, numeric: { min: 1.0, max: 5.0 })
96
+ # # => passes without error
97
+ #
98
+ # @example Exact value validation
99
+ # Numeric.call(42, numeric: { is: 42 })
100
+ # # => passes without error
101
+ #
102
+ # @example Float validation
103
+ # Numeric.call(19.99, numeric: { max: 20.0 })
104
+ # # => passes without error
9
105
  def call(value, options = {})
10
106
  case options[:numeric]
11
107
  in { within: within }
@@ -33,6 +129,12 @@ module CMDx
33
129
 
34
130
  private
35
131
 
132
+ # Raises validation error for range-based numeric violations.
133
+ #
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
36
138
  def raise_within_validation_error!(min, max, options)
37
139
  message = options.dig(:numeric, :within_message) ||
38
140
  options.dig(:numeric, :in_message) ||
@@ -47,6 +149,12 @@ module CMDx
47
149
  )
48
150
  end
49
151
 
152
+ # Raises validation error for forbidden range violations.
153
+ #
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
50
158
  def raise_not_within_validation_error!(min, max, options)
51
159
  message = options.dig(:numeric, :not_within_message) ||
52
160
  options.dig(:numeric, :not_in_message) ||
@@ -61,6 +169,11 @@ module CMDx
61
169
  )
62
170
  end
63
171
 
172
+ # Raises validation error for minimum value violations.
173
+ #
174
+ # @param min [Numeric] Minimum required value
175
+ # @param options [Hash] Validation options containing error messages
176
+ # @raise [ValidationError] With formatted error message
64
177
  def raise_min_validation_error!(min, options)
65
178
  message = options.dig(:numeric, :min_message) ||
66
179
  options.dig(:numeric, :message)
@@ -73,6 +186,11 @@ module CMDx
73
186
  )
74
187
  end
75
188
 
189
+ # Raises validation error for maximum value violations.
190
+ #
191
+ # @param max [Numeric] Maximum allowed value
192
+ # @param options [Hash] Validation options containing error messages
193
+ # @raise [ValidationError] With formatted error message
76
194
  def raise_max_validation_error!(max, options)
77
195
  message = options.dig(:numeric, :max_message) ||
78
196
  options.dig(:numeric, :message)
@@ -85,6 +203,11 @@ module CMDx
85
203
  )
86
204
  end
87
205
 
206
+ # Raises validation error for exact value violations.
207
+ #
208
+ # @param is [Numeric] Required exact value
209
+ # @param options [Hash] Validation options containing error messages
210
+ # @raise [ValidationError] With formatted error message
88
211
  def raise_is_validation_error!(is, options)
89
212
  message = options.dig(:numeric, :is_message) ||
90
213
  options.dig(:numeric, :message)
@@ -97,6 +220,11 @@ module CMDx
97
220
  )
98
221
  end
99
222
 
223
+ # Raises validation error for forbidden exact value violations.
224
+ #
225
+ # @param is_not [Numeric] Forbidden exact value
226
+ # @param options [Hash] Validation options containing error messages
227
+ # @raise [ValidationError] With formatted error message
100
228
  def raise_is_not_validation_error!(is_not, options)
101
229
  message = options.dig(:numeric, :is_not_message) ||
102
230
  options.dig(:numeric, :message)