From 7dd7ba16331611029dadc16b7b61d462a1e1c59e Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Tue, 29 Oct 2024 12:45:56 +0300 Subject: [PATCH 01/11] [fmtutil/table] Add automatic breaks feature --- fmtutil/table/table.go | 16 ++++++++++++++++ fmtutil/table/table_test.go | 1 + 2 files changed, 17 insertions(+) diff --git a/fmtutil/table/table.go b/fmtutil/table/table.go index 06f71043..18f2db19 100644 --- a/fmtutil/table/table.go +++ b/fmtutil/table/table.go @@ -47,6 +47,9 @@ type Table struct { // Width is table maximum width Width int + // Breaks is an interval for separators between given number of rows + Breaks int + // SeparatorSymbol is symbol used for borders rendering BorderSymbol string @@ -91,6 +94,9 @@ type Table struct { // Slice with auto calculated sizes columnSizes []int + + // Cursor is number of the latest record + cursor int } // ////////////////////////////////////////////////////////////////////////////////// // @@ -116,6 +122,9 @@ var SeparatorColorTag = "{s}" // ColumnSeparatorSymbol is default column separator symbol var ColumnSeparatorSymbol = "|" +// Breaks is an interval for separators between given number of rows +var Breaks = -1 + // FullScreen is a flag for full-screen table by default var FullScreen = true @@ -126,6 +135,7 @@ func NewTable(headers ...string) *Table { return &Table{ HeaderCapitalize: HeaderCapitalize, FullScreen: FullScreen, + Breaks: Breaks, Headers: headers, Processor: convertSlice, } @@ -354,6 +364,10 @@ func renderData(t *Table) { // renderRowData render data in row func renderRowData(t *Table, data []string, totalColumns int) { + if t.Breaks > 0 && t.cursor > 0 && t.cursor%t.Breaks == 0 { + renderSeparator(t) + } + for columnIndex, columnData := range data { if columnIndex == totalColumns { break @@ -377,6 +391,8 @@ func renderRowData(t *Table, data []string, totalColumns int) { } } + t.cursor++ + fmtc.NewLine() } diff --git a/fmtutil/table/table_test.go b/fmtutil/table/table_test.go index ee147934..b0b176f1 100644 --- a/fmtutil/table/table_test.go +++ b/fmtutil/table/table_test.go @@ -109,6 +109,7 @@ func (s *TableSuite) TestRender(c *C) { t.BorderColorTag = "{b}" t.SeparatorColorTag = "{g}" t.HeaderCapitalize = true + t.Breaks = 2 c.Assert(t.Render(), NotNil) From 9094cd2f7c1ddb977155e6055da6688b7807910e Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Tue, 29 Oct 2024 17:34:54 +0300 Subject: [PATCH 02/11] Bump version --- CHANGELOG.md | 4 ++++ version.go | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b802af..d46c1c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Changelog +### [13.9.3](https://kaos.sh/ek/13.9.3) + +- `[fmtutil/table]` Added automatic breaks feature + ### [13.9.2](https://kaos.sh/ek/13.9.2) - `[knf]` Added helper `Q` diff --git a/version.go b/version.go index cc2b131f..47564c0a 100644 --- a/version.go +++ b/version.go @@ -8,4 +8,4 @@ package ek // ////////////////////////////////////////////////////////////////////////////////// // // VERSION is current ek package version -const VERSION = "13.9.2" +const VERSION = "13.9.3" From d95ada9b14fc2493e9dba4f8feb084068beb1afc Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 30 Oct 2024 11:54:57 +0300 Subject: [PATCH 03/11] [terminal/input] Code refactoring --- terminal/input/input.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal/input/input.go b/terminal/input/input.go index 55d04d32..d12d1fcd 100644 --- a/terminal/input/input.go +++ b/terminal/input/input.go @@ -93,7 +93,7 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) { fmtc.Println(TitleColorTag + getAnswerTitle(title, defaultAnswer) + "{!}") } - fmtc.Println(Prompt + "y") + fmtc.Println(Prompt + "{s}Y{!}") if NewLine { fmtc.NewLine() From 9809e897a0c40fc27e87fe3cdc2abffd7a514e89 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 30 Oct 2024 14:46:17 +0300 Subject: [PATCH 04/11] [terminal/input] Add input validation feature --- CHANGELOG.md | 6 +- terminal/input/example_test.go | 30 ++++-- terminal/input/input.go | 165 ++++++++++++++++++++++++++++++--- terminal/input/input_stubs.go | 60 +++++++++++- version.go | 2 +- 5 files changed, 236 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d46c1c52..8d899ea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/terminal/input/example_test.go b/terminal/input/example_test.go index 3db31def..5315a94f 100644 --- a/terminal/input/example_test.go +++ b/terminal/input/example_test.go @@ -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() { @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/terminal/input/input.go b/terminal/input/input.go index d12d1fcd..fd7844f0 100644 --- a/terminal/input/input.go +++ b/terminal/input/input.go @@ -14,6 +14,7 @@ package input import ( "fmt" "os" + "strconv" "strings" "unicode/utf8" @@ -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 @@ -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 @@ -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 { @@ -121,7 +166,7 @@ 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() } } @@ -129,14 +174,14 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) { // 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 @@ -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 @@ -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 + "{!}") } @@ -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)) @@ -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 != "" { diff --git a/terminal/input/input_stubs.go b/terminal/input/input_stubs.go index f5d79924..0e2f966e 100644 --- a/terminal/input/input_stubs.go +++ b/terminal/input/input_stubs.go @@ -19,9 +19,22 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // +// CompletionHandler is completion handler +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 var ErrKillSignal = errors.New("") +// ////////////////////////////////////////////////////////////////////////////////// // + // ❗ Prompt is prompt string var Prompt = "> " @@ -51,8 +64,49 @@ var NewLine = false // ////////////////////////////////////////////////////////////////////////////////// // +var ( + // NotEmpty returns an error if input is empty + NotEmpty = func(input string) (string, error) { return "", nil } + + // IsNumber returns an error if the input is not a valid number + IsNumber = func(input string) (string, error) { return "", nil } + + // IsFloat returns an error if the input is not a valid floating number + IsFloat = func(input string) (string, error) { return "", nil } + + // IsEmail returns an error if the input is not a valid email + IsEmail = func(input string) (string, error) { return "", nil } + + // IsURL returns an error if the input is not a valid URL + IsURL = func(input string) (string, error) { return "", nil } +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +var ( + // ErrInvalidAnswer is error for wrong answer for Y/N question + ErrInvalidAnswer = errors.New("") + + // ErrIsEmpty is error for empty input + ErrIsEmpty = errors.New("") + + // ErrInvalidNumber is error for invalid number + ErrInvalidNumber = errors.New("") + + // ErrInvalidFloat is error for invalid floating number + ErrInvalidFloat = errors.New("") + + // ErrInvalidEmail is error for invalid email + ErrInvalidEmail = errors.New("") + + // ErrInvalidURL is error for invalid URL + ErrInvalidURL = errors.New("") +) + +// ////////////////////////////////////////////////////////////////////////////////// // + // ❗ Read reads user input -func Read(title string, nonEmpty bool) (string, error) { +func Read(title string, validators ...Validator) (string, error) { panic("UNSUPPORTED") } @@ -63,13 +117,13 @@ func ReadAnswer(title string, defaultAnswers ...string) (bool, error) { // ❗ ReadPassword reads password or some private input that will be hidden // after pressing Enter -func ReadPassword(title string, nonEmpty bool) (string, error) { +func ReadPassword(title string, validators ...Validator) (string, error) { panic("UNSUPPORTED") } // ❗ ReadPasswordSecure reads password or some private input that will be hidden // after pressing Enter -func ReadPasswordSecure(title string, nonEmpty bool) (*secstr.String, error) { +func ReadPasswordSecure(title string, validators ...Validator) (*secstr.String, error) { panic("UNSUPPORTED") } diff --git a/version.go b/version.go index 47564c0a..31058bbe 100644 --- a/version.go +++ b/version.go @@ -8,4 +8,4 @@ package ek // ////////////////////////////////////////////////////////////////////////////////// // // VERSION is current ek package version -const VERSION = "13.9.3" +const VERSION = "13.10.0" From 574025799339310b84c12076cc3097066f66439a Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 30 Oct 2024 14:46:28 +0300 Subject: [PATCH 05/11] [terminal/input] Add input validation feature --- terminal/input/input_test.go | 79 ++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 terminal/input/input_test.go diff --git a/terminal/input/input_test.go b/terminal/input/input_test.go new file mode 100644 index 00000000..d0de34c7 --- /dev/null +++ b/terminal/input/input_test.go @@ -0,0 +1,79 @@ +package input + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "testing" + + . "github.com/essentialkaos/check" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func Test(t *testing.T) { TestingT(t) } + +type InputSuite struct{} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var _ = Suite(&InputSuite{}) + +// ////////////////////////////////////////////////////////////////////////////////// // + +func (s *InputSuite) TestValidators(c *C) { + _, err := validatorNotEmpty("") + c.Assert(err, Equals, ErrIsEmpty) + _, err = validatorNotEmpty(" ") + c.Assert(err, Equals, ErrIsEmpty) + _, err = validatorNotEmpty("test") + c.Assert(err, IsNil) + + _, err = validatorIsNumber("ABCD") + c.Assert(err, Equals, ErrInvalidNumber) + _, err = validatorIsNumber("1234") + c.Assert(err, IsNil) + _, err = validatorIsNumber("-1234") + c.Assert(err, IsNil) + _, err = validatorIsNumber("") + c.Assert(err, IsNil) + + _, err = validatorIsFloat("ABCD") + c.Assert(err, Equals, ErrInvalidFloat) + _, err = validatorIsFloat("1234.56") + c.Assert(err, IsNil) + _, err = validatorIsFloat("-1234.56") + c.Assert(err, IsNil) + _, err = validatorIsFloat("") + c.Assert(err, IsNil) + + _, err = validatorIsEmail("ABCD") + c.Assert(err, Equals, ErrInvalidEmail) + _, err = validatorIsEmail("@test") + c.Assert(err, Equals, ErrInvalidEmail) + _, err = validatorIsEmail("abcd@") + c.Assert(err, Equals, ErrInvalidEmail) + _, err = validatorIsEmail("abcd@test") + c.Assert(err, Equals, ErrInvalidEmail) + _, err = validatorIsEmail("") + c.Assert(err, IsNil) + _, err = validatorIsEmail("test@domain.com") + c.Assert(err, IsNil) + + _, err = validatorIsURL("abcd") + c.Assert(err, Equals, ErrInvalidURL) + _, err = validatorIsURL("abcd.com") + c.Assert(err, Equals, ErrInvalidURL) + _, err = validatorIsURL("https://abcd") + c.Assert(err, Equals, ErrInvalidURL) + _, err = validatorIsURL("test://abcd.com") + c.Assert(err, Equals, ErrInvalidURL) + _, err = validatorIsURL("") + c.Assert(err, IsNil) + _, err = validatorIsURL("https://domain.com") + c.Assert(err, IsNil) +} From dfbd8d956b4612e0740b7632e79c861254a3bf66 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 30 Oct 2024 16:08:23 +0300 Subject: [PATCH 06/11] [terminal/input] Add input validation feature --- terminal/input/input.go | 138 ++------------------------ terminal/input/input_test.go | 46 ++++----- terminal/input/input_validators.go | 150 +++++++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 152 deletions(-) create mode 100644 terminal/input/input_validators.go diff --git a/terminal/input/input.go b/terminal/input/input.go index fd7844f0..c8967ce5 100644 --- a/terminal/input/input.go +++ b/terminal/input/input.go @@ -14,7 +14,6 @@ package input import ( "fmt" "os" - "strconv" "strings" "unicode/utf8" @@ -36,8 +35,12 @@ 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) +// ////////////////////////////////////////////////////////////////////////////////// // + +// Validator is input validator type +type Validator interface { + Validate(input string) (string, error) +} // ////////////////////////////////////////////////////////////////////////////////// // @@ -75,44 +78,8 @@ 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") -) +// ErrInvalidAnswer is error for wrong answer for Y/N question +var ErrInvalidAnswer = fmt.Errorf("Please enter Y or N") // ////////////////////////////////////////////////////////////////////////////////// // @@ -212,93 +179,6 @@ 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 @@ -369,7 +249,7 @@ INPUT_LOOP: if len(validators) != 0 { for _, validator := range validators { - input, err = validator(input) + input, err = validator.Validate(input) if err != nil { fmtc.NewLine() diff --git a/terminal/input/input_test.go b/terminal/input/input_test.go index d0de34c7..30768759 100644 --- a/terminal/input/input_test.go +++ b/terminal/input/input_test.go @@ -26,54 +26,54 @@ var _ = Suite(&InputSuite{}) // ////////////////////////////////////////////////////////////////////////////////// // func (s *InputSuite) TestValidators(c *C) { - _, err := validatorNotEmpty("") + _, err := NotEmpty.Validate("") c.Assert(err, Equals, ErrIsEmpty) - _, err = validatorNotEmpty(" ") + _, err = NotEmpty.Validate(" ") c.Assert(err, Equals, ErrIsEmpty) - _, err = validatorNotEmpty("test") + _, err = NotEmpty.Validate("test") c.Assert(err, IsNil) - _, err = validatorIsNumber("ABCD") + _, err = IsNumber.Validate("ABCD") c.Assert(err, Equals, ErrInvalidNumber) - _, err = validatorIsNumber("1234") + _, err = IsNumber.Validate("1234") c.Assert(err, IsNil) - _, err = validatorIsNumber("-1234") + _, err = IsNumber.Validate("-1234") c.Assert(err, IsNil) - _, err = validatorIsNumber("") + _, err = IsNumber.Validate("") c.Assert(err, IsNil) - _, err = validatorIsFloat("ABCD") + _, err = IsFloat.Validate("ABCD") c.Assert(err, Equals, ErrInvalidFloat) - _, err = validatorIsFloat("1234.56") + _, err = IsFloat.Validate("1234.56") c.Assert(err, IsNil) - _, err = validatorIsFloat("-1234.56") + _, err = IsFloat.Validate("-1234.56") c.Assert(err, IsNil) - _, err = validatorIsFloat("") + _, err = IsFloat.Validate("") c.Assert(err, IsNil) - _, err = validatorIsEmail("ABCD") + _, err = IsEmail.Validate("ABCD") c.Assert(err, Equals, ErrInvalidEmail) - _, err = validatorIsEmail("@test") + _, err = IsEmail.Validate("@test") c.Assert(err, Equals, ErrInvalidEmail) - _, err = validatorIsEmail("abcd@") + _, err = IsEmail.Validate("abcd@") c.Assert(err, Equals, ErrInvalidEmail) - _, err = validatorIsEmail("abcd@test") + _, err = IsEmail.Validate("abcd@test") c.Assert(err, Equals, ErrInvalidEmail) - _, err = validatorIsEmail("") + _, err = IsEmail.Validate("") c.Assert(err, IsNil) - _, err = validatorIsEmail("test@domain.com") + _, err = IsEmail.Validate("test@domain.com") c.Assert(err, IsNil) - _, err = validatorIsURL("abcd") + _, err = IsURL.Validate("abcd") c.Assert(err, Equals, ErrInvalidURL) - _, err = validatorIsURL("abcd.com") + _, err = IsURL.Validate("abcd.com") c.Assert(err, Equals, ErrInvalidURL) - _, err = validatorIsURL("https://abcd") + _, err = IsURL.Validate("https://abcd") c.Assert(err, Equals, ErrInvalidURL) - _, err = validatorIsURL("test://abcd.com") + _, err = IsURL.Validate("test://abcd.com") c.Assert(err, Equals, ErrInvalidURL) - _, err = validatorIsURL("") + _, err = IsURL.Validate("") c.Assert(err, IsNil) - _, err = validatorIsURL("https://domain.com") + _, err = IsURL.Validate("https://domain.com") c.Assert(err, IsNil) } diff --git a/terminal/input/input_validators.go b/terminal/input/input_validators.go new file mode 100644 index 00000000..dc09ba25 --- /dev/null +++ b/terminal/input/input_validators.go @@ -0,0 +1,150 @@ +//go:build !windows +// +build !windows + +package input + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "errors" + "strconv" + "strings" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +type notEmptyValidator struct{} +type isNumberValidator struct{} +type isFloatValidator struct{} +type isEmailValidator struct{} +type isURLValidator struct{} + +// ////////////////////////////////////////////////////////////////////////////////// // + +var ( + // NotEmpty returns an error if input is empty + NotEmpty = notEmptyValidator{} + + // IsNumber returns an error if the input is not a valid number + IsNumber = isNumberValidator{} + + // IsFloat returns an error if the input is not a valid floating number + IsFloat = isFloatValidator{} + + // IsEmail returns an error if the input is not a valid email + IsEmail = isEmailValidator{} + + // IsURL returns an error if the input is not a valid URL + IsURL = isURLValidator{} +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +var ( + // ErrIsEmpty is error for empty input + ErrIsEmpty = errors.New("You must enter non-empty value") + + // ErrInvalidNumber is error for invalid number + ErrInvalidNumber = errors.New("Entered value is not a valid number") + + // ErrInvalidFloat is error for invalid floating number + ErrInvalidFloat = errors.New("Entered value is not a valid floating number") + + // ErrInvalidEmail is error for invalid email + ErrInvalidEmail = errors.New("Entered value is not a valid e-mail") + + // ErrInvalidURL is error for invalid URL + ErrInvalidURL = errors.New("Entered value is not a valid URL") +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Validate validates for empty input +func (v notEmptyValidator) Validate(input string) (string, error) { + if strings.TrimSpace(input) != "" { + return input, nil + } + + return input, ErrIsEmpty +} + +// Validate checks if the given input is a number +func (v isNumberValidator) Validate(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 +} + +// Validate checks if the given input is a floating number +func (v isFloatValidator) Validate(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 +} + +// Validate checks if the given input is an email +func (v isEmailValidator) Validate(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 +} + +// Validate checks if the given input is a URL +func (v isURLValidator) Validate(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 +} From c94d5446b49c5dcce7197319fb104c889ccb7e81 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Wed, 30 Oct 2024 16:10:27 +0300 Subject: [PATCH 07/11] [terminal/input] Add input validation feature --- terminal/input/input.go | 7 ----- terminal/input/input_stubs.go | 43 ++---------------------------- terminal/input/input_validators.go | 10 ++++--- 3 files changed, 9 insertions(+), 51 deletions(-) diff --git a/terminal/input/input.go b/terminal/input/input.go index c8967ce5..fa01222d 100644 --- a/terminal/input/input.go +++ b/terminal/input/input.go @@ -37,13 +37,6 @@ type HintHandler = func(input string) string // ////////////////////////////////////////////////////////////////////////////////// // -// Validator is input validator type -type Validator interface { - Validate(input string) (string, error) -} - -// ////////////////////////////////////////////////////////////////////////////////// // - // ErrKillSignal is error type when user cancel input var ErrKillSignal = linenoise.ErrKillSignal diff --git a/terminal/input/input_stubs.go b/terminal/input/input_stubs.go index 0e2f966e..84febd4d 100644 --- a/terminal/input/input_stubs.go +++ b/terminal/input/input_stubs.go @@ -25,9 +25,6 @@ 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 @@ -64,44 +61,8 @@ var NewLine = false // ////////////////////////////////////////////////////////////////////////////////// // -var ( - // NotEmpty returns an error if input is empty - NotEmpty = func(input string) (string, error) { return "", nil } - - // IsNumber returns an error if the input is not a valid number - IsNumber = func(input string) (string, error) { return "", nil } - - // IsFloat returns an error if the input is not a valid floating number - IsFloat = func(input string) (string, error) { return "", nil } - - // IsEmail returns an error if the input is not a valid email - IsEmail = func(input string) (string, error) { return "", nil } - - // IsURL returns an error if the input is not a valid URL - IsURL = func(input string) (string, error) { return "", nil } -) - -// ////////////////////////////////////////////////////////////////////////////////// // - -var ( - // ErrInvalidAnswer is error for wrong answer for Y/N question - ErrInvalidAnswer = errors.New("") - - // ErrIsEmpty is error for empty input - ErrIsEmpty = errors.New("") - - // ErrInvalidNumber is error for invalid number - ErrInvalidNumber = errors.New("") - - // ErrInvalidFloat is error for invalid floating number - ErrInvalidFloat = errors.New("") - - // ErrInvalidEmail is error for invalid email - ErrInvalidEmail = errors.New("") - - // ErrInvalidURL is error for invalid URL - ErrInvalidURL = errors.New("") -) +// ErrInvalidAnswer is error for wrong answer for Y/N question +var ErrInvalidAnswer = errors.New("") // ////////////////////////////////////////////////////////////////////////////////// // diff --git a/terminal/input/input_validators.go b/terminal/input/input_validators.go index dc09ba25..20e5436c 100644 --- a/terminal/input/input_validators.go +++ b/terminal/input/input_validators.go @@ -1,6 +1,3 @@ -//go:build !windows -// +build !windows - package input // ////////////////////////////////////////////////////////////////////////////////// // @@ -18,6 +15,13 @@ import ( // ////////////////////////////////////////////////////////////////////////////////// // +// Validator is input validator type +type Validator interface { + Validate(input string) (string, error) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + type notEmptyValidator struct{} type isNumberValidator struct{} type isFloatValidator struct{} From d12f1a29378bf5e564be5855d8e14ff2a9d9bd17 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 31 Oct 2024 14:01:15 +0300 Subject: [PATCH 08/11] [terminal/input] Minor UI improvements --- terminal/input/input.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/terminal/input/input.go b/terminal/input/input.go index fa01222d..aaf6659d 100644 --- a/terminal/input/input.go +++ b/terminal/input/input.go @@ -207,9 +207,9 @@ func getAnswerTitle(title, defaultAnswer string) string { switch strings.ToUpper(defaultAnswer) { case "Y": - return fmt.Sprintf(TitleColorTag+"%s ({*}Y{!*}/n){!}", title) + return fmt.Sprintf(TitleColorTag+"%s (Y/n){!}", title) case "N": - return fmt.Sprintf(TitleColorTag+"%s (y/{*}N{!*}){!}", title) + return fmt.Sprintf(TitleColorTag+"%s (y/N){!}", title) default: return fmt.Sprintf(TitleColorTag+"%s (y/n){!}", title) } From 762f4a0657115aab1ac3d9e0c64f5639eb9a3149 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 31 Oct 2024 14:16:59 +0300 Subject: [PATCH 09/11] [terminal/input] Fixes --- CHANGELOG.md | 2 ++ terminal/input/input.go | 13 +++++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d899ea2..7fb8ebe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - `[fmtutil/table]` Added automatic breaks feature - `[terminal/input]` Added input validation feature +- `[terminal/input]` Fixed bug with hiding the password when HidePassword is set to true and an empty input error is displayed +- `[terminal/input]` Fixed bug with printing new line after input field on error ### [13.9.2](https://kaos.sh/ek/13.9.2) diff --git a/terminal/input/input.go b/terminal/input/input.go index aaf6659d..6a96fc65 100644 --- a/terminal/input/input.go +++ b/terminal/input/input.go @@ -224,18 +224,19 @@ func readUserInput(title string, private bool, validators []Validator) (string, var input string var err error + if NewLine { + defer fmtc.NewLine() + } + if private && HidePassword { linenoise.SetMaskMode(true) + defer linenoise.SetMaskMode(false) } INPUT_LOOP: for { input, err = linenoise.Line(fmtc.Sprintf(Prompt)) - if private && HidePassword { - linenoise.SetMaskMode(false) - } - if err != nil { return "", err } @@ -270,10 +271,6 @@ INPUT_LOOP: break } - if NewLine { - fmtc.NewLine() - } - return input, err } From e3ce5ec9aaabf776570b704f335015547b828c5a Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 31 Oct 2024 14:20:03 +0300 Subject: [PATCH 10/11] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb8ebe6..68c06007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - `[fmtutil/table]` Added automatic breaks feature - `[terminal/input]` Added input validation feature -- `[terminal/input]` Fixed bug with hiding the password when HidePassword is set to true and an empty input error is displayed +- `[terminal/input]` Fixed bug with hiding the password when `HidePassword` is set to true and an empty input error is displayed - `[terminal/input]` Fixed bug with printing new line after input field on error ### [13.9.2](https://kaos.sh/ek/13.9.2) From 3bc215f67cc23e7fe910dbb9929839ce14eec7a8 Mon Sep 17 00:00:00 2001 From: Anton Novojilov Date: Thu, 31 Oct 2024 23:13:19 +0300 Subject: [PATCH 11/11] Improve changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c06007..3654ee11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### [13.10.0](https://kaos.sh/ek/13.10.0) -> [!CAUTION] +> [!IMPORTANT] > 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