cmdx 1.20.0 → 2.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 (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +131 -1
  3. data/README.md +37 -24
  4. data/lib/cmdx/.DS_Store +0 -0
  5. data/lib/cmdx/callbacks.rb +179 -0
  6. data/lib/cmdx/chain.rb +78 -175
  7. data/lib/cmdx/coercions/array.rb +19 -33
  8. data/lib/cmdx/coercions/big_decimal.rb +12 -29
  9. data/lib/cmdx/coercions/boolean.rb +25 -45
  10. data/lib/cmdx/coercions/coerce.rb +32 -0
  11. data/lib/cmdx/coercions/complex.rb +12 -27
  12. data/lib/cmdx/coercions/date.rb +29 -33
  13. data/lib/cmdx/coercions/date_time.rb +29 -33
  14. data/lib/cmdx/coercions/float.rb +8 -29
  15. data/lib/cmdx/coercions/hash.rb +17 -43
  16. data/lib/cmdx/coercions/integer.rb +8 -32
  17. data/lib/cmdx/coercions/rational.rb +12 -33
  18. data/lib/cmdx/coercions/string.rb +6 -24
  19. data/lib/cmdx/coercions/symbol.rb +12 -26
  20. data/lib/cmdx/coercions/time.rb +31 -35
  21. data/lib/cmdx/coercions.rb +174 -0
  22. data/lib/cmdx/configuration.rb +45 -225
  23. data/lib/cmdx/context.rb +263 -242
  24. data/lib/cmdx/deprecation.rb +67 -0
  25. data/lib/cmdx/deprecators/error.rb +22 -0
  26. data/lib/cmdx/deprecators/log.rb +22 -0
  27. data/lib/cmdx/deprecators/warn.rb +21 -0
  28. data/lib/cmdx/deprecators.rb +101 -0
  29. data/lib/cmdx/errors.rb +145 -79
  30. data/lib/cmdx/executors/fiber.rb +42 -0
  31. data/lib/cmdx/executors/thread.rb +36 -0
  32. data/lib/cmdx/executors.rb +95 -0
  33. data/lib/cmdx/fault.rb +85 -78
  34. data/lib/cmdx/i18n_proxy.rb +104 -0
  35. data/lib/cmdx/input.rb +294 -0
  36. data/lib/cmdx/inputs.rb +218 -0
  37. data/lib/cmdx/log_formatters/json.rb +9 -20
  38. data/lib/cmdx/log_formatters/key_value.rb +10 -21
  39. data/lib/cmdx/log_formatters/line.rb +7 -19
  40. data/lib/cmdx/log_formatters/logstash.rb +8 -21
  41. data/lib/cmdx/log_formatters/raw.rb +8 -20
  42. data/lib/cmdx/logger_proxy.rb +30 -0
  43. data/lib/cmdx/mergers/deep_merge.rb +23 -0
  44. data/lib/cmdx/mergers/last_write_wins.rb +23 -0
  45. data/lib/cmdx/mergers/no_merge.rb +20 -0
  46. data/lib/cmdx/mergers.rb +95 -0
  47. data/lib/cmdx/middlewares.rb +128 -0
  48. data/lib/cmdx/output.rb +115 -0
  49. data/lib/cmdx/outputs.rb +66 -0
  50. data/lib/cmdx/pipeline.rb +144 -131
  51. data/lib/cmdx/railtie.rb +10 -36
  52. data/lib/cmdx/result.rb +252 -473
  53. data/lib/cmdx/retriers/bounded_random.rb +24 -0
  54. data/lib/cmdx/retriers/decorrelated_jitter.rb +28 -0
  55. data/lib/cmdx/retriers/exponential.rb +23 -0
  56. data/lib/cmdx/retriers/fibonacci.rb +39 -0
  57. data/lib/cmdx/retriers/full_random.rb +23 -0
  58. data/lib/cmdx/retriers/half_random.rb +24 -0
  59. data/lib/cmdx/retriers/linear.rb +23 -0
  60. data/lib/cmdx/retriers.rb +106 -0
  61. data/lib/cmdx/retry.rb +117 -138
  62. data/lib/cmdx/runtime.rb +251 -0
  63. data/lib/cmdx/settings.rb +68 -196
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -336
  66. data/lib/cmdx/telemetry.rb +108 -0
  67. data/lib/cmdx/util.rb +73 -0
  68. data/lib/cmdx/validators/absence.rb +10 -39
  69. data/lib/cmdx/validators/exclusion.rb +33 -52
  70. data/lib/cmdx/validators/format.rb +19 -49
  71. data/lib/cmdx/validators/inclusion.rb +33 -54
  72. data/lib/cmdx/validators/length.rb +125 -127
  73. data/lib/cmdx/validators/numeric.rb +123 -123
  74. data/lib/cmdx/validators/presence.rb +10 -39
  75. data/lib/cmdx/validators/validate.rb +31 -0
  76. data/lib/cmdx/validators.rb +161 -0
  77. data/lib/cmdx/version.rb +2 -4
  78. data/lib/cmdx/workflow.rb +74 -82
  79. data/lib/cmdx.rb +111 -42
  80. data/lib/generators/cmdx/install_generator.rb +7 -17
  81. data/lib/generators/cmdx/task_generator.rb +12 -29
  82. data/lib/generators/cmdx/templates/install.rb +128 -52
  83. data/lib/generators/cmdx/templates/task.rb.tt +1 -1
  84. data/lib/generators/cmdx/templates/workflow.rb.tt +1 -2
  85. data/lib/generators/cmdx/workflow_generator.rb +12 -29
  86. data/lib/locales/en.yml +9 -6
  87. data/mkdocs.yml +25 -23
  88. metadata +39 -138
  89. data/lib/cmdx/attribute.rb +0 -440
  90. data/lib/cmdx/attribute_registry.rb +0 -185
  91. data/lib/cmdx/attribute_value.rb +0 -252
  92. data/lib/cmdx/callback_registry.rb +0 -169
  93. data/lib/cmdx/coercion_registry.rb +0 -138
  94. data/lib/cmdx/deprecator.rb +0 -77
  95. data/lib/cmdx/exception.rb +0 -46
  96. data/lib/cmdx/executor.rb +0 -374
  97. data/lib/cmdx/identifier.rb +0 -30
  98. data/lib/cmdx/locale.rb +0 -78
  99. data/lib/cmdx/middleware_registry.rb +0 -148
  100. data/lib/cmdx/middlewares/correlate.rb +0 -140
  101. data/lib/cmdx/middlewares/runtime.rb +0 -62
  102. data/lib/cmdx/middlewares/timeout.rb +0 -78
  103. data/lib/cmdx/parallelizer.rb +0 -100
  104. data/lib/cmdx/utils/call.rb +0 -53
  105. data/lib/cmdx/utils/condition.rb +0 -71
  106. data/lib/cmdx/utils/format.rb +0 -82
  107. data/lib/cmdx/utils/normalize.rb +0 -52
  108. data/lib/cmdx/utils/wrap.rb +0 -38
  109. data/lib/cmdx/validator_registry.rb +0 -143
  110. data/lib/generators/cmdx/locale_generator.rb +0 -39
  111. data/lib/locales/af.yml +0 -53
  112. data/lib/locales/ar.yml +0 -53
  113. data/lib/locales/az.yml +0 -53
  114. data/lib/locales/be.yml +0 -53
  115. data/lib/locales/bg.yml +0 -53
  116. data/lib/locales/bn.yml +0 -53
  117. data/lib/locales/bs.yml +0 -53
  118. data/lib/locales/ca.yml +0 -53
  119. data/lib/locales/cnr.yml +0 -53
  120. data/lib/locales/cs.yml +0 -53
  121. data/lib/locales/cy.yml +0 -53
  122. data/lib/locales/da.yml +0 -53
  123. data/lib/locales/de.yml +0 -53
  124. data/lib/locales/dz.yml +0 -53
  125. data/lib/locales/el.yml +0 -53
  126. data/lib/locales/eo.yml +0 -53
  127. data/lib/locales/es.yml +0 -53
  128. data/lib/locales/et.yml +0 -53
  129. data/lib/locales/eu.yml +0 -53
  130. data/lib/locales/fa.yml +0 -53
  131. data/lib/locales/fi.yml +0 -53
  132. data/lib/locales/fr.yml +0 -53
  133. data/lib/locales/fy.yml +0 -53
  134. data/lib/locales/gd.yml +0 -53
  135. data/lib/locales/gl.yml +0 -53
  136. data/lib/locales/he.yml +0 -53
  137. data/lib/locales/hi.yml +0 -53
  138. data/lib/locales/hr.yml +0 -53
  139. data/lib/locales/hu.yml +0 -53
  140. data/lib/locales/hy.yml +0 -53
  141. data/lib/locales/id.yml +0 -53
  142. data/lib/locales/is.yml +0 -53
  143. data/lib/locales/it.yml +0 -53
  144. data/lib/locales/ja.yml +0 -53
  145. data/lib/locales/ka.yml +0 -53
  146. data/lib/locales/kk.yml +0 -53
  147. data/lib/locales/km.yml +0 -53
  148. data/lib/locales/kn.yml +0 -53
  149. data/lib/locales/ko.yml +0 -53
  150. data/lib/locales/lb.yml +0 -53
  151. data/lib/locales/lo.yml +0 -53
  152. data/lib/locales/lt.yml +0 -53
  153. data/lib/locales/lv.yml +0 -53
  154. data/lib/locales/mg.yml +0 -53
  155. data/lib/locales/mk.yml +0 -53
  156. data/lib/locales/ml.yml +0 -53
  157. data/lib/locales/mn.yml +0 -53
  158. data/lib/locales/mr-IN.yml +0 -53
  159. data/lib/locales/ms.yml +0 -53
  160. data/lib/locales/nb.yml +0 -53
  161. data/lib/locales/ne.yml +0 -53
  162. data/lib/locales/nl.yml +0 -53
  163. data/lib/locales/nn.yml +0 -53
  164. data/lib/locales/oc.yml +0 -53
  165. data/lib/locales/or.yml +0 -53
  166. data/lib/locales/pa.yml +0 -53
  167. data/lib/locales/pl.yml +0 -53
  168. data/lib/locales/pt.yml +0 -53
  169. data/lib/locales/rm.yml +0 -53
  170. data/lib/locales/ro.yml +0 -53
  171. data/lib/locales/ru.yml +0 -53
  172. data/lib/locales/sc.yml +0 -53
  173. data/lib/locales/sk.yml +0 -53
  174. data/lib/locales/sl.yml +0 -53
  175. data/lib/locales/sq.yml +0 -53
  176. data/lib/locales/sr.yml +0 -53
  177. data/lib/locales/st.yml +0 -53
  178. data/lib/locales/sv.yml +0 -53
  179. data/lib/locales/sw.yml +0 -53
  180. data/lib/locales/ta.yml +0 -53
  181. data/lib/locales/te.yml +0 -53
  182. data/lib/locales/th.yml +0 -53
  183. data/lib/locales/tl.yml +0 -53
  184. data/lib/locales/tr.yml +0 -53
  185. data/lib/locales/tt.yml +0 -53
  186. data/lib/locales/ug.yml +0 -53
  187. data/lib/locales/uk.yml +0 -53
  188. data/lib/locales/ur.yml +0 -53
  189. data/lib/locales/uz.yml +0 -53
  190. data/lib/locales/vi.yml +0 -53
  191. data/lib/locales/wo.yml +0 -53
  192. data/lib/locales/zh-CN.yml +0 -53
  193. data/lib/locales/zh-HK.yml +0 -53
  194. data/lib/locales/zh-TW.yml +0 -53
  195. data/lib/locales/zh-YUE.yml +0 -53
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Pub/sub for runtime lifecycle events (see {EVENTS}). Subscribers are
5
+ # callables receiving an {Event} data object. Runtime emits events only when
6
+ # subscribers are registered so telemetry has zero cost when unused.
7
+ class Telemetry
8
+
9
+ # Immutable event payload passed to subscribers.
10
+ Event = Data.define(:xid, :cid, :root, :type, :task, :tid, :name, :payload, :timestamp)
11
+
12
+ # Lifecycle event names Runtime emits.
13
+ EVENTS = %i[
14
+ task_started
15
+ task_deprecated
16
+ task_retried
17
+ task_rolled_back
18
+ task_executed
19
+ ].freeze
20
+
21
+ attr_reader :registry
22
+
23
+ def initialize
24
+ @registry = {}
25
+ end
26
+
27
+ # @param source [Telemetry] registry to duplicate
28
+ # @return [void]
29
+ def initialize_copy(source)
30
+ @registry = source.registry.transform_values(&:dup)
31
+ end
32
+
33
+ # Registers a subscriber for `event`.
34
+ #
35
+ # @param event [Symbol] one of {EVENTS}
36
+ # @param callable [#call, nil] subscriber callable; pass either this or a block
37
+ # @param block [#call, nil] subscriber when `callable` is omitted
38
+ # @yieldparam evt [Event]
39
+ # @return [Telemetry] self for chaining
40
+ # @raise [ArgumentError] when both `callable` and a block are provided, when
41
+ # the subscriber isn't callable, or when `event` is unknown
42
+ def subscribe(event, callable = nil, &block)
43
+ subscriber = callable || block
44
+
45
+ if callable && block
46
+ raise ArgumentError, "provide either a callable or a block, not both"
47
+ elsif !subscriber.respond_to?(:call)
48
+ raise ArgumentError, "subscriber must respond to #call"
49
+ elsif !EVENTS.include?(event)
50
+ raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}"
51
+ end
52
+
53
+ (registry[event] ||= []) << subscriber
54
+ self
55
+ end
56
+
57
+ # Removes a previously registered subscriber. Drops the event entry
58
+ # entirely when no subscribers remain.
59
+ #
60
+ # @param event [Symbol] one of {EVENTS}
61
+ # @param callable [#call] the subscriber to remove
62
+ # @return [Telemetry] self for chaining
63
+ # @raise [ArgumentError] when `event` is unknown
64
+ def unsubscribe(event, callable)
65
+ raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}" unless EVENTS.include?(event)
66
+
67
+ return self unless subscribed?(event)
68
+
69
+ registry[event].delete(callable)
70
+ registry.delete(event) if registry[event].empty?
71
+ self
72
+ end
73
+
74
+ # @param event [Symbol]
75
+ # @return [Boolean] true when at least one subscriber exists for `event`
76
+ def subscribed?(event)
77
+ registry.key?(event)
78
+ end
79
+
80
+ # @return [Boolean]
81
+ def empty?
82
+ registry.empty?
83
+ end
84
+
85
+ # @return [Integer] number of subscribed events
86
+ def size
87
+ registry.size
88
+ end
89
+
90
+ # @return [Integer] total subscribers across all events
91
+ def count
92
+ registry.each_value.sum(&:size)
93
+ end
94
+
95
+ # Dispatches `payload` to every subscriber of `event`. No-op when there
96
+ # are no subscribers.
97
+ #
98
+ # @param event [Symbol]
99
+ # @param payload [Event]
100
+ # @return [void]
101
+ def emit(event, payload)
102
+ return unless (subscribers = registry[event])
103
+
104
+ subscribers.each { |s| s.call(payload) }
105
+ end
106
+
107
+ end
108
+ end
data/lib/cmdx/util.rb ADDED
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CMDx
4
+ # Shared helpers for resolving `:if` / `:unless` conditional options across
5
+ # tasks, callbacks, inputs, outputs, validators, and deprecations. Normalizes
6
+ # booleans, symbols (method names), procs, and call-ables into a truth value.
7
+ module Util
8
+
9
+ extend self
10
+
11
+ # Evaluates a condition against `receiver`, dispatching by type.
12
+ #
13
+ # @param condition [Boolean, nil, Symbol, Proc, #call] `:if`/`:unless`-style gate, method name, or callable evaluated against `receiver`
14
+ # @param receiver [Object] object the condition runs against (usually a Task)
15
+ # @param args [Array<Object>] extra arguments forwarded to the condition
16
+ # @return [Boolean, Object] truthiness result (Procs `instance_exec` on receiver)
17
+ # @raise [ArgumentError] when the condition is not a supported type
18
+ def evaluate(condition, receiver, *args)
19
+ case condition
20
+ when FalseClass, NilClass
21
+ false
22
+ when TrueClass
23
+ true
24
+ when Symbol
25
+ receiver.send(condition, *args)
26
+ when Proc
27
+ receiver.instance_exec(*args, &condition)
28
+ else
29
+ return condition.call(receiver, *args) if condition.respond_to?(:call)
30
+
31
+ raise ArgumentError, "condition must be a Symbol, Proc, or respond to #call"
32
+ end
33
+ end
34
+
35
+ # Evaluates an `:if`-style condition. `nil` is treated as "always true".
36
+ #
37
+ # @param condition [Boolean, nil, Symbol, Proc, #call] gate to check
38
+ # @param receiver [Object] object the condition runs against
39
+ # @param args [Array<Object>] extra arguments forwarded to the condition
40
+ # @return [Boolean] true when `condition` is nil or evaluates truthy
41
+ def if?(condition, receiver, *args)
42
+ return true if condition.nil?
43
+
44
+ evaluate(condition, receiver, *args)
45
+ end
46
+
47
+ # Evaluates an `:unless`-style condition. `nil` is treated as "always true".
48
+ #
49
+ # @param condition [Boolean, nil, Symbol, Proc, #call] gate to check
50
+ # @param receiver [Object] object the condition runs against
51
+ # @param args [Array<Object>] extra arguments forwarded to the condition
52
+ # @return [Boolean] true when `condition` is nil or evaluates falsy
53
+ def unless?(condition, receiver, *args)
54
+ return true if condition.nil?
55
+
56
+ !evaluate(condition, receiver, *args)
57
+ end
58
+
59
+ # Combines `:if` and `:unless` gates. Used across the framework to decide
60
+ # whether a conditional feature (callback, retry, validator, etc.) should run.
61
+ #
62
+ # @param condition_if [Boolean, nil, Symbol, Proc, #call] `:if` gate
63
+ # @param condition_unless [Boolean, nil, Symbol, Proc, #call] `:unless` gate
64
+ # @param receiver [Object] object the conditions run against
65
+ # @param args [Array<Object>] extra arguments forwarded to both conditions
66
+ # @return [Boolean] true only when both gates pass
67
+ def satisfied?(condition_if, condition_unless, receiver, *args)
68
+ if?(condition_if, receiver, *args) &&
69
+ unless?(condition_unless, receiver, *args)
70
+ end
71
+
72
+ end
73
+ end
@@ -1,47 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Validators
5
- # Validates that a value is absent or empty
6
- #
7
- # This validator ensures that the given value is nil, empty, or consists only of whitespace.
8
- # It handles different value types appropriately:
9
- # - Strings: checks for absence of non-whitespace characters
10
- # - Collections: checks for empty collections
11
- # - Other objects: checks for nil values
4
+ class Validators
5
+ # Validates that a value is blank: `nil`, whitespace-only string, or
6
+ # empty collection.
12
7
  module Absence
