clack 0.6.2 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +281 -14
- data/examples/migration_from_tty_prompt.rb +13 -4
- data/lib/clack/box.rb +11 -5
- data/lib/clack/colors.rb +9 -4
- data/lib/clack/core/chrome.rb +139 -0
- data/lib/clack/core/ci_mode.rb +6 -3
- data/lib/clack/core/key_reader.rb +78 -18
- data/lib/clack/core/options_helper.rb +43 -6
- data/lib/clack/core/prompt.rb +47 -72
- data/lib/clack/core/settings.rb +87 -6
- data/lib/clack/environment.rb +28 -4
- data/lib/clack/errors.rb +19 -0
- data/lib/clack/log.rb +31 -17
- data/lib/clack/note.rb +12 -5
- data/lib/clack/prompts/autocomplete.rb +13 -12
- data/lib/clack/prompts/autocomplete_multiselect.rb +12 -21
- data/lib/clack/prompts/confirm.rb +30 -12
- data/lib/clack/prompts/date.rb +1 -1
- data/lib/clack/prompts/group_multiselect.rb +48 -32
- data/lib/clack/prompts/multiline_text.rb +6 -6
- data/lib/clack/prompts/multiselect.rb +12 -19
- data/lib/clack/prompts/password.rb +2 -2
- data/lib/clack/prompts/path.rb +14 -3
- data/lib/clack/prompts/range.rb +37 -4
- data/lib/clack/prompts/select.rb +8 -1
- data/lib/clack/prompts/select_key.rb +53 -30
- data/lib/clack/prompts/spinner.rb +198 -17
- data/lib/clack/prompts/tasks.rb +34 -11
- data/lib/clack/prompts/text.rb +2 -2
- data/lib/clack/stream.rb +38 -18
- data/lib/clack/symbols.rb +6 -1
- data/lib/clack/task_log.rb +24 -8
- data/lib/clack/utils.rb +18 -6
- data/lib/clack/validators.rb +136 -7
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +197 -31
- metadata +3 -1
data/lib/clack/validators.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Clack
|
|
|
12
12
|
# Clack.text(message: "Email?", validate: Clack::Validators.format(/@/, "Must be an email"))
|
|
13
13
|
# Clack.password(message: "Password?", validate: Clack::Validators.min_length(8))
|
|
14
14
|
#
|
|
15
|
+
# @example Shorthand shapes (resolved by {Validators.resolve})
|
|
16
|
+
# Clack.text(message: "Slug?", validate: /\A[a-z0-9-]+\z/) # Regexp, "Invalid format"
|
|
17
|
+
# Clack.text(message: "Email?", validate: :email) # zero-argument built-in
|
|
18
|
+
# Clack.text(message: "User?", validate: [:required, /\A\w+\z/]) # combined, first failure wins
|
|
19
|
+
# Clack.text(message: "User?", validate: {required: "Name needed", /\A\w+\z/ => "Word chars only"})
|
|
20
|
+
#
|
|
15
21
|
# @example Database validation (blocking I/O)
|
|
16
22
|
# Clack.text(
|
|
17
23
|
# message: "Email?",
|
|
@@ -32,7 +38,64 @@ module Clack
|
|
|
32
38
|
# )
|
|
33
39
|
#
|
|
34
40
|
module Validators
|
|
41
|
+
# Built-ins reachable by a bare Symbol (+validate: :email+) or as a Hash key with a
|
|
42
|
+
# custom message (+validate: {email: "Bad email"}+). Each takes only an optional message.
|
|
43
|
+
#
|
|
44
|
+
# Shortcuts are typed against the prompt's value: +:future_date+ and +:past_date+
|
|
45
|
+
# expect a Date (the +date+ prompt); +:path_exists+, +:directory_exists+, and
|
|
46
|
+
# +:file_exists_warning+ expect a path String (+text+ or +path+); +:required+,
|
|
47
|
+
# +:email+, +:url+, and +:integer+ work on any string-ish value.
|
|
48
|
+
SHORTCUTS = %i[
|
|
49
|
+
required email url integer path_exists directory_exists
|
|
50
|
+
future_date past_date file_exists_warning
|
|
51
|
+
].freeze
|
|
52
|
+
|
|
53
|
+
# Built-ins that need arguments, so a bare Symbol can't name them. Consulted only to
|
|
54
|
+
# raise a pointed "needs arguments" error instead of "Unknown validator".
|
|
55
|
+
PARAMETERIZED = %i[min_length max_length format one_of in_range date_range combine as_warning].freeze
|
|
56
|
+
private_constant :PARAMETERIZED
|
|
57
|
+
|
|
35
58
|
class << self
|
|
59
|
+
# Normalize the +validate:+ option into a callable, or nil.
|
|
60
|
+
#
|
|
61
|
+
# Accepted shapes:
|
|
62
|
+
# - +nil+ or +false+: no validation
|
|
63
|
+
# - +Regexp+: {format} with the default "Invalid format" message
|
|
64
|
+
# - +Symbol+: a zero-argument built-in listed in {SHORTCUTS}
|
|
65
|
+
# - +Array+: {combine} of each entry, resolved recursively; +nil+ or +false+ entries are dropped
|
|
66
|
+
# - +Hash+: +Regexp+ or +Symbol+ keys mapped to a custom message, checked in
|
|
67
|
+
# insertion order. A String message makes the entry an error; a {Clack::Warning}
|
|
68
|
+
# message makes it a soft check (wrapped with {as_warning})
|
|
69
|
+
# - anything responding to +#call+: returned unchanged
|
|
70
|
+
#
|
|
71
|
+
# {Core::Prompt#initialize} calls this, so a bad validator raises when the prompt
|
|
72
|
+
# is built rather than when the user presses Enter.
|
|
73
|
+
#
|
|
74
|
+
# @param validator [Regexp, Symbol, Array, Hash, #call, nil, false] the +validate:+ option value to normalize
|
|
75
|
+
# @return [#call, nil] the validator, or nil when no validation was requested
|
|
76
|
+
# @raise [ArgumentError] for unknown Symbols, built-ins that need arguments,
|
|
77
|
+
# malformed Hash entries, or any other unsupported type
|
|
78
|
+
#
|
|
79
|
+
# @example
|
|
80
|
+
# Clack::Validators.resolve(/\A\d+\z/).call("abc") # => "Invalid format"
|
|
81
|
+
# Clack::Validators.resolve(:email).call("nope") # => "Must be a valid email address"
|
|
82
|
+
# Clack::Validators.resolve({required: "Name needed"}).call("") # => "Name needed"
|
|
83
|
+
# Clack::Validators.resolve("oops") # raises ArgumentError
|
|
84
|
+
def resolve(validator)
|
|
85
|
+
case validator
|
|
86
|
+
when nil, false then nil
|
|
87
|
+
when Regexp then format(validator)
|
|
88
|
+
when Symbol then resolve_symbol(validator)
|
|
89
|
+
when Array then combine(*validator)
|
|
90
|
+
when Hash then combine(*validator.map { |shape, message| resolve_with_message(shape, message) })
|
|
91
|
+
else
|
|
92
|
+
return validator if validator.respond_to?(:call)
|
|
93
|
+
|
|
94
|
+
raise ArgumentError,
|
|
95
|
+
"Validate must be a Regexp, Symbol, Array, Hash, or respond to #call, got #{validator.class}"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
36
99
|
# Validates that the input is not empty.
|
|
37
100
|
#
|
|
38
101
|
# @param message [String] Custom error message
|
|
@@ -102,12 +165,19 @@ module Clack
|
|
|
102
165
|
end
|
|
103
166
|
end
|
|
104
167
|
|
|
105
|
-
# Combines multiple validators. Returns the first error
|
|
168
|
+
# Combines multiple validators. Returns the first error or warning, or nil if all pass.
|
|
169
|
+
# Each argument is normalized with {resolve}, so Regexps, Symbols, nested Arrays,
|
|
170
|
+
# and Hashes are accepted alongside procs. +nil+ or +false+ entries are ignored.
|
|
106
171
|
#
|
|
107
|
-
# @param validators [Array<Proc>]
|
|
108
|
-
# @return [Proc]
|
|
172
|
+
# @param validators [Array<Proc, Regexp, Symbol, Array, Hash, nil, false>] validators to combine
|
|
173
|
+
# @return [Proc] combined validator proc
|
|
174
|
+
# @raise [ArgumentError] if any entry can't be resolved
|
|
175
|
+
#
|
|
176
|
+
# @example
|
|
177
|
+
# Clack::Validators.combine(:required, /\A\d+\z/, Clack::Validators.in_range(1..65535))
|
|
109
178
|
def combine(*validators)
|
|
110
|
-
|
|
179
|
+
resolved = validators.map { |validator| resolve(validator) }.compact
|
|
180
|
+
->(value) { first_failing_validation(resolved, value) }
|
|
111
181
|
end
|
|
112
182
|
|
|
113
183
|
# Common email format validator.
|
|
@@ -184,9 +254,12 @@ module Clack
|
|
|
184
254
|
|
|
185
255
|
# Convert any validator to return a warning instead of an error.
|
|
186
256
|
# Warnings allow the user to proceed with confirmation.
|
|
257
|
+
# The argument is normalized with {resolve}, so +as_warning(:email)+ and
|
|
258
|
+
# +as_warning(/\A[a-z]+\z/)+ work.
|
|
187
259
|
#
|
|
188
|
-
# @param validator [Proc]
|
|
189
|
-
# @return [Proc]
|
|
260
|
+
# @param validator [Proc, Regexp, Symbol, Array, Hash] original validator
|
|
261
|
+
# @return [Proc] validator that returns Warning instead of String
|
|
262
|
+
# @raise [ArgumentError] if validator is nil or false, or can't be resolved
|
|
190
263
|
#
|
|
191
264
|
# @example
|
|
192
265
|
# # Make max_length a warning instead of error
|
|
@@ -196,9 +269,16 @@ module Clack
|
|
|
196
269
|
# Clack::Validators.max_length(100, "Bio is quite long")
|
|
197
270
|
# )
|
|
198
271
|
# )
|
|
272
|
+
#
|
|
273
|
+
# @example Shorthand shapes
|
|
274
|
+
# Clack::Validators.as_warning(:email)
|
|
275
|
+
# Clack::Validators.as_warning(/\A[a-z]+\z/)
|
|
199
276
|
def as_warning(validator)
|
|
277
|
+
raise ArgumentError, "as_warning needs a validator, got #{validator.inspect}" unless validator
|
|
278
|
+
|
|
279
|
+
resolved = resolve(validator)
|
|
200
280
|
lambda do |value|
|
|
201
|
-
result =
|
|
281
|
+
result = resolved.call(value)
|
|
202
282
|
next if result.nil?
|
|
203
283
|
|
|
204
284
|
result.is_a?(Clack::Warning) ? result : Clack::Warning.new(result)
|
|
@@ -214,6 +294,55 @@ module Clack
|
|
|
214
294
|
end
|
|
215
295
|
nil
|
|
216
296
|
end
|
|
297
|
+
|
|
298
|
+
# Only names on the SHORTCUTS allowlist reach public_send (never respond_to?),
|
|
299
|
+
# so :resolve or :to_s can't be smuggled in as a validator.
|
|
300
|
+
def resolve_symbol(name, *args)
|
|
301
|
+
return public_send(name, *args) if SHORTCUTS.include?(name)
|
|
302
|
+
|
|
303
|
+
if PARAMETERIZED.include?(name)
|
|
304
|
+
raise ArgumentError,
|
|
305
|
+
"Validator #{name.inspect} needs arguments; pass Clack::Validators.#{name}(...) instead"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
raise ArgumentError,
|
|
309
|
+
"Unknown validator: #{name.inspect}. Available: #{SHORTCUTS.map(&:inspect).join(", ")}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# A Hash entry. A Warning message is unwrapped to its String, the built-in is
|
|
313
|
+
# constructed with that String, and the result is wrapped with as_warning. Passing
|
|
314
|
+
# the Warning object itself as the message would nest a Warning inside a Warning
|
|
315
|
+
# for built-ins that already return one (file_exists_warning).
|
|
316
|
+
def resolve_with_message(shape, message)
|
|
317
|
+
case message
|
|
318
|
+
when String then build_with_message(shape, message)
|
|
319
|
+
when Clack::Warning then as_warning(build_with_message(shape, warning_text(message)))
|
|
320
|
+
else
|
|
321
|
+
raise ArgumentError,
|
|
322
|
+
"Validate Hash values must be a String or Clack::Warning message, got #{message.class}"
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# The String carried by a Warning Hash value. Clack.warning always takes a String,
|
|
327
|
+
# but Clack::Warning.new(nil) is constructible and would otherwise hand a nil message
|
|
328
|
+
# to the built-in, silently producing a check that always passes.
|
|
329
|
+
def warning_text(warning)
|
|
330
|
+
text = warning.message
|
|
331
|
+
return text if text.is_a?(String)
|
|
332
|
+
|
|
333
|
+
raise ArgumentError,
|
|
334
|
+
"Validate Hash values must be a String or Clack::Warning message, got Clack::Warning(#{text.class})"
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def build_with_message(shape, message)
|
|
338
|
+
case shape
|
|
339
|
+
when Regexp then format(shape, message)
|
|
340
|
+
when Symbol then resolve_symbol(shape, message)
|
|
341
|
+
else
|
|
342
|
+
raise ArgumentError,
|
|
343
|
+
"Validate Hash keys must be a Regexp or a validator Symbol, got #{shape.class}"
|
|
344
|
+
end
|
|
345
|
+
end
|
|
217
346
|
end
|
|
218
347
|
end
|
|
219
348
|
end
|
data/lib/clack/version.rb
CHANGED