Module: Clack::Validators

Defined in:
lib/clack/validators.rb

Overview

Built-in validators for common validation patterns. Use these with the validate: option on prompts.

Validation procs can perform any operation including slow I/O (database lookups, API calls, etc.) - they simply block until complete.

Examples:

Using built-in validators

Clack.text(message: "Name?", validate: Clack::Validators.required)
Clack.text(message: "Email?", validate: Clack::Validators.format(/@/, "Must be an email"))
Clack.password(message: "Password?", validate: Clack::Validators.min_length(8))

Shorthand shapes (resolved by Validators.resolve)

Clack.text(message: "Slug?", validate: /\A[a-z0-9-]+\z/)      # Regexp, "Invalid format"
Clack.text(message: "Email?", validate: :email)               # zero-argument built-in
Clack.text(message: "User?", validate: [:required, /\A\w+\z/]) # combined, first failure wins
Clack.text(message: "User?", validate: {required: "Name needed", /\A\w+\z/ => "Word chars only"})

Database validation (blocking I/O)

Clack.text(
  message: "Email?",
  validate: ->(email) {
    "Already taken" if User.exists?(email: email)
  }
)

Combining validators

Clack.text(
  message: "Username?",
  validate: Clack::Validators.combine(
    Clack::Validators.required("Username is required"),
    Clack::Validators.min_length(3, "Must be at least 3 characters"),
    Clack::Validators.max_length(20, "Must be at most 20 characters"),
    Clack::Validators.format(/\A[a-z0-9_]+\z/i, "Only letters, numbers, and underscores")
  )
)

Constant Summary collapse

SHORTCUTS =

Built-ins reachable by a bare Symbol (+validate: :email+) or as a Hash key with a custom message (+validate: "Bad email"+). Each takes only an optional message.

Shortcuts are typed against the prompt's value: :future_date and :past_date expect a Date (the date prompt); :path_exists, :directory_exists, and :file_exists_warning expect a path String (+text+ or path); :required, :email, :url, and :integer work on any string-ish value.

%i[
  required email url integer path_exists directory_exists
  future_date past_date file_exists_warning
].freeze

Class Method Summary collapse

Class Method Details

.as_warning(validator) ⇒ Proc

Convert any validator to return a warning instead of an error. Warnings allow the user to proceed with confirmation. The argument is normalized with resolve, so as_warning(:email) and +as_warning(/\A[a-z]+\z/)+ work.

Examples:

# Make max_length a warning instead of error
Clack.text(
  message: "Bio?",
  validate: Clack::Validators.as_warning(
    Clack::Validators.max_length(100, "Bio is quite long")
  )
)

Shorthand shapes

Clack::Validators.as_warning(:email)
Clack::Validators.as_warning(/\A[a-z]+\z/)

Parameters:

  • validator (Proc, Regexp, Symbol, Array, Hash)

    original validator

Returns:

  • (Proc)

    validator that returns Warning instead of String

Raises:

  • (ArgumentError)

    if validator is nil or false, or can't be resolved



276
277
278
279
280
281
282
283
284
285
286
# File 'lib/clack/validators.rb', line 276

def as_warning(validator)
  raise ArgumentError, "as_warning needs a validator, got #{validator.inspect}" unless validator

  resolved = resolve(validator)
  lambda do |value|
    result = resolved.call(value)
    next if result.nil?

    result.is_a?(Clack::Warning) ? result : Clack::Warning.new(result)
  end
end

.combine(*validators) ⇒ Proc

Combines multiple validators. Returns the first error or warning, or nil if all pass. Each argument is normalized with resolve, so Regexps, Symbols, nested Arrays, and Hashes are accepted alongside procs. nil or false entries are ignored.

Examples:

Clack::Validators.combine(:required, /\A\d+\z/, Clack::Validators.in_range(1..65535))

Parameters:

  • validators (Array<Proc, Regexp, Symbol, Array, Hash, nil, false>)

    validators to combine

Returns:

  • (Proc)

    combined validator proc

Raises:

  • (ArgumentError)

    if any entry can't be resolved



178
179
180
181
# File 'lib/clack/validators.rb', line 178

def combine(*validators)
  resolved = validators.map { |validator| resolve(validator) }.compact
  ->(value) { first_failing_validation(resolved, value) }
end

.date_range(min:, max:, message: nil) ⇒ Proc

Validates that the date is within a given range.