13
8
 
14
9
  extend self
15
10
 
16
- # Validates that a value is absent or empty
17
- #
18
- # @param value [Object] The value to validate for absence
19
- # @param options [Hash] Validation configuration options
20
- # @option options [String] :message Custom error message
21
- #
22
- # @return [nil] Returns nil if validation passes
23
- #
24
- # @raise [ValidationError] When the value is present, not empty, or contains non-whitespace characters
25
- #
26
- # @example Validate string absence
27
- # Absence.call("")
28
- # # => nil (validation passes)
29
- # @example Validate non-empty string
30
- # Absence.call("hello")
31
- # # => raises ValidationError
32
- # @example Validate array absence
33
- # Absence.call([])
34
- # # => nil (validation passes)
35
- # @example Validate non-empty array
36
- # Absence.call([1, 2, 3])
37
- # # => raises ValidationError
38
- # @example Validate with custom message
39
- # Absence.call("hello", message: "Value must be empty")
40
- # # => raises ValidationError with custom message
41
- #
42
- # @rbs (untyped value, ?Hash[Symbol, untyped] options) -> nil
11
+ # @param value [Object]
12
+ # @param options [Hash{Symbol => Object}]
13
+ # @option options [String] :message override for the failure message
14
+ # @return [Validators::Failure, nil]
43
15
  def call(value, options = EMPTY_HASH)
