cmdx 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -8
  3. data/lib/cmdx/callbacks.rb +31 -11
  4. data/lib/cmdx/chain.rb +29 -10
  5. data/lib/cmdx/coercions/big_decimal.rb +1 -1
  6. data/lib/cmdx/coercions/boolean.rb +3 -9
  7. data/lib/cmdx/coercions/coerce.rb +4 -1
  8. data/lib/cmdx/coercions/date_time.rb +1 -1
  9. data/lib/cmdx/coercions/integer.rb +11 -2
  10. data/lib/cmdx/coercions/symbol.rb +23 -4
  11. data/lib/cmdx/coercions.rb +25 -10
  12. data/lib/cmdx/configuration.rb +31 -16
  13. data/lib/cmdx/context.rb +40 -56
  14. data/lib/cmdx/deprecation.rb +4 -7
  15. data/lib/cmdx/deprecators/error.rb +4 -1
  16. data/lib/cmdx/deprecators.rb +17 -8
  17. data/lib/cmdx/errors.rb +15 -11
  18. data/lib/cmdx/executors/fiber.rb +16 -4
  19. data/lib/cmdx/executors/thread.rb +18 -4
  20. data/lib/cmdx/executors.rb +22 -7
  21. data/lib/cmdx/fault.rb +15 -3
  22. data/lib/cmdx/i18n_proxy.rb +10 -6
  23. data/lib/cmdx/input.rb +23 -21
  24. data/lib/cmdx/inputs.rb +14 -26
  25. data/lib/cmdx/log_formatters/json.rb +8 -1
  26. data/lib/cmdx/log_formatters/logstash.rb +7 -1
  27. data/lib/cmdx/mergers.rb +22 -7
  28. data/lib/cmdx/middlewares.rb +40 -24
  29. data/lib/cmdx/output.rb +5 -2
  30. data/lib/cmdx/pipeline.rb +28 -11
  31. data/lib/cmdx/railtie.rb +1 -0
  32. data/lib/cmdx/result.rb +22 -6
  33. data/lib/cmdx/retriers/decorrelated_jitter.rb +10 -5
  34. data/lib/cmdx/retriers/exponential.rb +10 -2
  35. data/lib/cmdx/retriers/fibonacci.rb +29 -12
  36. data/lib/cmdx/retriers.rb +17 -8
  37. data/lib/cmdx/retry.rb +20 -13
  38. data/lib/cmdx/runtime.rb +22 -40
  39. data/lib/cmdx/settings.rb +9 -9
  40. data/lib/cmdx/signal.rb +1 -1
  41. data/lib/cmdx/task.rb +90 -45
  42. data/lib/cmdx/telemetry.rb +52 -11
  43. data/lib/cmdx/util.rb +50 -4
  44. data/lib/cmdx/validators/absence.rb +1 -1
  45. data/lib/cmdx/validators/exclusion.rb +15 -15
  46. data/lib/cmdx/validators/format.rb +12 -4
  47. data/lib/cmdx/validators/inclusion.rb +15 -15
  48. data/lib/cmdx/validators/length.rb +5 -49
  49. data/lib/cmdx/validators/numeric.rb +5 -49
  50. data/lib/cmdx/validators/presence.rb +1 -1
  51. data/lib/cmdx/validators/validate.rb +7 -1
  52. data/lib/cmdx/validators.rb +21 -9
  53. data/lib/cmdx/version.rb +1 -1
  54. data/lib/cmdx/workflow.rb +28 -14
  55. data/lib/cmdx.rb +24 -0
  56. data/lib/generators/cmdx/templates/install.rb +96 -33
  57. data/mkdocs.yml +2 -0
  58. metadata +1 -1
@@ -7,7 +7,21 @@ module CMDx
7
7
  class Telemetry
8
8
 
9
9
  # Immutable event payload passed to subscribers.