Parameters:

  • min (Date)

    Minimum date

  • max (Date)

    Maximum date

  • message (String, nil) (defaults to: nil)

    Custom error message

Returns:

  • (Proc)

    Validator proc



239
240
241
242
# File 'lib/clack/validators.rb', line 239

def date_range(min:, max:, message: nil)
  msg = message || "Date must be between #{min} and #{max}"
  ->(date) { msg unless (min..max).cover?(date) }
end

.directory_exists(message = "Directory does not exist") ⇒ Proc

Validates directory path exists.

Parameters:

  • message (String) (defaults to: "Directory does not exist")

    Error message

Returns:

  • (Proc)

    Validator proc



211
212
213
# File 'lib/clack/validators.rb', line 211

def directory_exists(message = "Directory does not exist")
  ->(value) { message unless File.directory?(value.to_s) }
end

.email(message = "Must be a valid email address") ⇒ Proc

Common email format validator.

Parameters:

  • message (String) (defaults to: "Must be a valid email address")

    Error message

Returns:

  • (Proc)

    Validator proc



187
188
189
# File 'lib/clack/validators.rb', line 187

def email(message = "Must be a valid email address")
  format(/\A[^@\s]+@[^@\s]+\.[^@\s]+\z/, message)
end

.file_exists_warning(message = "File already exists. Overwrite?") ⇒ Proc

Warning if file exists. Allows user to confirm overwrite.

Examples:

Clack.text(message: "Output file?", validate: Clack::Validators.file_exists_warning)

Parameters:

  • message (String) (defaults to: "File already exists. Overwrite?")

    Warning message

Returns:

  • (Proc)

    Validator proc returning Warning



251
252
253
# File 'lib/clack/validators.rb', line 251

def file_exists_warning(message = "File already exists. Overwrite?")
  ->(value) { Clack::Warning.new(message) if File.exist?(value.to_s) }
end

.format(pattern, message = "Invalid format") ⇒ Proc

Validates that input matches a regular expression.

Parameters:

  • pattern (Regexp)

    Pattern to match

  • message (String) (defaults to: "Invalid format")

    Error message if pattern doesn't match

Returns:

  • (Proc)

    Validator proc



132
133
134
# File 'lib/clack/validators.rb', line 132

def format(pattern, message = "Invalid format")
  ->(value) { message unless pattern.match?(value.to_s) }
end

.future_date(message = "Date must be in the future") ⇒ Proc

Validates that the date is strictly after today. Today itself is not considered "future" and will fail validation.

Parameters:

  • message (String) (defaults to: "Date must be in the future")

    Error message

Returns:

  • (Proc)

    Validator proc



220
221
222
# File 'lib/clack/validators.rb', line 220

def future_date(message = "Date must be in the future")
  ->(date) { message if date <= Date.today }
end

.in_range(range, message = nil) ⇒ Proc

Validates that input is within a numeric range. Note: Parses value as integer for comparison.

Parameters:

  • range (Range)

    Allowed range

  • message (String, nil) (defaults to: nil)

    Custom error message

Returns:

  • (Proc)

    Validator proc



160
161
162
163
164
165
166
# File 'lib/clack/validators.rb', line 160

def in_range(range, message = nil)
  msg = message || "Must be between #{range.first} and #{range.last}"
  lambda do |value|
    int_val = value.to_s.to_i
    msg unless range.cover?(int_val) && value.to_s.match?(/\A-?\d+\z/)
  end
end

.integer(message = "Must be a number") ⇒ Proc

Validates that input is a valid integer.

Parameters:

  • message (String) (defaults to: "Must be a number")

    Error message

Returns:

  • (Proc)

    Validator proc



150
151
152
# File 'lib/clack/validators.rb', line 150

def integer(message = "Must be a number")
  ->(value) { message unless value.to_s.match?(/\A-?\d+\z/) }
end

.max_length(length, message = nil) ⇒ Proc

Validates maximum length.

Parameters:

  • length (Integer)

    Maximum length

  • message (String, nil) (defaults to: nil)

    Custom error message

Returns:

  • (Proc)

    Validator proc



122
123
124
125
# File 'lib/clack/validators.rb', line 122

def max_length(length, message = nil)
  msg = message || "Must be at most #{length} characters"
  ->(value) { msg if value.to_s.length > length }
end

.min_length(length, message = nil) ⇒ Proc