44
- match =
16
+ present =
45
17
  if value.is_a?(String)
46
18
  /\S/.match?(value)
47
19
  elsif value.respond_to?(:empty?)
@@ -50,10 +22,9 @@ module CMDx
50
22
  !value.nil?
51
23
  end
52
24
 
53
- return unless match
25
+ return unless present
54
26
 
55
- message = options[:message] if options.is_a?(Hash)
56
- raise ValidationError, message || Locale.t("cmdx.validators.absence")
27
+ Failure.new(options[:message] || I18nProxy.t("cmdx.validators.absence"))
57
28
  end
58
29
 
59
30
  end
@@ -1,79 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Validators
5
- # Validates that a value is not included in a specified set or range
6
- #
7
- # This validator ensures that the given value is excluded from a collection
8
- # of forbidden values or falls outside a specified range. It supports both
9
- # discrete value lists and range-based exclusions.
4
+ class Validators
5
+ # Inverse of {Inclusion}: the value must not be within the given
6
+ # enumerable or `Range`.
10
7
  module Exclusion
11
8
 
12
9
  extend self
13
10
 
14
- # Validates that a value is excluded from the specified options
15
- #
16
- # @param value [Object] The value to validate for exclusion
17
- # @param options [Hash] Validation configuration options
18
- # @option options [Array, Range] :in The collection of forbidden values or range
19
- # @option options [Array, Range] :within Alias for :in option
20
- # @option options [String] :message Custom error message template
21
- # @option options [String] :of_message Custom message for discrete value exclusions
22
- # @option options [String] :in_message Custom message for range-based exclusions
23
- # @option options [String] :within_message Custom message for range-based exclusions
24
- #
25
- # @raise [ValidationError] When the value is found in the forbidden collection
26
- #
27
- # @example Exclude specific values
28
- # Exclusion.call("admin", in: ["admin", "root", "superuser"])
29
- # # => raises ValidationError if value is "admin"
30
- # @example Exclude values within a range
31
- # Exclusion.call(5, in: 1..10)
32
- # # => raises ValidationError if value is 5 (within 1..10)
33
- # @example Exclude with custom message
34
- # Exclusion.call("test", in: ["test", "demo"], message: "value %{values} is forbidden")
35
- #
36
- # @rbs (untyped value, Hash[Symbol, untyped] options) -> nil
11
+ # @param value [Object]
12
+ # @param options [Hash{Symbol => Object}]
13
+ # @option options [Range, Array, Set, Enumerable] :in disallowed values
14
+ # @option options [Range, Array, Set, Enumerable] :within alias for `:in`
15
+ # @option options [String] :message global failure-message override
16
+ # @option options [String] :of_message override for enumerable failures
17
+ # @option options [String] :in_message, :within_message overrides for range failures
18
+ # @return [Validators::Failure, nil]
19
+ # @raise [ArgumentError] when neither `:in` nor `:within` is given
37
20
  def call(value, options = EMPTY_HASH)
