summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAbinoam P. Marques Jr <abinoam@gmail.com>2023-01-02 19:24:22 -0300
committerGitHub <noreply@github.com>2023-01-02 19:24:22 -0300
commit7755f1f3f3b80e4c1c48446ea8234eaf954e6538 (patch)
treee0398311e72a08fc2a92ca187674c2fa03115bb4
parentee18153d63db64242fdbf467a89774c9627d54d5 (diff)
parentaa89af1a5a61cc03e1bdac7913a507b1177832aa (diff)
downloadhighline-7755f1f3f3b80e4c1c48446ea8234eaf954e6538.tar.gz
Merge pull request #258 from abinoam/issue_246
Issue #246 - Add custom validation class option to `Question#validate`
-rw-r--r--Changelog.md6
-rw-r--r--README.md27
-rw-r--r--examples/custom_parser_custom_validator.rb39
-rw-r--r--highline.gemspec1
-rw-r--r--lib/highline/question.rb6
-rwxr-xr-xtest/test_highline.rb49
6 files changed, 126 insertions, 2 deletions
diff --git a/Changelog.md b/Changelog.md
index 9a86f8a..cafba85 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -3,7 +3,11 @@
Below is a complete listing of changes for each revision of HighLine.
### Unreleased 2.2.0.develop.1
-* I #233 Show Question#default hint for non String values (@abinoam)
+* PR #258 / I #246 - Add validation class support
+ * Make it dry-types compatible through the use of `#valid?`
+ * Solve the multiple answers in one line problem with a combination of
+ custom coercion (parser) and custom validation
+* PR #257 / I #233 - Show Question#default hint for non String values (@abinoam)
* Add Question#default_hint_show to allow disabling it.
### 2.1.0 / 2022-12-31
diff --git a/README.md b/README.md
index c1baf49..0d878cf 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,33 @@ end
cli.ask("Age? ", Integer) { |q| q.in = 0..105 }
cli.ask("Name? (last, first) ") { |q| q.validate = /\A\w+, ?\w+\Z/ }
+## Validation with custom class
+class ZeroToTwentyFourValidator
+ def self.valid?(answer)
+ (0..24).include? answer.to_i
+ end
+
+ def self.inspect
+ "(0..24) rule"
+ end
+end
+
+cli.ask("What hour of the day is it?: ", Integer) do |q|
+ q.validate = ZeroToTwentyFourValidator
+end
+
+## Validation with Dry::Types
+## `Dry::Types` provides a `valid?` method so it can be used effortlessly
+
+require 'dry-type'
+
+module Types
+ include Dry.Types
+end
+
+cli.ask("Type an integer:", Integer) do |q|
+ q.validate = Types::Coercible::Integer
+end
# Type conversion for answers:
diff --git a/examples/custom_parser_custom_validator.rb b/examples/custom_parser_custom_validator.rb
new file mode 100644
index 0000000..0744e7b
--- /dev/null
+++ b/examples/custom_parser_custom_validator.rb
@@ -0,0 +1,39 @@
+require 'highline'
+
+cli = HighLine.new
+
+# The parser
+class ArrayOfNumbersFromString
+ def self.parse(string)
+ string.scan(/\d+/).map(&:to_i)
+ end
+end
+
+# The validator
+class ArrayOfNumbersFromStringInRange
+ def self.in?(range)
+ new(range)
+ end
+
+ attr_reader :range
+
+ def initialize(range)
+ @range = range
+ end
+
+ def valid?(answer)
+ ary = ArrayOfNumbersFromString.parse(answer)
+ ary.all? ->(number) { range.include? number }
+ end
+
+ def inspect
+ "in range #@range validator"
+ end
+end
+
+answer = cli.ask("Which number? (0 or <Enter> to skip): ", ArrayOfNumbersFromString) { |q|
+ q.validate = ArrayOfNumbersFromStringInRange.in?(0..10)
+ q.default = 0
+}
+
+puts "Your answer was: #{answer} and it was correctly validated and coerced into an #{answer.class}"
diff --git a/highline.gemspec b/highline.gemspec
index 1c92ce0..e19f557 100644
--- a/highline.gemspec
+++ b/highline.gemspec
@@ -32,4 +32,5 @@ DESCRIPTION
spec.add_development_dependency "bundler"
spec.add_development_dependency "rake"
spec.add_development_dependency "minitest"
+ spec.add_development_dependency "dry-types"
end
diff --git a/lib/highline/question.rb b/lib/highline/question.rb
index ce2245d..8fcd68d 100644
--- a/lib/highline/question.rb
+++ b/lib/highline/question.rb
@@ -145,6 +145,9 @@ class HighLine
# If set to a Regexp, the answer must match (before type conversion).
# Can also be set to a Proc which will be called with the provided
# answer to validate with a +true+ or +false+ return.
+ # It's possible to use a custom validator class. It must respond to
+ # `#valid?`. The result of `#inspect` will be used in error messages.
+ # See README.md for details.
#
attr_accessor :validate
# Used to control range checks for answer.
@@ -502,7 +505,8 @@ class HighLine
def valid_answer?
!validate ||
(validate.is_a?(Regexp) && answer =~ validate) ||
- (validate.is_a?(Proc) && validate[answer])
+ (validate.is_a?(Proc) && validate[answer]) ||
+ (validate.respond_to?(:valid?) && validate.valid?(answer))
end
#
diff --git a/test/test_highline.rb b/test/test_highline.rb
index 13ff406..8de8551 100755
--- a/test/test_highline.rb
+++ b/test/test_highline.rb
@@ -1626,6 +1626,55 @@ class TestHighLine < Minitest::Test
assert_equal("Gray, James Edward", answer)
end
+ class ZeroToTwentyFourValidator
+ def self.valid?(answer)
+ (0..24).include? answer.to_i
+ end
+
+ def self.inspect
+ "(0..24) rule"
+ end
+ end
+
+ def test_validation_with_custom_validator_class
+ @input << "26\n25\n24\n"
+ @input.rewind
+
+ answer = @terminal.ask("What hour of the day is it?: ", Integer) do |q|
+ q.validate = ZeroToTwentyFourValidator
+ end
+
+ assert_equal(24, answer)
+ assert_equal("What hour of the day is it?: " \
+ "Your answer isn't valid (must match (0..24) rule).\n" \
+ "? Your answer isn't valid (must match (0..24) rule).\n" \
+ "? ", @output.string)
+ end
+
+ require 'dry/types'
+
+ module Types
+ include Dry.Types
+ end
+
+ def test_validation_with_dry_types
+ @input << "random string\nanother uncoercible string\n42\n"
+ @input.rewind
+
+ answer = @terminal.ask("Type a number: ", Integer) do |q|
+ q.validate = Types::Coercible::Integer
+ end
+
+ assert_equal(42, answer)
+ assert_match(Regexp.new(<<~REGEXP.chomp),
+ Type a number: Your answer isn't valid .must match .*Dry.*Types.*Integer.*..
+ \\\? Your answer isn't valid .must match .*Dry.*Types.*Integer.*..
+ \\\?
+ REGEXP
+ @output.string
+ )
+ end
+
def test_validation_with_overriding_static_message
@input << "Not valid answer\n" \
"42\n"