Skip to content

Commit

Permalink
[terminal/input] Add input validation feature
Browse files Browse the repository at this point in the history
  • Loading branch information
andyone committed Oct 30, 2024
1 parent d95ada9 commit 9809e89
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 27 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
## Changelog

### [13.9.3](https://kaos.sh/ek/13.9.3)
### [13.10.0](https://kaos.sh/ek/13.10.0)

> [!CAUTION]
> This release contains breaking changes to the `input.Read`, `input.ReadPassword`, and `input.ReadPasswordSecure` methods. Prior to this release, all of these methods took a boolean argument to disallow empty input. Since we are adding input validators, you will need to use the `NotEmpty` validator for the same behaviour.
- `[fmtutil/table]` Added automatic breaks feature
- `[terminal/input]` Added input validation feature

### [13.9.2](https://kaos.sh/ek/13.9.2)

Expand Down
30 changes: 20 additions & 10 deletions terminal/input/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,35 @@ import (

func ExampleRead() {
// User must enter name
input, err := Read("Please enter user name", true)
userName, err := Read("Please enter user name", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("User name: %s\n", input)
fmt.Printf("User name: %s\n", userName)

// You can read user input without providing any title
fmt.Println("Please enter user name")
input, err = Read("", true)
userName, err = Read("")

if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("User name: %s\n", input)
fmt.Printf("User name: %s\n", userName)

// You can define many validators at once
userEmail, err := Read("Please enter user email", NotEmpty, IsEmail)

if err != nil {
fmt.Printf("Error: %v\n", err)
return
}

fmt.Printf("User email: %s\n", userEmail)
}

func ExampleReadPassword() {
Expand All @@ -44,7 +54,7 @@ func ExampleReadPassword() {
NewLine = true

// User must enter the password
password, err := ReadPassword("Please enter password", true)
password, err := ReadPassword("Please enter password", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand All @@ -60,7 +70,7 @@ func ExampleReadPasswordSecure() {
MaskSymbolColorTag = "{s}"

// User must enter the password
password, err := ReadPasswordSecure("Please enter password", true)
password, err := ReadPasswordSecure("Please enter password", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand All @@ -87,7 +97,7 @@ func ExampleReadAnswer() {
}

func ExampleAddHistory() {
input, err := Read("Please enter user name", true)
input, err := Read("Please enter user name", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand All @@ -101,7 +111,7 @@ func ExampleAddHistory() {
}

func ExampleSetHistoryCapacity() {
input, err := Read("Please enter user name", true)
input, err := Read("Please enter user name", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand Down Expand Up @@ -142,7 +152,7 @@ func ExampleSetCompletionHandler() {
return ""
})

input, err := Read("Please enter command", true)
input, err := Read("Please enter command", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand Down Expand Up @@ -177,7 +187,7 @@ func ExampleSetHintHandler() {
return ""
})

input, err := Read("Please enter command", true)
input, err := Read("Please enter command", NotEmpty)

if err != nil {
fmt.Printf("Error: %v\n", err)
Expand Down
165 changes: 153 additions & 12 deletions terminal/input/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package input
import (
"fmt"
"os"
"strconv"
"strings"
"unicode/utf8"

Expand All @@ -35,6 +36,9 @@ type CompletionHandler = func(input string) []string
// HintHandler is hint handler
type HintHandler = func(input string) string

// Validator is input validation function
type Validator = func(input string) (string, error)

// ////////////////////////////////////////////////////////////////////////////////// //

// ErrKillSignal is error type when user cancel input
Expand Down Expand Up @@ -71,13 +75,54 @@ var NewLine = false

// ////////////////////////////////////////////////////////////////////////////////// //

var (
// NotEmpty returns an error if input is empty
NotEmpty = validatorNotEmpty

// IsNumber returns an error if the input is not a valid number
IsNumber = validatorIsNumber

// IsFloat returns an error if the input is not a valid floating number
IsFloat = validatorIsFloat

// IsEmail returns an error if the input is not a valid email
IsEmail = validatorIsEmail

// IsURL returns an error if the input is not a valid URL
IsURL = validatorIsURL
)

// ////////////////////////////////////////////////////////////////////////////////// //

var (
// ErrInvalidAnswer is error for wrong answer for Y/N question
ErrInvalidAnswer = fmt.Errorf("Please enter Y or N")

// ErrIsEmpty is error for empty input
ErrIsEmpty = fmt.Errorf("You must enter non-empty value")

// ErrInvalidNumber is error for invalid number
ErrInvalidNumber = fmt.Errorf("Entered value is not a valid number")

// ErrInvalidFloat is error for invalid floating number
ErrInvalidFloat = fmt.Errorf("Entered value is not a valid floating number")

// ErrInvalidEmail is error for invalid email
ErrInvalidEmail = fmt.Errorf("Entered value is not a valid e-mail")

// ErrInvalidURL is error for invalid URL
ErrInvalidURL = fmt.Errorf("Entered value is not a valid URL")
)

// ////////////////////////////////////////////////////////////////////////////////// //

var oldTMUXFlag int8

// ////////////////////////////////////////////////////////////////////////////////// //

// Read reads user input
func Read(title string, nonEmpty bool) (string, error) {
return readUserInput(title, nonEmpty, false)
func Read(title string, validators ...Validator) (string, error) {
return readUserInput(title, false, validators)
}

// ReadAnswer reads user's answer to yes/no question
Expand All @@ -104,7 +149,7 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) {

for {
answer, err := readUserInput(
getAnswerTitle(title, defaultAnswer), false, false,
getAnswerTitle(title, defaultAnswer), false, nil,
)

if err != nil {
Expand All @@ -121,22 +166,22 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) {
case "N":
return false, nil
default:
terminal.Warn("Please enter Y or N")
terminal.Warn(ErrInvalidAnswer)
fmtc.NewLine()
}
}
}

// ReadPassword reads password or some private input that will be hidden
// after pressing Enter
func ReadPassword(title string, nonEmpty bool) (string, error) {
return readUserInput(title, nonEmpty, true)
func ReadPassword(title string, validators ...Validator) (string, error) {
return readUserInput(title, true, validators)
}

// ReadPasswordSecure reads password or some private input that will be hidden
// after pressing Enter
func ReadPasswordSecure(title string, nonEmpty bool) (*secstr.String, error) {
password, err := readUserInput(title, nonEmpty, true)
func ReadPasswordSecure(title string, validators ...Validator) (*secstr.String, error) {
password, err := readUserInput(title, true, validators)

if err != nil {
return nil, err
Expand Down Expand Up @@ -167,6 +212,93 @@ func SetHintHandler(h HintHandler) {

// ////////////////////////////////////////////////////////////////////////////////// //

// validatorNotEmpty is validator for empty input
func validatorNotEmpty(input string) (string, error) {
if strings.TrimSpace(input) != "" {
return input, nil
}

return input, ErrIsEmpty
}

// validatorIsNumber is validator for number
func validatorIsNumber(input string) (string, error) {
input = strings.TrimSpace(input)

if input == "" {
return input, nil // Empty imput is okay
}

_, err := strconv.ParseInt(input, 10, 64)

if err != nil {
return input, ErrInvalidNumber
}

return input, nil
}

// validatorIsFloat is validator for floating number
func validatorIsFloat(input string) (string, error) {
input = strings.TrimSpace(input)

if input == "" {
return input, nil // Empty imput is okay
}

_, err := strconv.ParseFloat(input, 64)

if err != nil {
return input, ErrInvalidFloat
}

return input, nil
}

// validatorIsEmail is validator for email
func validatorIsEmail(input string) (string, error) {
input = strings.TrimSpace(input)

if input == "" {
return input, nil // Empty imput is okay
}

name, domain, ok := strings.Cut(input, "@")

if !ok || strings.TrimSpace(name) == "" ||
strings.TrimSpace(domain) == "" || !strings.Contains(domain, ".") {
return input, ErrInvalidEmail
}

return input, nil
}

// validatorIsURL is validator for URL
func validatorIsURL(input string) (string, error) {
input = strings.TrimSpace(input)

if input == "" {
return input, nil // Empty imput is okay
}

switch {
case strings.HasPrefix(input, "http://"),
strings.HasPrefix(input, "https://"),
strings.HasPrefix(input, "ftp://"):
// OK
default:
return input, ErrInvalidURL
}

if !strings.Contains(input, ".") {
return input, ErrInvalidURL
}

return input, nil
}

// ////////////////////////////////////////////////////////////////////////////////// //

// getMask returns mask for password
func getMask(message string) string {
var masking string
Expand Down Expand Up @@ -211,7 +343,7 @@ func getAnswerTitle(title, defaultAnswer string) string {
}

// readUserInput reads user input
func readUserInput(title string, nonEmpty, private bool) (string, error) {
func readUserInput(title string, private bool, validators []Validator) (string, error) {
if title != "" {
fmtc.Println(TitleColorTag + title + "{!}")
}
Expand All @@ -223,6 +355,7 @@ func readUserInput(title string, nonEmpty, private bool) (string, error) {
linenoise.SetMaskMode(true)
}

INPUT_LOOP:
for {
input, err = linenoise.Line(fmtc.Sprintf(Prompt))

Expand All @@ -234,9 +367,17 @@ func readUserInput(title string, nonEmpty, private bool) (string, error) {
return "", err
}

if nonEmpty && strings.TrimSpace(input) == "" {
terminal.Warn("\nYou must enter non-empty value\n")
continue
if len(validators) != 0 {
for _, validator := range validators {
input, err = validator(input)

if err != nil {
fmtc.NewLine()
terminal.Warn(err.Error())
fmtc.NewLine()
continue INPUT_LOOP
}
}
}

if private && input != "" {
Expand Down
Loading

0 comments on commit 9809e89

Please sign in to comment.