38
21
  values = options[:in] || options[:within]
22
+ raise ArgumentError, "exclusion validator requires :in or :within option" if values.nil?
39
23
 
40
24
  if values.is_a?(Range)
41
- raise_within_validation_error!(values.begin, values.end, options) if values.cover?(value)
42
- elsif Utils::Wrap.array(values).any? { |v| v === value }
43
- raise_of_validation_error!(values, options)
25
+ within_failure(values.begin, values.end, options) if values.cover?(value)
26
+ elsif Array(values).any? { |v| v === value }
27
+ of_failure(values, options)
44
28
  end
45
29
  end
46
30
 
47
31
  private
48
32
 
49
- # Raises validation error for discrete value exclusions
50
- #
51
- # @param values [Array] The forbidden values that caused the error
52
- # @param options [Hash] Validation options containing custom messages
53
- # @option options [Object] :* Any validation option key-value pairs
54
- #
55
- # @raise [ValidationError] With appropriate error message
56
- def raise_of_validation_error!(values, options)
57
- values = values.map(&:inspect).join(", ") unless values.nil?
33
+ # @param values [Enumerable] collection rendered into the failure message
34
+ # @param options [Hash{Symbol => Object}]
35
+ # @option options [String] :of_message
36
+ # @option options [String] :message
37
+ # @return [Validators::Failure]
38
+ def of_failure(values, options)
39
+ values = values.map(&:inspect).join(", ")
58
40
  message = options[:of_message] || options[:message]
