cmdx 1.21.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +127 -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 -237
  23. data/lib/cmdx/context.rb +266 -245
  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 +148 -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 +145 -130
  51. data/lib/cmdx/railtie.rb +10 -36
  52. data/lib/cmdx/result.rb +247 -524
  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 +232 -0
  63. data/lib/cmdx/settings.rb +68 -200
  64. data/lib/cmdx/signal.rb +165 -0
  65. data/lib/cmdx/task.rb +443 -343
  66. data/lib/cmdx/telemetry.rb +122 -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 +71 -96
  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 +143 -49
  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 +8 -7
  87. data/mkdocs.yml +26 -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 -378
  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 -77
  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 -55
  112. data/lib/locales/ar.yml +0 -55
  113. data/lib/locales/az.yml +0 -55
  114. data/lib/locales/be.yml +0 -55
  115. data/lib/locales/bg.yml +0 -55
  116. data/lib/locales/bn.yml +0 -55
  117. data/lib/locales/bs.yml +0 -55
  118. data/lib/locales/ca.yml +0 -55
  119. data/lib/locales/cnr.yml +0 -55
  120. data/lib/locales/cs.yml +0 -55
  121. data/lib/locales/cy.yml +0 -55
  122. data/lib/locales/da.yml +0 -55
  123. data/lib/locales/de.yml +0 -55
  124. data/lib/locales/dz.yml +0 -55
  125. data/lib/locales/el.yml +0 -55
  126. data/lib/locales/eo.yml +0 -55
  127. data/lib/locales/es.yml +0 -55
  128. data/lib/locales/et.yml +0 -55
  129. data/lib/locales/eu.yml +0 -55
  130. data/lib/locales/fa.yml +0 -55
  131. data/lib/locales/fi.yml +0 -55
  132. data/lib/locales/fr.yml +0 -55
  133. data/lib/locales/fy.yml +0 -55
  134. data/lib/locales/gd.yml +0 -55
  135. data/lib/locales/gl.yml +0 -55
  136. data/lib/locales/he.yml +0 -55
  137. data/lib/locales/hi.yml +0 -55
  138. data/lib/locales/hr.yml +0 -55
  139. data/lib/locales/hu.yml +0 -55
  140. data/lib/locales/hy.yml +0 -55
  141. data/lib/locales/id.yml +0 -55
  142. data/lib/locales/is.yml +0 -55
  143. data/lib/locales/it.yml +0 -55
  144. data/lib/locales/ja.yml +0 -55
  145. data/lib/locales/ka.yml +0 -55
  146. data/lib/locales/kk.yml +0 -55
  147. data/lib/locales/km.yml +0 -55
  148. data/lib/locales/kn.yml +0 -55
  149. data/lib/locales/ko.yml +0 -55
  150. data/lib/locales/lb.yml +0 -55
  151. data/lib/locales/lo.yml +0 -55
  152. data/lib/locales/lt.yml +0 -55
  153. data/lib/locales/lv.yml +0 -55
  154. data/lib/locales/mg.yml +0 -55
  155. data/lib/locales/mk.yml +0 -55
  156. data/lib/locales/ml.yml +0 -55
  157. data/lib/locales/mn.yml +0 -55
  158. data/lib/locales/mr-IN.yml +0 -55
  159. data/lib/locales/ms.yml +0 -55
  160. data/lib/locales/nb.yml +0 -55
  161. data/lib/locales/ne.yml +0 -55
  162. data/lib/locales/nl.yml +0 -55
  163. data/lib/locales/nn.yml +0 -55
  164. data/lib/locales/oc.yml +0 -55
  165. data/lib/locales/or.yml +0 -55
  166. data/lib/locales/pa.yml +0 -55
  167. data/lib/locales/pl.yml +0 -55
  168. data/lib/locales/pt.yml +0 -55
  169. data/lib/locales/rm.yml +0 -55
  170. data/lib/locales/ro.yml +0 -55
  171. data/lib/locales/ru.yml +0 -55
  172. data/lib/locales/sc.yml +0 -55
  173. data/lib/locales/sk.yml +0 -55
  174. data/lib/locales/sl.yml +0 -55
  175. data/lib/locales/sq.yml +0 -55
  176. data/lib/locales/sr.yml +0 -55
  177. data/lib/locales/st.yml +0 -55
  178. data/lib/locales/sv.yml +0 -55
  179. data/lib/locales/sw.yml +0 -55
  180. data/lib/locales/ta.yml +0 -55
  181. data/lib/locales/te.yml +0 -55
  182. data/lib/locales/th.yml +0 -55
  183. data/lib/locales/tl.yml +0 -55
  184. data/lib/locales/tr.yml +0 -55
  185. data/lib/locales/tt.yml +0 -55
  186. data/lib/locales/ug.yml +0 -55
  187. data/lib/locales/uk.yml +0 -55
  188. data/lib/locales/ur.yml +0 -55
  189. data/lib/locales/uz.yml +0 -55
  190. data/lib/locales/vi.yml +0 -55
  191. data/lib/locales/wo.yml +0 -55
  192. data/lib/locales/zh-CN.yml +0 -55
  193. data/lib/locales/zh-HK.yml +0 -55
  194. data/lib/locales/zh-TW.yml +0 -55
  195. data/lib/locales/zh-YUE.yml +0 -55