10
- Event = Data.define(:xid, :cid, :root, :type, :task, :tid, :name, :payload, :timestamp)
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
11
25
 
12
26
  # Lifecycle event names Runtime emits.
13
27
  EVENTS = %i[
@@ -43,11 +57,17 @@ module CMDx
43
57
  subscriber = callable || block
44
58
 
45
59
  if callable && block
46
- raise ArgumentError, "provide either a callable or a block, not both"
60
+ raise ArgumentError, "subscriber: provide either a callable or a block, not both"
47
61
  elsif !subscriber.respond_to?(:call)
48
- raise ArgumentError, "subscriber must respond to #call"
62
+ raise ArgumentError, <<~MSG.chomp
63
+ subscriber must respond to #call (got #{subscriber.class}).
64
+ See https://drexed.github.io/cmdx/configuration/#telemetry
65
+ MSG
49
66
  elsif !EVENTS.include?(event)
50
- raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}"
67
+ raise ArgumentError, <<~MSG.chomp
68
+ unknown telemetry event #{event.inspect}, must be one of #{EVENTS.inspect}.
69
+ See https://drexed.github.io/cmdx/configuration/#telemetry
70
+ MSG
51
71
  end
52
72
 
53
73
  (registry[event] ||= []) << subscriber
@@ -60,14 +80,20 @@ module CMDx
60
80
  # @param event [Symbol] one of {EVENTS}
61
81
  # @param callable [#call] the subscriber to remove
62
82
  # @return [Telemetry] self for chaining
63
- # @raise [ArgumentError] when `event` is unknown
83
+ # @raise [UnknownEntryError] when `event` is unknown
64
84
  def unsubscribe(event, callable)
65
- raise ArgumentError, "unknown event #{event.inspect}, must be one of #{EVENTS.join(', ')}" unless EVENTS.include?(event)
85
+ unless EVENTS.include?(event)
86
+ raise UnknownEntryError, <<~MSG.chomp
87
+ unknown telemetry event #{event.inspect}, must be one of #{EVENTS.inspect}.
88
+ See https://drexed.github.io/cmdx/configuration/#telemetry
89
+ MSG
90
+ end
66
91
 
67
- return self unless subscribed?(event)
92
+ if (subscribers = registry[event])
93
+ subscribers.delete(callable)
94
+ registry.delete(event) if subscribers.empty?
95
+ end
68
96
 
69
- registry[event].delete(callable)
70
- registry.delete(event) if registry[event].empty?
71
97
  self
72
98
  end
73
99
 
@@ -77,6 +103,18 @@ module CMDx
77
103
  registry.key?(event)
78
104
  end
79
105
 
106
+ # @param event [Symbol]
107
+ # @return [#call]
108
+ # @raise [UnknownEntryError] when `event` isn't registered
109
+ def lookup(event)
110
+ registry[event] || begin
111
+ raise UnknownEntryError, <<~MSG.chomp
112
+ unknown telemetry event #{event.inspect}; registered: #{registry.keys.inspect}.
113
+ See https://drexed.github.io/cmdx/configuration/#telemetry
114
+ MSG
115
+ end
116
+ end
117
+
80
118
  # @return [Boolean]
81
119
  def empty?
82
120
  registry.empty?
@@ -99,9 +137,12 @@ module CMDx
99
137
  # @param payload [Event]
100
138
  # @return [void]
101
139
  def emit(event, payload)
102
- return unless (subscribers = registry[event])
140
+ return if empty?
141
+
142
+ subscribers = lookup(event)
143
+ return if subscribers.nil? || subscribers.empty?
103
144
 
104
- subscribers.each { |s| s.call(payload) }
145
+ subscribers.each { |callable| callable.call(payload) }
105
146
  end
106
147
 
107
148
  end
data/lib/cmdx/util.rb CHANGED
@@ -1,9 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
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.
4
+ # Shared helpers for `:if` / `:unless` gates, recursive hash merge/dup, and
5
+ # related tree utilities used across tasks, context, and i18n.
7
6
  module Util