59
41
  message %= { values: } unless message.nil?
60
42
 
61
- raise ValidationError, message || Locale.t("cmdx.validators.exclusion.of", values:)
43
+ Failure.new(message || I18nProxy.t("cmdx.validators.exclusion.of", values:))
62
44
  end
63
45
 
64
- # Raises validation error for range-based exclusions
65
- #
66
- # @param min [Object] The minimum value of the forbidden range
67
- # @param max [Object] The maximum value of the forbidden range
68
- # @param options [Hash] Validation options containing custom messages
69
- # @option options [Object] :* Any validation option key-value pairs
70
- #
71
- # @raise [ValidationError] With appropriate error message
72
- def raise_within_validation_error!(min, max, options)
46
+ # @param min [Object] range/exclusion lower bound
47
+ # @param max [Object] range/exclusion upper bound
48
+ # @param options [Hash{Symbol => Object}]
49
+ # @option options [String] :in_message
50
+ # @option options [String] :within_message
51
+ # @option options [String] :message
52
+ # @return [Validators::Failure]
53
+ def within_failure(min, max, options)
73
54
  message = options[:in_message] || options[:within_message] || options[:message]
74
55
  message %= { min:, max: } unless message.nil?
75
56
 
76
- raise ValidationError, message || Locale.t("cmdx.validators.exclusion.within", min:, max:)
57
+ Failure.new(message || I18nProxy.t("cmdx.validators.exclusion.within", min:, max:))
77
58
  end