@@ -0,0 +1,122 @@
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) do
11
+ def self.build(task, name, root: false, payload: EMPTY_HASH)
12
+ new(
13
+ xid: Chain.current.xid,
14
+ cid: Chain.current.id,
15
+ root:,
16
+ type: task.class.type,
17
+ task: task.class,
18
+ tid: task.tid,
19
+ name:,
20
+ payload:,
21
+ timestamp: Time.now.utc
22
+ )
23
+ end
24
+ end
25
+
26
+ # Lifecycle event names Runtime emits.
27
+ EVENTS = %i[
28
+ task_started
29
+ task_deprecated
30
+ task_retried
31
+ task_rolled_back
32
+ task_executed
33
+ ].freeze
34
+
35
+ attr_reader :registry
36
+
37
+ def initialize
38
+ @registry = {}
39
+ end
40
+
41
+ # @param source [Telemetry] registry to duplicate
42
+ # @return [void]
43
+ def initialize_copy(source)
44
+ @registry = source.registry.transform_values(&:dup)
45
+ end
46
+
47
+ # Registers a subscriber for `event`.
48
+ #
49
+ # @param event [Symbol] one of {EVENTS}
50
+ # @param callable [#call, nil] subscriber callable; pass either this or a block
51
+ # @param block [#call, nil] subscriber when `callable` is omitted
52
+ # @yieldparam evt [Event]
53
+ # @return [Telemetry] self for chaining
54
+ # @raise [ArgumentError] when both `callable` and a block are provided, when
55
+ # the subscriber isn't callable, or when `event` is unknown
56
+ def subscribe(event, callable = nil, &block)
57
+ subscriber = callable || block
58
+
59
+ if callable && block
60
+ raise ArgumentError, "provide either a callable or a block, not both"
61
+ elsif !subscriber.respond_to?(:call)
62
+ raise ArgumentError, "subscriber must respond to #call"
63
+ elsif !EVENTS.include?(event)
64
+ raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}"
65
+ end
66
+
67
+ (registry[event] ||= []) << subscriber
68
+ self
69
+ end
70
+
71
+ # Removes a previously registered subscriber. Drops the event entry
72
+ # entirely when no subscribers remain.
73
+ #
74
+ # @param event [Symbol] one of {EVENTS}
75
+ # @param callable [#call] the subscriber to remove
76
+ # @return [Telemetry] self for chaining
77
+ # @raise [ArgumentError] when `event` is unknown
78
+ def unsubscribe(event, callable)
79
+ raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}" unless EVENTS.include?(event)
80
+
81
+ return self unless subscribed?(event)
82
+
83
+ registry[event].delete(callable)
84
+ registry.delete(event) if registry[event].empty?
85
+ self
86
+ end
87
+
88
+ # @param event [Symbol]
89
+ # @return [Boolean] true when at least one subscriber exists for `event`
90
+ def subscribed?(event)
91
+ registry.key?(event)
92
+ end
93
+
94
+ # @return [Boolean]
95
+ def empty?
96
+ registry.empty?
97
+ end
98
+
99
+ # @return [Integer] number of subscribed events
100
+ def size
101
+ registry.size
102
+ end
103
+
104
+ # @return [Integer] total subscribers across all events
105
+ def count
106
+ registry.each_value.sum(&:size)
107
+ end
108
+
109
+ # Dispatches `payload` to every subscriber of `event`. No-op when there
110
+ # are no subscribers.
111
+ #
112
+ # @param event [Symbol]
113
+ # @param payload [Event]
114
+ # @return [void]
115
+ def emit(event, payload)
116
+ return unless (subscribers = registry[event])
117
+
118
+ subscribers.each { |s| s.call(payload) }
119
+ end
120
+
121
+ end
122
+ 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