8
7
 
9
8
  extend self
@@ -28,7 +27,8 @@ module CMDx
28
27
  else
29
28
  return condition.call(receiver, *args) if condition.respond_to?(:call)
30
29
 
31
- raise ArgumentError, "condition must be a Symbol, Proc, or respond to #call"
30
+ raise ArgumentError,
31
+ "condition must be a Symbol, Proc, or respond to #call (got #{condition.class})"
32
32
  end
33
33
  end
34
34
 
@@ -69,5 +69,51 @@ module CMDx
69
69
  unless?(condition_unless, receiver, *args)
70
70
  end
71
71
 
72
+ # Recursively merges two Hash-like trees. When both values at a key are
73
+ # Hashes, they merge recursively; otherwise the right-hand value wins
74
+ # (last-write-wins). When either top-level operand is not a Hash, returns
75
+ # `rhs` unchanged — useful when folding unknown YAML roots.
76
+ #
77
+ # @param lhs [Object] left tree (typically a Hash)
78
+ # @param rhs [Object] right tree (typically a Hash)
79
+ # @return [Object] merged Hash or `rhs` when a Hash-only merge is impossible
80
+ def deep_merge(lhs, rhs)
81
+ return rhs unless lhs.is_a?(Hash) && rhs.is_a?(Hash)
82
+
83
+ lhs.merge(rhs) { |_key, l, r| deep_merge(l, r) }
84
+ end
85
+
86
+ # Returns a deep copy of `value`. Immutable scalars (`Numeric`, `Symbol`,
87
+ # booleans, `nil`) are returned as-is; `Hash` and `Array` are walked
88
+ # recursively; other objects use `#dup`, falling back to the original when
89
+ # `#dup` raises.
90
+ #
91
+ # @param value [Object]
92
+ # @return [Object]
93
+ def deep_dup(value)
94
+ case value
95
+ when Numeric, Symbol, TrueClass, FalseClass, NilClass
96
+ value
97
+ when Hash
98
+ value.each_with_object({}) { |(k, v), acc| acc[k] = deep_dup(v) }
99
+ when Array
100
+ value.map { |e| deep_dup(e) }
101
+ else
102
+ begin
103
+ value.dup
104
+ rescue StandardError
105
+ value
106
+ end
107
+ end
108
+ end
109
+
110
+ # Returns a string representation of `error` in the format `[Class] Message`.
111
+ #
112
+ # @param error [Exception]
113
+ # @return [String]
114
+ def to_error_s(error)
115
+ "[#{error.class}] #{error.message}"
116
+ end
117
+
72
118
  end
73
119
  end
@@ -19,7 +19,7 @@ module CMDx
19
19
  elsif value.respond_to?(:empty?)
20
20
  !value.empty?
21
21
  else
22
- !value.nil?
22
+ !!value
23
23
  end
24
24
 
25
25
  return unless present
@@ -19,22 +19,29 @@ module CMDx
19
19
  # @raise [ArgumentError] when neither `:in` nor `:within` is given
20
20
  def call(value, options = EMPTY_HASH)
21
21
  values = options[:in] || options[:within]