78
59
 
79
60
  end
@@ -1,66 +1,36 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Validators
5
- # Validates that a value matches a specified format pattern
6
- #
7
- # This validator ensures that the given value conforms to a specific format
8
- # using regular expressions. It supports both direct regex matching and
9
- # conditional matching with inclusion/exclusion patterns.
4
+ class Validators
5
+ # Validates that a value matches a `:with` regex and/or does not match a
6
+ # `:without` regex. Both may be combined; at least one is required.
10
7
  module Format
11
8
 
12
9
  extend self
13
10
 
14
- # Validates that a value matches the specified format pattern
15
- #
16
- # @param value [Object] The value to validate for format compliance
17
- # @param options [Hash, Regexp] Validation configuration options or direct regex pattern
18
- # @option options [Regexp] :with Required pattern that the value must match
19
- # @option options [Regexp] :without Pattern that the value must not match
20
- # @option options [String] :message Custom error message
21
- #
22
- # @return [nil] Returns nil if validation passes
23
- #
24
- # @raise [ValidationError] When the value doesn't match the required format
25
- #
26
- # @example Direct regex validation
27
- # Format.call("user@example.com", /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
28
- # # => nil (validation passes)
29
- # @example Validate with required pattern
30
- # Format.call("ABC123", with: /\A[A-Z]{3}\d{3}\z/)
31
- # # => nil (validation passes)
32
- # @example Validate with exclusion pattern
33
- # Format.call("hello", without: /\d/)
34
- # # => nil (validation passes - no digits)
35
- # @example Validate with both patterns
36
- # Format.call("test123", with: /\A\w+\z/, without: /\A\d+\z/)
37
- # # => nil (validation passes - alphanumeric but not all digits)
38
- # @example Validate with custom message
39
- # Format.call("invalid", with: /\A\d+\z/, message: "Must contain only digits")
40
- # # => raises ValidationError with custom message
41
- #
42
- # @rbs (untyped value, (Hash[Symbol, untyped] | Regexp) options) -> nil
11
+ # @param value [String, nil]
12
+ # @param options [Hash{Symbol => Object}]
13
+ # @option options [Regexp] :with must match
14
+ # @option options [Regexp] :without must not match
15
+ # @option options [String] :message override for the failure message
16
+ # @return [Validators::Failure, nil]
17
+ # @raise [ArgumentError] when neither `:with` nor `:without` is given
43
18
  def call(value, options = EMPTY_HASH)