Validates minimum length.

Parameters:

  • length (Integer)

    Minimum length

  • message (String, nil) (defaults to: nil)

    Custom error message

Returns:

  • (Proc)

    Validator proc



112
113
114
115
# File 'lib/clack/validators.rb', line 112

def min_length(length, message = nil)
  msg = message || "Must be at least #{length} characters"
  ->(value) { msg if value.to_s.length < length }
end

.one_of(allowed, message = nil) ⇒ Proc

Validates that input is in a list of allowed values.

Parameters:

  • allowed (Array)

    Allowed values

  • message (String, nil) (defaults to: nil)

    Custom error message

Returns:

  • (Proc)

    Validator proc



141
142
143
144
# File 'lib/clack/validators.rb', line 141

def one_of(allowed, message = nil)
  msg = message || "Must be one of: #{allowed.join(", ")}"
  ->(value) { msg unless allowed.include?(value) }
end

.past_date(message = "Date must be in the past") ⇒ Proc

Validates that the date is strictly before today. Today itself is not considered "past" and will fail validation.

Parameters:

  • message (String) (defaults to: "Date must be in the past")

    Error message

Returns:

  • (Proc)

    Validator proc



229
230
231
# File 'lib/clack/validators.rb', line 229

def past_date(message = "Date must be in the past")
  ->(date) { message if date >= Date.today }
end

.path_exists(message = "Path does not exist") ⇒ Proc

Validates file path exists.

Parameters:

  • message (String) (defaults to: "Path does not exist")

    Error message

Returns:

  • (Proc)

    Validator proc



203
204
205
# File 'lib/clack/validators.rb', line 203

def path_exists(message = "Path does not exist")
  ->(value) { message unless File.exist?(value.to_s) }
end

.required(message = "This field is required") ⇒ Proc

Validates that the input is not empty.

Parameters:

  • message (String) (defaults to: "This field is required")

    Custom error message

Returns:

  • (Proc)

    Validator proc



103
104
105
# File 'lib/clack/validators.rb', line 103

def required(message = "This field is required")
  ->(value) { message if value.to_s.strip.empty? }
end

.resolve(validator) ⇒ #call?

Normalize the validate: option into a callable, or nil.

Accepted shapes:

  • nil or false: no validation
  • Regexp: format with the default "Invalid format" message
  • Symbol: a zero-argument built-in listed in SHORTCUTS
  • Array: combine of each entry, resolved recursively; nil or false entries are dropped
  • Hash: Regexp or Symbol keys mapped to a custom message, checked in insertion order. A String message makes the entry an error; a Warning message makes it a soft check (wrapped with as_warning)
  • anything responding to #call: returned unchanged

Core::Prompt#initialize calls this, so a bad validator raises when the prompt is built rather than when the user presses Enter.

Examples:

Clack::Validators.resolve(/\A\d+\z/).call("abc")        # => "Invalid format"
Clack::Validators.resolve(:email).call("nope")          # => "Must be a valid email address"
Clack::Validators.resolve({required: "Name needed"}).call("")  # => "Name needed"
Clack::Validators.resolve("oops")                       # raises ArgumentError

Parameters:

  • validator (Regexp, Symbol, Array, Hash, #call, nil, false)

    the validate: option value to normalize

Returns:

  • (#call, nil)

    the validator, or nil when no validation was requested

Raises:

  • (ArgumentError)

    for unknown Symbols, built-ins that need arguments, malformed Hash entries, or any other unsupported type



84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/clack/validators.rb', line 84

def resolve(validator)
  case validator
  when nil, false then nil
  when Regexp then format(validator)
  when Symbol then resolve_symbol(validator)
  when Array then combine(*validator)
  when Hash then combine(*validator.map { |shape, message| resolve_with_message(shape, message) })
  else
    return validator if validator.respond_to?(:call)

    raise ArgumentError,
      "Validate must be a Regexp, Symbol, Array, Hash, or respond to #call, got #{validator.class}"
  end
end

.url(message = "Must be a valid URL") ⇒ Proc

Common URL format validator.

Parameters:

  • message (String) (defaults to: "Must be a valid URL")

    Error message

Returns:

  • (Proc)

    Validator proc



195
196
197
# File 'lib/clack/validators.rb', line 195

def url(message = "Must be a valid URL")
  format(%r{\Ahttps?://\S+\z}, message)
end