22
- raise ArgumentError, "exclusion validator requires :in or :within option" if values.nil?
22
+ if values.nil?
23
+ raise ArgumentError, <<~MSG.chomp
24
+ exclusion validator requires :in or :within (got #{options.keys.inspect}).
25
+ See https://drexed.github.io/cmdx/inputs/validations/#exclusion
26
+ MSG
27
+ elsif values.is_a?(Hash)
28
+ raise ArgumentError, <<~MSG.chomp
29
+ exclusion validator :in/:within does not accept a Hash; pass an Array,
30
+ Set, Range, or other Enumerable (e.g. `#{values.inspect}.keys`).
31
+ See https://drexed.github.io/cmdx/inputs/validations/#exclusion
32
+ MSG
33
+ end
23
34
 
24
35
  if values.is_a?(Range)
25
36
  within_failure(values.begin, values.end, options) if values.cover?(value)
26
- elsif Array(values).any? { |v| v === value }
27
- of_failure(values, options)
37
+ else
38
+ enum = values.is_a?(Enumerable) ? values : [values]
39
+ of_failure(enum, options) if enum.any? { |v| v === value }
28
40
  end
29
41
  end
30
42
 
31
43
  private
32
44
 
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
45
  def of_failure(values, options)
39
46
  values = values.map(&:inspect).join(", ")
40
47
  message = options[:of_message] || options[:message]
@@ -43,13 +50,6 @@ module CMDx
43
50
  Failure.new(message || I18nProxy.t("cmdx.validators.exclusion.of", values:))
44
51
  end
45
52
 
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
53
  def within_failure(min, max, options)
54
54
  message = options[:in_message] || options[:within_message] || options[:message]
55
55
  message %= { min:, max: } unless message.nil?
@@ -15,17 +15,25 @@ module CMDx
15
15
  # @option options [String] :message override for the failure message
16
16
  # @return [Validators::Failure, nil]
17
17
  # @raise [ArgumentError] when neither `:with` nor `:without` is given
18
+ # @note Non-String values that do not respond to `#match?` fail with the
19
+ # regular format failure rather than raise `NoMethodError`. Coerce inputs
20
+ # to String beforehand when format checks are required.
18
21
  def call(value, options = EMPTY_HASH)
22
+ str = value.nil? || value.respond_to?(:match?) ? value : value.to_s
23
+
19
24
  match =
20
25
  case options
21
26
  in with:, without:
22
- value&.match?(with) && !value&.match?(without)
27
+ str&.match?(with) && !str&.match?(without)
23
28
  in with:
24
- value&.match?(with)
29
+ str&.match?(with)
25
30
  in without:
26
- !value&.match?(without)
31
+ !str&.match?(without)
27
32
  else
28
- raise ArgumentError, "format validator requires :with and/or :without option"
33
+ raise ArgumentError, <<~MSG.chomp
34
+ format validator requires :with and/or :without (got #{options.keys.inspect}).
35
+ See https://drexed.github.io/cmdx/inputs/validations/#format
36
+ MSG
29
37
  end
30
38
 
31
39
  return if match
@@ -19,22 +19,29 @@ module CMDx
19
19
  # @raise [ArgumentError] when neither `:in` nor `:within` is given
20
20
  def call(value, options = EMPTY_HASH)
21
21
  values = options[:in] || options[:within]
22
- raise ArgumentError, "inclusion validator requires :in or :within option" if values.nil?
22
+ if values.nil?
23
+ raise ArgumentError, <<~MSG.chomp
24
+ inclusion validator requires :in or :within (got #{options.keys.inspect}).
25
+ See https://drexed.github.io/cmdx/inputs/validations/#inclusion
26
+ MSG
27
+ elsif values.is_a?(Hash)
28
+ raise ArgumentError, <<~MSG.chomp
29
+ inclusion validator :in/:within does not accept a Hash; pass an Array,
30
+ Set, Range, or other Enumerable (e.g. `#{values.inspect}.keys`).
31
+ See https://drexed.github.io/cmdx/inputs/validations/#inclusion
32
+ MSG
33
+ end
23
34
 
24
35
  if values.is_a?(Range)
25
36
  within_failure(values.begin, values.end, options) unless values.cover?(value)
26
- elsif Array(values).none? { |v| v === value }
27
- of_failure(values, options)
37
+ else
38
+ enum = values.is_a?(Enumerable) ? values : [values]
39
+ of_failure(enum, options) if enum.none? { |v| v === value }
28
40
  end
29
41
  end
30
42
 
31
43
  private
32
44
 
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
45
  def of_failure(values, options)
39
46
  values = values.map(&:inspect).join(", ")
40
47
  message = options[:of_message] || options[:message]
@@ -43,13 +50,6 @@ module CMDx
43
50
  Failure.new(message || I18nProxy.t("cmdx.validators.inclusion.of", values:))
44
51
  end
45
52
 
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
53
  def within_failure(min, max, options)
54
54
  message = options[:in_message] || options[:within_message] || options[:message]
55
55
  message %= { min:, max: } unless message.nil?
@@ -63,28 +63,21 @@ module CMDx
63
63
  in is_not:
64
64
  is_not_failure(is_not, options) if length == is_not
65
65
  else
66
- raise ArgumentError, "unknown length validator options given"
66
+ raise ArgumentError, <<~MSG.chomp
67
+ unknown length validator options #{options.keys.inspect};
68
+ expected one of [:within, :not_within, :in, :not_in, :min, :max, :gt, :lt, :is, :is_not] (aliases: :gte, :lte, :eq, :not_eq).
69
+ See https://drexed.github.io/cmdx/inputs/validations/#length
70
+ MSG
67
71
  end
68
72
  end
69
73
 
70
74
  private
71
75
 
72
- # @param options [Hash{Symbol => Object}]
73
- # @option options [String] :nil_message
74
- # @option options [String] :message
75
- # @return [Validators::Failure]
76
76
  def nil_failure(options)
77
77
  message = options[:nil_message] || options[:message]
78
78
  Failure.new(message || I18nProxy.t("cmdx.validators.length.nil_value"))
79
79
  end
80
80
 
81
- # @param min [Object]
82
- # @param max [Object]
83
- # @param options [Hash{Symbol => Object}]
84
- # @option options [String] :within_message
85
- # @option options [String] :in_message
86
- # @option options [String] :message
87
- # @return [Validators::Failure]
88
81
  def within_failure(min, max, options)
89
82
  message = options[:within_message] || options[:in_message] || options[:message]
90
83
  message %= { min:, max: } unless message.nil?
@@ -92,13 +85,6 @@ module CMDx
92
85
  Failure.new(message || I18nProxy.t("cmdx.validators.length.within", min:, max:))
93
86
  end
94
87
 
95
- # @param min [Object]
96
- # @param max [Object]
97
- # @param options [Hash{Symbol => Object}]
98
- # @option options [String] :not_within_message
99
- # @option options [String] :not_in_message
100
- # @option options [String] :message
101
- # @return [Validators::Failure]
102
88
  def not_within_failure(min, max, options)
103
89
  message = options[:not_within_message] || options[:not_in_message] || options[:message]
104
90
  message %= { min:, max: } unless message.nil?
@@ -106,11 +92,6 @@ module CMDx
106
92
  Failure.new(message || I18nProxy.t("cmdx.validators.length.not_within", min:, max:))
107
93
  end
108
94
 
109
- # @param min [Object]
110
- # @param options [Hash{Symbol => Object}]
111
- # @option options [String] :min_message
112
- # @option options [String] :message
113
- # @return [Validators::Failure]
114
95
  def min_failure(min, options)
115
96
  message = options[:min_message] || options[:message]
116
97
  message %= { min: } unless message.nil?
@@ -118,11 +99,6 @@ module CMDx
118
99
  Failure.new(message || I18nProxy.t("cmdx.validators.length.min", min:))
119
100
  end
120
101
 
121
- # @param max [Object]
122
- # @param options [Hash{Symbol => Object}]
123
- # @option options [String] :max_message
124
- # @option options [String] :message
125
- # @return [Validators::Failure]
126
102
  def max_failure(max, options)
127
103
  message = options[:max_message] || options[:message]
128
104
  message %= { max: } unless message.nil?
@@ -130,11 +106,6 @@ module CMDx
130
106
  Failure.new(message || I18nProxy.t("cmdx.validators.length.max", max:))
131
107
  end
132
108
 
133
- # @param gt [Object]
134
- # @param options [Hash{Symbol => Object}]
135
- # @option options [String] :gt_message
136
- # @option options [String] :message
137
- # @return [Validators::Failure]
138
109
  def gt_failure(gt, options)
139
110
  message = options[:gt_message] || options[:message]
140
111
  message %= { gt: } unless message.nil?
@@ -142,11 +113,6 @@ module CMDx
142
113
  Failure.new(message || I18nProxy.t("cmdx.validators.length.gt", gt:))
143
114
  end
144
115
 
145
- # @param lt [Object]
146
- # @param options [Hash{Symbol => Object}]
147
- # @option options [String] :lt_message
148
- # @option options [String] :message
149
- # @return [Validators::Failure]
150
116
  def lt_failure(lt, options)
151
117
  message = options[:lt_message] || options[:message]
152
118
  message %= { lt: } unless message.nil?
@@ -154,11 +120,6 @@ module CMDx
154
120
  Failure.new(message || I18nProxy.t("cmdx.validators.length.lt", lt:))
155
121
  end
156
122
 
157
- # @param is [Object]
158
- # @param options [Hash{Symbol => Object}]
159
- # @option options [String] :is_message
160
- # @option options [String] :message
161
- # @return [Validators::Failure]
162
123
  def is_failure(is, options) # rubocop:disable Naming/PredicatePrefix
163
124
  message = options[:is_message] || options[:message]
164
125
  message %= { is: } unless message.nil?
@@ -166,11 +127,6 @@ module CMDx
166
127
  Failure.new(message || I18nProxy.t("cmdx.validators.length.is", is:))
167
128
  end
168
129
 
169
- # @param is_not [Object]
170
- # @param options [Hash{Symbol => Object}]
171
- # @option options [String] :is_not_message
172
- # @option options [String] :message
173
- # @return [Validators::Failure]
174
130
  def is_not_failure(is_not, options) # rubocop:disable Naming/PredicatePrefix
175
131
  message = options[:is_not_message] || options[:message]
176
132
  message %= { is_not: } unless message.nil?
@@ -60,28 +60,21 @@ module CMDx
60
60
  in is_not:
61
61
  is_not_failure(is_not, options) if value == is_not
62
62
  else
63
- raise ArgumentError, "unknown numeric validator options given"
63
+ raise ArgumentError, <<~MSG.chomp
64
+ unknown numeric validator options #{options.keys.inspect};
65
+ expected one of [:within, :not_within, :in, :not_in, :min, :max, :gt, :lt, :is, :is_not] (aliases: :gte, :lte, :eq, :not_eq).
66
+ See https://drexed.github.io/cmdx/inputs/validations/#numeric
67
+ MSG
64
68
  end
65
69
  end
66
70
 
67
71
  private
68
72
 
69
- # @param options [Hash{Symbol => Object}]
70
- # @option options [String] :nil_message
71
- # @option options [String] :message
72
- # @return [Validators::Failure]
73
73
  def nil_failure(options)
74
74
  message = options[:nil_message] || options[:message]
75
75
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.nil_value"))
76
76
  end
77
77
 
78
- # @param min [Object]
79
- # @param max [Object]
80
- # @param options [Hash{Symbol => Object}]
81
- # @option options [String] :within_message
82
- # @option options [String] :in_message
83
- # @option options [String] :message
84
- # @return [Validators::Failure]
85
78
  def within_failure(min, max, options)
86
79
  message = options[:within_message] || options[:in_message] || options[:message]
87
80
  message %= { min:, max: } unless message.nil?
@@ -89,13 +82,6 @@ module CMDx
89
82
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.within", min:, max:))
90
83
  end
91
84
 
92
- # @param min [Object]
93
- # @param max [Object]
94
- # @param options [Hash{Symbol => Object}]
95
- # @option options [String] :not_within_message
96
- # @option options [String] :not_in_message
97
- # @option options [String] :message
98
- # @return [Validators::Failure]
99
85
  def not_within_failure(min, max, options)
100
86
  message = options[:not_within_message] || options[:not_in_message] || options[:message]
101
87
  message %= { min:, max: } unless message.nil?
@@ -103,11 +89,6 @@ module CMDx
103
89
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.not_within", min:, max:))
104
90
  end
105
91
 
106
- # @param min [Object]
107
- # @param options [Hash{Symbol => Object}]
108
- # @option options [String] :min_message
109
- # @option options [String] :message
110
- # @return [Validators::Failure]
111
92
  def min_failure(min, options)
112
93
  message = options[:min_message] || options[:message]
113
94
  message %= { min: } unless message.nil?
@@ -115,11 +96,6 @@ module CMDx
115
96
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.min", min:))
116
97
  end
117
98
 
118
- # @param max [Object]
119
- # @param options [Hash{Symbol => Object}]
120
- # @option options [String] :max_message
121
- # @option options [String] :message
122
- # @return [Validators::Failure]
123
99
  def max_failure(max, options)
124
100
  message = options[:max_message] || options[:message]
125
101
  message %= { max: } unless message.nil?
@@ -127,11 +103,6 @@ module CMDx
127
103
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.max", max:))
128
104
  end
129
105
 
130
- # @param gt [Object]
131
- # @param options [Hash{Symbol => Object}]
132
- # @option options [String] :gt_message
133
- # @option options [String] :message
134
- # @return [Validators::Failure]
135
106
  def gt_failure(gt, options)
136
107
  message = options[:gt_message] || options[:message]
137
108
  message %= { gt: } unless message.nil?
@@ -139,11 +110,6 @@ module CMDx
139
110
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.gt", gt:))
140
111
  end
141
112
 
142
- # @param lt [Object]
143
- # @param options [Hash{Symbol => Object}]
144
- # @option options [String] :lt_message
145
- # @option options [String] :message
146
- # @return [Validators::Failure]
147
113
  def lt_failure(lt, options)
148
114
  message = options[:lt_message] || options[:message]
149
115
  message %= { lt: } unless message.nil?
@@ -151,11 +117,6 @@ module CMDx
151
117
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.lt", lt:))
152
118
  end
153
119
 
154
- # @param is [Object]
155
- # @param options [Hash{Symbol => Object}]
156
- # @option options [String] :is_message
157
- # @option options [String] :message
158
- # @return [Validators::Failure]
159
120
  def is_failure(is, options) # rubocop:disable Naming/PredicatePrefix
160
121
  message = options[:is_message] || options[:message]
161
122
  message %= { is: } unless message.nil?
@@ -163,11 +124,6 @@ module CMDx
163
124
  Failure.new(message || I18nProxy.t("cmdx.validators.numeric.is", is:))
164
125
  end
165
126
 
166
- # @param is_not [Object]
167
- # @param options [Hash{Symbol => Object}]
168
- # @option options [String] :is_not_message
169
- # @option options [String] :message
170
- # @return [Validators::Failure]
171
127
  def is_not_failure(is_not, options) # rubocop:disable Naming/PredicatePrefix
172
128
  message = options[:is_not_message] || options[:message]
173
129
  message %= { is_not: } unless message.nil?
@@ -19,7 +19,7 @@ module CMDx
19
19
  elsif value.respond_to?(:empty?)
20
20
  !value.empty?
21
21
  else
22
- !value.nil?
22
+ !!value
23
23
  end
24
24
 
25
25
  return if present
@@ -13,6 +13,9 @@ module CMDx
13
13
  # @param handler [Symbol, Proc, #call]
14
14
  # @return [Validators::Failure, nil, Object] handler's return value
15
15
  # @raise [ArgumentError] when `handler` isn't a supported type
16
+ # @note Symbol handlers are dispatched via `send` so private helpers on
17
+ # the task are reachable. Handlers are baked into class definitions;
18
+ # never derive them from untrusted input.
16
19
  def call(task, value, handler)
17
20
  case handler
18
21
  when Symbol
@@ -22,7 +25,10 @@ module CMDx
22
25
  else
23
26
  return handler.call(value, task) if handler.respond_to?(:call)
24
27
 
25
- raise ArgumentError, "validate handler must be a Symbol, Proc, or respond to #call"
28
+ raise ArgumentError, <<~MSG.chomp
29
+ validate handler must be a Symbol, Proc, or respond to #call (got #{handler.class}).
30
+ See https://drexed.github.io/cmdx/inputs/validations/#inline-validate-callable
31
+ MSG
26
32
  end
27
33
  end
28
34
 
@@ -45,9 +45,12 @@ module CMDx
45
45
  validator = callable || block
46
46
 
47
47
  if callable && block
48
- raise ArgumentError, "provide either a callable or a block, not both"
48
+ raise ArgumentError, "validator: provide either a callable or a block, not both"
49
49
  elsif !validator.respond_to?(:call)
50
- raise ArgumentError, "validator must respond to #call"
50
+ raise ArgumentError, <<~MSG.chomp
51
+ validator must respond to #call (got #{validator.class}).
52
+ See https://drexed.github.io/cmdx/inputs/validations/#declarations
53
+ MSG
51
54
  end
52
55
 
53
56
  registry[name.to_sym] = validator
@@ -57,16 +60,25 @@ module CMDx
57
60
  # @param name [Symbol]
58
61
  # @return [Validators] self for chaining
59
62
  def deregister(name)
60
- registry.delete(name.to_sym)
63
+ registry.delete(name)
61
64
  self
62
65
  end
63
66
 
67
+ # @param name [Symbol]
68
+ # @return [Boolean] whether a validator is registered under `name`
69
+ def key?(name)
70
+ registry.key?(name)
71
+ end
72
+
64
73
  # @param name [Symbol]
65
74
  # @return [#call]
66
- # @raise [ArgumentError] when `name` isn't registered
75
+ # @raise [UnknownEntryError] when `name` isn't registered
67
76
  def lookup(name)
68
77
  registry[name] || begin
69
- raise ArgumentError, "unknown validator: #{name}"
78
+ raise UnknownEntryError, <<~MSG.chomp
79
+ unknown validator #{name.inspect}; registered: #{registry.keys.inspect}.
80
+ See https://drexed.github.io/cmdx/inputs/validations/#built-in-validators
81
+ MSG
70
82
  end
71
83
  end
72
84
 
@@ -137,9 +149,6 @@ module CMDx
137
149
 
138
150
  private
139
151
 
140
- # @param raw_options [Object] truthy flag, Hash, Array, Regexp, etc. from a declaration
141
- # @return [Hash{Symbol => Object}, nil] normalized rule options, or nil when disabled
142
- # @raise [ArgumentError] when `raw_options` has an unsupported shape
143
152
  def normalize_options(raw_options)
144
153
  case raw_options
145
154
  when FalseClass, NilClass
@@ -153,7 +162,10 @@ module CMDx
153
162
  when Regexp
154
163
  { with: raw_options }
155
164
  else
156
- raise ArgumentError, "unsupported validator option format: #{raw_options.inspect}"
165
+ raise ArgumentError, <<~MSG.chomp
166
+ unsupported validator option format #{raw_options.inspect}; expected Boolean, Hash, Array, or Regexp.
167
+ See https://drexed.github.io/cmdx/inputs/validations/#common-options
168
+ MSG
157
169
  end
158
170
  end
159
171
 
data/lib/cmdx/version.rb CHANGED
@@ -3,6 +3,6 @@
3
3
  module CMDx
4
4
 
5
5
  # Gem version. Bumped on release; mirrored in the gemspec.
6
- VERSION = "2.0.0"
6
+ VERSION = "2.1.0"
7
7
 
8
8
  end