44
19
  match =
45
- if options.is_a?(Regexp)
46
- value&.match?(options)
20
+ case options
21
+ in with:, without:
22
+ value&.match?(with) && !value&.match?(without)
23
+ in with:
24
+ value&.match?(with)
25
+ in without:
26
+ !value&.match?(without)
47
27
  else
48
- case options
49
- in with:, without:
50
- value&.match?(with) && !value&.match?(without)
51
- in with:
52
- value&.match?(with)
53
- in without:
54
- !value&.match?(without)
55
- else
56
- false
57
- end
28
+ raise ArgumentError, "format validator requires :with and/or :without option"
58
29
  end
59
30
 
60
31
  return if match
61
32
 
62
- message = options[:message] if options.is_a?(Hash)
63
- raise ValidationError, message || Locale.t("cmdx.validators.format")
33
+ Failure.new(options[:message] || I18nProxy.t("cmdx.validators.format"))
64
34
  end
65
35
 
66
36
  end
@@ -1,81 +1,60 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CMDx
4
- module Validators
5
- # Validates that a value is included in a specified set or range
6
- #
7
- # This validator ensures that the given value is present within a collection
8
- # of allowed values or falls within a specified range. It supports both
9
- # discrete value lists and range-based validations.
4
+ class Validators
5
+ # Validates that a value is within an enumerable or `Range`. Range uses
6
+ # `#cover?`; other enumerables use `===` (so regex/class matchers work).
10
7
  module Inclusion
11
8
 
12
9
  extend self
13
10
 
14
- # Validates that a value is included in the specified options
15
- #
16
- # @param value [Object] The value to validate for inclusion
17
- # @param options [Hash] Validation configuration options
18
- # @option options [Array, Range] :in The collection of allowed values or range
19
- # @option options [Array, Range] :within Alias for :in option
20
- # @option options [String] :message Custom error message template
21
- # @option options [String] :of_message Custom message for discrete value inclusions
22
- # @option options [String] :in_message Custom message for range-based inclusions
23
- # @option options [String] :within_message Custom message for range-based inclusions
24
- #
25
- # @return [nil] Returns nil if validation passes
26
- #
27
- # @raise [ValidationError] When the value is not found in the allowed collection
28
- #
29
- # @example Include specific values
30
- # Inclusion.call("admin", in: ["admin", "user", "guest"])
31
- # # => nil (validation passes)
32
- # @example Include values within a range
33
- # Inclusion.call(5, in: 1..10)
34
- # # => nil (validation passes - 5 is within 1..10)
35
- # @example Include with custom message
36
- # Inclusion.call("test", in: ["admin", "user"], message: "must be one of: %{values}")
37
- #
38
- # @rbs (untyped value, Hash[Symbol, untyped] options) -> nil
11
+ # @param value [Object]
12
+ # @param options [Hash{Symbol => Object}]
13
+ # @option options [Range, Array, Set, Enumerable] :in allowed values
14
+ # @option options [Range, Array, Set, Enumerable] :within alias for `:in`
15
+ # @option options [String] :message global failure-message override
16
+ # @option options [String] :of_message override for enumerable failures
17
+ # @option options [String] :in_message, :within_message overrides for range failures
18
+ # @return [Validators::Failure, nil]
19
+ # @raise [ArgumentError] when neither `:in` nor `:within` is given
39
20
  def call(value, options = EMPTY_HASH)
40
21
  values = options[:in] || options[:within]
22
+ raise ArgumentError, "inclusion validator requires :in or :within option" if values.nil?
41
23
 
42
24
  if values.is_a?(Range)
43
- raise_within_validation_error!(values.begin, values.end, options) unless values.cover?(value)
44
- elsif Utils::Wrap.array(values).none? { |v| v === value }
45
- raise_of_validation_error!(values, options)
25
+ within_failure(values.begin, values.end, options) unless values.cover?(value)
26
+ elsif Array(values).none? { |v| v === value }
27
+ of_failure(values, options)
46
28
  end
47
29
  end
48
30
 
49
31
  private
50
32
 
51
- # Raises validation error for discrete value inclusions
52
- #
53
- # @param values [Array] The allowed values that caused the error
54
- # @param options [Hash] Validation options containing custom messages
55
- # @option options [Object] :* Any validation option key-value pairs
56
- #
57
- # @raise [ValidationError] With appropriate error message
58
- def raise_of_validation_error!(values, options)
59
- values = values.map(&:inspect).join(", ") unless values.nil?
33
+ # @param values [Enumerable] collection rendered into the failure message
34
+ # @param options [Hash{Symbol => Object}]
35
+ # @option options [String] :of_message
36
+ # @option options [String] :message
37
+ # @return [Validators::Failure]
38
+ def of_failure(values, options)
39
+ values = values.map(&:inspect).join(", ")
60
40
  message = options[:of_message] || options[:message]
61
41
  message %= { values: } unless message.nil?
62
42
 
63
- raise ValidationError, message || Locale.t("cmdx.validators.inclusion.of", values:)
43
+ Failure.new(message || I18nProxy.t("cmdx.validators.inclusion.of", values:))
64
44
  end
65
45
 
66
- # Raises validation error for range-based inclusions
67
- #
68
- # @param min [Object] The minimum value of the allowed range
69
- # @param max [Object] The maximum value of the allowed range
70
- # @param options [Hash] Validation options containing custom messages
71
- # @option options [Object] :* Any validation option key-value pairs
72
- #
73
- # @raise [ValidationError] With appropriate error message
74
- def raise_within_validation_error!(min, max, options)
46
+ # @param min [Object] range/inclusion lower bound
47
+ # @param max [Object] range/inclusion upper bound
48
+ # @param options [Hash{Symbol => Object}]
49
+ # @option options [String] :in_message
50
+ # @option options [String] :within_message
51
+ # @option options [String] :message
52
+ # @return [Validators::Failure]
53
+ def within_failure(min, max, options)
75
54
  message = options[:in_message] || options[:within_message] || options[:message]
76
55
  message %= { min:, max: } unless message.nil?
77
56
 
78
- raise ValidationError, message || Locale.t("cmdx.validators.inclusion.within", min:, max:)
57
+ Failure.new(message || I18nProxy.t("cmdx.validators.inclusion.within", min:, max:))
79
58
  end
80
59
 
81
60
  end