diff --git a/.tasks.yml b/.tasks.yml new file mode 100644 index 00000000..d87975aa --- /dev/null +++ b/.tasks.yml @@ -0,0 +1,10 @@ +tests: + summary: Run the test suite + command: go test {{.files}} + +install-deps: + summary: Install all of package dependencies + command: go get -t {{.files}} + +variables: + files: '$(go list -v ./... | grep -Ev "github.com/AlecAivazis/survey/(tests|examples)")' diff --git a/.travis.yml b/.travis.yml index e04b93ea..bd8c87bd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,10 @@ language: go +before_install: + - go get github.com/AlecAivazis/run + install: - - go get -t . ./terminal/... + - run install-deps script: - - go test -v . ./terminal/... + - run tests diff --git a/README.md b/README.md index 83c7010a..a66caa32 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Survey [![Build Status](https://travis-ci.org/AlecAivazis/survey.svg?branch=feature%2Fpretty)](https://travis-ci.org/AlecAivazis/survey) -[![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](https://godoc.org/github.com/alecaivazis/survey) +[![GoDoc](http://img.shields.io/badge/godoc-reference-5272B4.svg)](https://godoc.org/github.com/AlecAivazis/survey) A library for building interactive prompts. Heavily inspired by the great [inquirer.js](https://github.com/SBoudrias/Inquirer.js/). @@ -14,7 +14,7 @@ package main import ( "fmt" - "gopkg.in/alecaivazis/survey.v0" + "gopkg.in/AlecAivazis/survey.v1" ) // the questions to ask @@ -26,23 +26,146 @@ var qs = []*survey.Question{ }, { Name: "color", - Prompt: &survey.Choice{ + Prompt: &survey.Select{ Message: "Choose a color:", - Choices: []string{"red", "blue", "green"}, + Options: []string{"red", "blue", "green"}, Default: "red", }, }, } func main() { - answers, err := survey.Ask(qs) + // the answers will be written to this struct + answers := struct { + Name string // survey will match the question and field names + FavoriteColor string `survey:"color"` // or you can tag fields to match a specific name + }{} + // perform the questions + err := survey.Ask(qs, &answers) if err != nil { - fmt.Println("\n", err.Error()) + fmt.Println(err.Error()) return } - fmt.Printf("%s chose %s.", answers["name"], answers["color"]) + fmt.Printf("%s chose %s.", answers.Name, answers.FavoriteColor) } +``` + +## Examples +Examples can be found in the `examples/` directory. Run them +to see basic behavior: +```bash +go get github.com/AlecAivazis/survey + +# ... navigate to the repo in your GOPATH + +go run examples/simple.go +go run examples/validation.go +``` + +## Prompts + +### Input + + +```golang +name := "" +prompt := &survey.Input{ + Message: "ping", +} +survey.AskOne(prompt, &name, nil) +``` + + +### Password + + +```golang +password := "" +prompt := &survey.Password{ + Message: "Please type your password", +} +survey.AskOne(prompt, &password, nil) +``` + + +### Confirm + + +```golang +name := false +prompt := &survey.Confirm{ + Message: "Do you like pie?", +} +survey.AskOne(prompt, &name, nil) +``` + + +### Select + + +```golang +color := "" +prompt := &survey.Select{ + Message: "Choose a color:", + Options: []string{"red", "blue", "green"}, +} +survey.AskOne(prompt, &color, nil) +``` + + +### MultiSelect + + +```golang +days := []string{} +prompt := &survey.MultiSelect{ + Message: "What days do you prefer:", + Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, +} +survey.AskOne(prompt, &days, nil) +``` + +## Validation + +Validating individual responses for a particular question can be done by defining a +`Validate` field on the `survey.Question` to be validated. This function takes an +`interface{}` type and returns an error to show to the user, prompting them for another +response: + +```golang +q := &survey.Question{ + Prompt: &survey.Input{Message: "Hello world validation"}, + Validate: func (val interface{}) error { + // since we are validating an Input, this will always succeed + if str, ok := val.(string) ; ok { + if len(str) > 10 { + return errors.New("This response cannot be longer than 10 characters.") + } + } + } +} +``` + +### Built-in Validators +`survey` comes prepackaged with a few validators to fit common situations. Currently these +validators include: + +| name | valid types | description | +|--------------|-----------------|---------------------------------------------------------------| +| Required | any | Rejects zero values of the response type | +| MinLength(n) | string | Enforces that a response is at least the given length | +| MaxLength(n) | string | Enforces that a response is no longer than the given length | + +## Versioning + +This project tries to maintain semantic GitHub releases as closely as possible. As such, services +like [gopkg.in](http://labix.org/gopkg.in) work very well to ensure non-breaking changes whenever +you build your application. For example, importing v1 of survey would look something like + +```golang +package main +import "gopkg.in/AlecAivazis/survey.v1" ``` diff --git a/confirm.go b/confirm.go index 9425f842..786b4898 100644 --- a/confirm.go +++ b/confirm.go @@ -4,7 +4,8 @@ import ( "fmt" "regexp" - "github.com/alecaivazis/survey/terminal" + "github.com/AlecAivazis/survey/core" + "github.com/AlecAivazis/survey/terminal" "github.com/chzyer/readline" ) @@ -12,7 +13,6 @@ import ( type Confirm struct { Message string Default bool - Answer *bool } // data available to the templates when processing @@ -64,7 +64,7 @@ func (c *Confirm) getBool(rl *readline.Instance) (bool, error) { answer = c.Default default: // we didnt get a valid answer, so print error and prompt again - out, err := RunTemplate( + out, err := core.RunTemplate( ErrorTemplate, fmt.Errorf("%q is not a valid answer, please try again.", val), ) // if something went wrong @@ -88,16 +88,9 @@ func (c *Confirm) getBool(rl *readline.Instance) (bool, error) { // Prompt prompts the user with a simple text field and expects a reply followed // by a carriage return. -func (c *Confirm) Prompt(rl *readline.Instance) (string, error) { - // if we weren't passed an answer - if c.Answer == nil { - // build one - answer := false - c.Answer = &answer - } - +func (c *Confirm) Prompt(rl *readline.Instance) (interface{}, error) { // render the question template - out, err := RunTemplate( + out, err := core.RunTemplate( ConfirmQuestionTemplate, ConfirmTemplateData{Confirm: *c}, ) @@ -116,24 +109,30 @@ func (c *Confirm) Prompt(rl *readline.Instance) (string, error) { return "", err } - // return the value - *c.Answer = answer - // convert the boolean into the appropriate string - return yesNo(answer), nil + return answer, nil } // Cleanup overwrite the line with the finalized formatted version -func (c *Confirm) Cleanup(rl *readline.Instance, val string) error { +func (c *Confirm) Cleanup(rl *readline.Instance, val interface{}) error { // go up one line terminal.CursorPreviousLine(1) // clear the line - terminal.EraseInLine(1) + terminal.EraseInLine(0) + + // the string version of the answer + ans := "" + // if the value was previously true + if val.(bool) { + ans = "yes" + } else { + ans = "no" + } // render the template - out, err := RunTemplate( + out, err := core.RunTemplate( ConfirmQuestionTemplate, - ConfirmTemplateData{Confirm: *c, Answer: val}, + ConfirmTemplateData{Confirm: *c, Answer: ans}, ) if err != nil { return err diff --git a/confirm_test.go b/confirm_test.go index ad730fb9..c117d0bf 100644 --- a/confirm_test.go +++ b/confirm_test.go @@ -2,11 +2,13 @@ package survey import ( "testing" + + "github.com/AlecAivazis/survey/core" ) func init() { // disable color output for all prompts to simplify testing - DisableColor = true + core.DisableColor = true } func TestConfirmFormatQuestion(t *testing.T) { @@ -16,7 +18,7 @@ func TestConfirmFormatQuestion(t *testing.T) { Default: true, } - actual, err := RunTemplate( + actual, err := core.RunTemplate( ConfirmQuestionTemplate, ConfirmTemplateData{Confirm: *prompt}, ) @@ -38,7 +40,7 @@ func TestConfirmFormatQuestionDefaultFalse(t *testing.T) { Default: false, } - actual, err := RunTemplate( + actual, err := core.RunTemplate( ConfirmQuestionTemplate, ConfirmTemplateData{Confirm: *prompt}, ) @@ -60,7 +62,7 @@ func TestConfirmFormatAnswer(t *testing.T) { Message: "Is pizza your favorite food?", } - actual, err := RunTemplate( + actual, err := core.RunTemplate( ConfirmQuestionTemplate, ConfirmTemplateData{Confirm: *prompt, Answer: "Yes"}, ) diff --git a/template.go b/core/template.go similarity index 98% rename from template.go rename to core/template.go index 88ce8a2f..c2716cc0 100644 --- a/template.go +++ b/core/template.go @@ -1,4 +1,4 @@ -package survey +package core import ( "bytes" diff --git a/core/write.go b/core/write.go new file mode 100644 index 00000000..6c3c862e --- /dev/null +++ b/core/write.go @@ -0,0 +1,102 @@ +package core + +import ( + "errors" + "fmt" + "reflect" + "strings" +) + +// the tag used to denote the name of the question +const tagName = "survey" + +func WriteAnswer(t interface{}, name string, v interface{}) (err error) { + // the target to write to + target := reflect.ValueOf(t) + // the value to write from + value := reflect.ValueOf(v) + + // make sure we are writing to a pointer + if target.Kind() != reflect.Ptr { + return errors.New("you must pass a pointer as the target of a Write operation") + } + // the object "inside" of the target pointer + elem := target.Elem() + + // handle the special types + switch elem.Kind() { + // if we are writing to a struct + case reflect.Struct: + // get the name of the field that matches the string we were given + fieldIndex, err := findFieldIndex(elem, name) + // if something went wrong + if err != nil { + // bubble up + return err + } + + // copy the value over to the field + return copy(elem.Field(fieldIndex), value) + } + + // otherwise just copy the value to the target + return copy(elem, value) +} + +// BUG(AlecAivazis): the current implementation might cause weird conflicts if there are +// two fields with same name that only differ by casing. +func findFieldIndex(s reflect.Value, name string) (int, error) { + // the type of the value + sType := s.Type() + + // first look for matching tags so we can overwrite matching field names + for i := 0; i < sType.NumField(); i++ { + // the field we are current scanning + field := sType.Field(i) + + // the value of the survey tag + tag := field.Tag.Get(tagName) + // if the tag matches the name we are looking for + if tag != "" && tag == name { + // then we found our index + return i, nil + } + } + + // then look for matching names + for i := 0; i < sType.NumField(); i++ { + // the field we are current scanning + field := sType.Field(i) + + // if the name of the field matches what we're looking for + if strings.ToLower(field.Name) == strings.ToLower(name) { + return i, nil + } + } + + // we didn't find the field + return -1, fmt.Errorf("could not find field matching %v", name) +} + +// Write takes a value and copies it to the target +func copy(t reflect.Value, v reflect.Value) (err error) { + // if something ends up panicing we need to catch it in a deferred func + defer func() { + if r := recover(); r != nil { + // if we paniced with an error + if _, ok := r.(error); ok { + // cast the result to an error object + err = r.(error) + } else if _, ok := r.(string); ok { + // otherwise we could have paniced with a string so wrap it in an error + err = errors.New(r.(string)) + } + } + }() + + // attempt to copy the underlying value to the target + t.Set(v) + + // we're done + return +} diff --git a/core/write_test.go b/core/write_test.go new file mode 100644 index 00000000..a410df3a --- /dev/null +++ b/core/write_test.go @@ -0,0 +1,219 @@ +package core + +import ( + "reflect" + "testing" +) + +func TestWrite_returnsErrorIfTargetNotPtr(t *testing.T) { + // try to copy a value to a non-pointer + err := WriteAnswer(true, "hello", true) + // make sure there was an error + if err == nil { + t.Error("Did not encounter error when writing to non-pointer.") + } +} + +func TestWrite_canWriteToBool(t *testing.T) { + // a pointer to hold the boolean value + ptr := true + + // try to copy a false value to the pointer + WriteAnswer(&ptr, "", false) + + // if the value is true + if ptr { + // the test failed + t.Error("Could not write a false bool to a pointer") + } +} + +func TestWrite_canWriteString(t *testing.T) { + // a pointer to hold the boolean value + ptr := "" + + // try to copy a false value to the pointer + err := WriteAnswer(&ptr, "", "hello") + if err != nil { + t.Error(err) + } + + // if the value is not what we wrote + if ptr != "hello" { + t.Error("Could not write a string value to a pointer") + } +} + +func TestWrite_canWriteSlice(t *testing.T) { + // a pointer to hold the value + ptr := []string{} + + // copy in a value + WriteAnswer(&ptr, "", []string{"hello", "world"}) + + // make sure there are two entries + if len(ptr) != 2 { + // the test failed + t.Errorf("Incorrect number of entries in written list. Expected 2, found %v.", len(ptr)) + // dont move on + return + } + + // make sure the first entry is hello + if ptr[0] != "hello" { + // the test failed + t.Errorf("incorrect first value in written pointer. expected hello found %v.", ptr[0]) + } + + // make sure the second entry is world + if ptr[1] != "world" { + // the test failed + t.Errorf("incorrect second value in written pointer. expected world found %v.", ptr[0]) + } +} + +func TestWrite_recoversInvalidReflection(t *testing.T) { + // a variable to mutate + ptr := false + + // write a boolean value to the string + err := WriteAnswer(&ptr, "", "hello") + + // if there was no error + if err == nil { + // the test failed + t.Error("Did not encounter error when forced invalid write.") + } +} + +func TestWriteAnswer_handlesNonStructValues(t *testing.T) { + // the value to write to + ptr := "" + + // write a value to the pointer + WriteAnswer(&ptr, "", "world") + + // if we didn't change the value appropriate + if ptr != "world" { + // the test failed + t.Error("Did not write value to primitive pointer") + } +} + +func TestWriteAnswer_canMutateStruct(t *testing.T) { + // the struct to hold the answer + ptr := struct{ Name string }{} + + // write a value to an existing field + err := WriteAnswer(&ptr, "name", "world") + if err != nil { + // the test failed + t.Errorf("Encountered error while writing answer: %v", err.Error()) + // we're done here + return + } + + // make sure we changed the field + if ptr.Name != "world" { + // the test failed + t.Error("Did not mutate struct field when writing answer.") + } +} + +func TestWriteAnswer_returnsErrWhenFieldNotFound(t *testing.T) { + // the struct to hold the answer + ptr := struct{ Name string }{} + + // write a value to an existing field + err := WriteAnswer(&ptr, "", "world") + + if err == nil { + // the test failed + t.Error("Did not encountered error while writing answer to non-existing field.") + } +} + +func TestFindFieldIndex_canFindExportedField(t *testing.T) { + // create a reflective wrapper over the struct to look through + val := reflect.ValueOf(struct{ Name string }{}) + + // find the field matching "name" + fieldIndex, err := findFieldIndex(val, "name") + // if something went wrong + if err != nil { + // the test failed + t.Error(err.Error()) + return + } + + // make sure we got the right value + if val.Type().Field(fieldIndex).Name != "Name" { + // the test failed + t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name) + } +} + +func TestFindFieldIndex_canFindTaggedField(t *testing.T) { + // the struct to look through + val := reflect.ValueOf(struct { + Username string `survey:"name"` + }{}) + + // find the field matching "name" + fieldIndex, err := findFieldIndex(val, "name") + // if something went wrong + if err != nil { + // the test failed + t.Error(err.Error()) + return + } + + // make sure we got the right value + if val.Type().Field(fieldIndex).Name != "Username" { + // the test failed + t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name) + } +} + +func TestFindFieldIndex_canHandleCapitalAnswerNames(t *testing.T) { + // create a reflective wrapper over the struct to look through + val := reflect.ValueOf(struct{ Name string }{}) + + // find the field matching "name" + fieldIndex, err := findFieldIndex(val, "Name") + // if something went wrong + if err != nil { + // the test failed + t.Error(err.Error()) + return + } + + // make sure we got the right value + if val.Type().Field(fieldIndex).Name != "Name" { + // the test failed + t.Errorf("Did not find the correct field name. Expected 'Name' found %v.", val.Type().Field(fieldIndex).Name) + } +} + +func TestFindFieldIndex_tagOverwriteFieldName(t *testing.T) { + // the struct to look through + val := reflect.ValueOf(struct { + Name string + Username string `survey:"name"` + }{}) + + // find the field matching "name" + fieldIndex, err := findFieldIndex(val, "name") + // if something went wrong + if err != nil { + // the test failed + t.Error(err.Error()) + return + } + + // make sure we got the right value + if val.Type().Field(fieldIndex).Name != "Username" { + // the test failed + t.Errorf("Did not find the correct field name. Expected 'Username' found %v.", val.Type().Field(fieldIndex).Name) + } +} diff --git a/examples/confirm.go b/examples/confirm.go deleted file mode 100644 index d90ca8d8..00000000 --- a/examples/confirm.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/alecaivazis/survey" -) - -func main() { - prompt := &survey.Confirm{ - Message: "Are you happy?", - } - - answer, err := survey.AskOne(prompt) - - if err != nil { - fmt.Println(err.Error()) - return - } - - fmt.Printf("response string: %s\n", answer) -} diff --git a/examples/multichoice.go b/examples/multichoice.go deleted file mode 100644 index 17ba0f01..00000000 --- a/examples/multichoice.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/alecaivazis/survey" -) - -func main() { - days := []string{} - prompt := &survey.MultiChoice{ - Message: "What days do you prefer:", - Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, - Defaults: []string{"Saturday", "Sunday"}, - Answer: &days, - } - - answer, err := survey.AskOne(prompt) - - if err != nil { - fmt.Println(err.Error()) - return - } - - fmt.Printf("response string (json for MultiChoice): %s\n", answer) - fmt.Printf("response days: %#v\n", days) -} diff --git a/examples/simple.go b/examples/simple.go index 6fd98b3d..c3723fa5 100644 --- a/examples/simple.go +++ b/examples/simple.go @@ -3,7 +3,7 @@ package main import ( "fmt" - "github.com/alecaivazis/survey" + "github.com/AlecAivazis/survey" ) // the questions to ask @@ -17,22 +17,26 @@ var simpleQs = []*survey.Question{ }, { Name: "color", - Prompt: &survey.Choice{ + Prompt: &survey.Select{ Message: "Choose a color:", - Choices: []string{"red", "blue", "green"}, + Options: []string{"red", "blue", "green"}, }, Validate: survey.Required, }, } func main() { + answers := struct { + Name string + Color string + }{} // ask the question - answers, err := survey.Ask(simpleQs) + err := survey.Ask(simpleQs, &answers) if err != nil { fmt.Println(err.Error()) return } // print the answers - fmt.Printf("%s chose %s", answers["name"], answers["color"]) + fmt.Printf("%s chose %s.\n", answers.Name, answers.Color) } diff --git a/examples/validation.go b/examples/validation.go index bc0b758c..396aa0cb 100644 --- a/examples/validation.go +++ b/examples/validation.go @@ -1,9 +1,9 @@ package main import ( - "errors" "fmt" - "github.com/alecaivazis/survey" + + "github.com/AlecAivazis/survey" ) // the questions to ask @@ -16,10 +16,10 @@ var validationQs = []*survey.Question{ { Name: "valid", Prompt: &survey.Input{"Enter 'foo':", "not foo"}, - Validate: func(str string) error { + Validate: func(val interface{}) error { // if the input matches the expectation - if str != "foo" { - return errors.New(fmt.Sprintf("You entered %s, not 'foo'.", str)) + if str := val.(string); str != "foo" { + return fmt.Errorf("You entered %s, not 'foo'.", str) } // nothing was wrong return nil @@ -28,8 +28,12 @@ var validationQs = []*survey.Question{ } func main() { - - _, err := survey.Ask(validationQs) + // the place to hold the answers + answers := struct { + Name string + Valid string + }{} + err := survey.Ask(validationQs, &answers) if err != nil { fmt.Println("\n", err.Error()) diff --git a/input.go b/input.go index fcf0cbf0..3ca884db 100644 --- a/input.go +++ b/input.go @@ -3,7 +3,8 @@ package survey import ( "fmt" - "github.com/alecaivazis/survey/terminal" + "github.com/AlecAivazis/survey/core" + "github.com/AlecAivazis/survey/terminal" "github.com/chzyer/readline" ) @@ -30,9 +31,9 @@ var InputQuestionTemplate = ` {{- if .Default}}{{color "white"}}({{.Default}}) {{color "reset"}}{{end}} {{- end}}` -func (i *Input) Prompt(rl *readline.Instance) (line string, err error) { +func (i *Input) Prompt(rl *readline.Instance) (line interface{}, err error) { // render the template - out, err := RunTemplate( + out, err := core.RunTemplate( InputQuestionTemplate, InputTemplateData{Input: *i}, ) @@ -47,16 +48,16 @@ func (i *Input) Prompt(rl *readline.Instance) (line string, err error) { return line, err } -func (i *Input) Cleanup(rl *readline.Instance, val string) error { +func (i *Input) Cleanup(rl *readline.Instance, val interface{}) error { // go up one line terminal.CursorPreviousLine(1) // clear the line - terminal.EraseInLine(1) + terminal.EraseInLine(0) // render the template - out, err := RunTemplate( + out, err := core.RunTemplate( InputQuestionTemplate, - InputTemplateData{Input: *i, Answer: val}, + InputTemplateData{Input: *i, Answer: val.(string)}, ) if err != nil { return err diff --git a/input_test.go b/input_test.go index 3d680a6a..32e372f7 100644 --- a/input_test.go +++ b/input_test.go @@ -2,11 +2,13 @@ package survey import ( "testing" + + "github.com/AlecAivazis/survey/core" ) func init() { // disable color output for all prompts to simplify testing - DisableColor = true + core.DisableColor = true } func TestInputFormatQuestion(t *testing.T) { @@ -16,7 +18,7 @@ func TestInputFormatQuestion(t *testing.T) { Default: "April", } - actual, err := RunTemplate( + actual, err := core.RunTemplate( InputQuestionTemplate, InputTemplateData{Input: *prompt}, ) @@ -38,7 +40,7 @@ func TestInputFormatAnswer(t *testing.T) { Default: "April", } - actual, err := RunTemplate( + actual, err := core.RunTemplate( InputQuestionTemplate, InputTemplateData{Input: *prompt, Answer: "October"}, ) diff --git a/multichoice_test.go b/multichoice_test.go deleted file mode 100644 index 41c38d9c..00000000 --- a/multichoice_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package survey - -import "testing" - -func init() { - // disable color output for all prompts to simplify testing - DisableColor = true -} - -func TestCanFormatMultiChoiceOptions(t *testing.T) { - - prompt := &MultiChoice{ - Options: []string{"foo", "bar", "baz", "buz"}, - Defaults: []string{"bar", "buz"}, - } - - actual, err := RunTemplate( - MultiChoiceOptionsTemplate, - MultiChoiceTemplateData{ - MultiChoice: *prompt, - SelectedIndex: 2, - Checked: map[int]bool{1: true, 3: true}, - }, - ) - - if err != nil { - t.Errorf("Failed to run template to format checkbox options: %s", err) - } - - expected := ` ◯ foo - ◉ bar -❯ ◯ baz - ◉ buz -` - - if actual != expected { - t.Errorf("Formatted checkbox options were not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) - } -} - -func TestMultiChoiceFormatQuestion(t *testing.T) { - - prompt := &MultiChoice{ - Message: "Pick your words:", - Options: []string{"foo", "bar", "baz", "buz"}, - Defaults: []string{"bar", "buz"}, - } - - actual, err := RunTemplate( - MultiChoiceQuestionTemplate, - MultiChoiceTemplateData{MultiChoice: *prompt}, - ) - if err != nil { - t.Errorf("Failed to run template to format checkbox question: %s", err) - } - - expected := `? Pick your words: ` - - if actual != expected { - t.Errorf("Formatted checkbox question was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) - } -} - -func TestMultiChoiceFormatAnswer(t *testing.T) { - - prompt := &MultiChoice{ - Message: "Pick your words:", - Options: []string{"foo", "bar", "baz", "buz"}, - Defaults: []string{"bar", "buz"}, - } - - actual, err := RunTemplate( - MultiChoiceQuestionTemplate, - MultiChoiceTemplateData{MultiChoice: *prompt, Answer: []string{"foo", "buz"}}, - ) - if err != nil { - t.Errorf("Failed to run template to format checkbox answer: %s", err) - } - - expected := `? Pick your words: ["foo" "buz"]` - - if actual != expected { - t.Errorf("Formatted checkbox answer was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) - } -} diff --git a/multichoice.go b/multiselect.go similarity index 66% rename from multichoice.go rename to multiselect.go index 3310baf9..7a0ff0e4 100644 --- a/multichoice.go +++ b/multiselect.go @@ -1,40 +1,39 @@ package survey import ( - "encoding/json" "errors" "io/ioutil" "strings" - "github.com/alecaivazis/survey/terminal" + "github.com/AlecAivazis/survey/core" + "github.com/AlecAivazis/survey/terminal" "github.com/chzyer/readline" ) -// MultiChoice is a prompt that presents a list of various options to the user +// MultiSelect is a prompt that presents a list of various options to the user // for them to select using the arrow keys and enter. -type MultiChoice struct { +type MultiSelect struct { Message string Options []string - Defaults []string - Answer *[]string + Default []string selectedIndex int checked map[int]bool } // data available to the templates when processing -type MultiChoiceTemplateData struct { - MultiChoice - Answer []string +type MultiSelectTemplateData struct { + MultiSelect + Answer string Checked map[int]bool SelectedIndex int } -var MultiChoiceQuestionTemplate = ` +var MultiSelectQuestionTemplate = ` {{- color "green+hb"}}? {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}} -{{- if .Answer}}{{color "cyan"}}{{.Answer | printf "%q"}}{{color "reset"}}{{end}}` +{{- if .Answer}}{{color "cyan"}}{{.Answer}}{{color "reset"}}{{end}}` -var MultiChoiceOptionsTemplate = ` +var MultiSelectOptionsTemplate = ` {{- range $ix, $option := .Options}} {{- if eq $ix $.SelectedIndex}}{{color "cyan"}}❯{{color "reset"}}{{else}} {{end}} {{- if index $.Checked $ix}}{{color "green"}} ◉ {{else}}{{color "default+hb"}} ◯ {{end}} @@ -43,7 +42,7 @@ var MultiChoiceOptionsTemplate = ` {{end}}` // OnChange is called on every keypress. -func (m *MultiChoice) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { +func (m *MultiSelect) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { if key == terminal.KeyEnter { // just pass on the current value return line, 0, true @@ -72,18 +71,18 @@ func (m *MultiChoice) OnChange(line []rune, pos int, key rune) (newLine []rune, return line, 0, true } -func (m *MultiChoice) render() error { +func (m *MultiSelect) render() error { // clean up what we left behind last time for range m.Options { terminal.CursorPreviousLine(1) - terminal.EraseInLine(1) + terminal.EraseInLine(0) } // render the template summarizing the current state - out, err := RunTemplate( - MultiChoiceOptionsTemplate, - MultiChoiceTemplateData{ - MultiChoice: *m, + out, err := core.RunTemplate( + MultiSelectOptionsTemplate, + MultiSelectTemplateData{ + MultiSelect: *m, SelectedIndex: m.selectedIndex, Checked: m.checked, }, @@ -99,14 +98,7 @@ func (m *MultiChoice) render() error { return nil } -func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) { - // if the user didn't pass an answer reference - if m.Answer == nil { - // build one - answer := []string{} - m.Answer = &answer - } - +func (m *MultiSelect) Prompt(rl *readline.Instance) (interface{}, error) { // the readline config config := &readline.Config{ Listener: m, @@ -117,8 +109,8 @@ func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) { // compute the default state m.checked = make(map[int]bool) // if there is a default - if len(m.Defaults) > 0 { - for _, dflt := range m.Defaults { + if len(m.Default) > 0 { + for _, dflt := range m.Default { for i, opt := range m.Options { // if the option correponds to the default if opt == dflt { @@ -137,10 +129,10 @@ func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) { return "", errors.New("please provide options to select from") } // generate the template for the current state of the prompt - out, err := RunTemplate( - MultiChoiceQuestionTemplate, - MultiChoiceTemplateData{ - MultiChoice: *m, + out, err := core.RunTemplate( + MultiSelectQuestionTemplate, + MultiSelectTemplateData{ + MultiSelect: *m, SelectedIndex: m.selectedIndex, Checked: m.checked, }, @@ -171,48 +163,27 @@ func (m *MultiChoice) Prompt(rl *readline.Instance) (string, error) { answers = append(answers, option) } } - *m.Answer = answers - // nothing went wrong - return m.value() -} - -func (m *MultiChoice) value() (string, error) { - answers := []string{} - for ix, option := range m.Options { - if val, ok := m.checked[ix]; ok && val { - answers = append(answers, option) - } - } - // return the selected option - js, err := json.Marshal(answers) - if err != nil { - return "", err - } - return string(js), nil + return answers, nil } // Cleanup removes the options section, and renders the ask like a normal question. -func (m *MultiChoice) Cleanup(rl *readline.Instance, val string) error { +func (m *MultiSelect) Cleanup(rl *readline.Instance, val interface{}) error { terminal.CursorPreviousLine(1) - terminal.EraseInLine(1) + terminal.EraseInLine(0) for range m.Options { terminal.CursorPreviousLine(1) - terminal.EraseInLine(1) + terminal.EraseInLine(0) } - // parse the value into a list of strings - var value []string - json.Unmarshal([]byte(val), &value) - // execute the output summary template with the answer - output, err := RunTemplate( - MultiChoiceQuestionTemplate, - MultiChoiceTemplateData{ - MultiChoice: *m, + output, err := core.RunTemplate( + MultiSelectQuestionTemplate, + MultiSelectTemplateData{ + MultiSelect: *m, SelectedIndex: m.selectedIndex, Checked: m.checked, - Answer: value, + Answer: strings.Join(val.([]string), ", "), }, ) if err != nil { diff --git a/multiselect_test.go b/multiselect_test.go new file mode 100644 index 00000000..ce53a510 --- /dev/null +++ b/multiselect_test.go @@ -0,0 +1,90 @@ +package survey + +import ( + "strings" + "testing" + + "github.com/AlecAivazis/survey/core" +) + +func init() { + // disable color output for all prompts to simplify testing + core.DisableColor = true +} + +func TestCanFormatMultiSelectOptions(t *testing.T) { + + prompt := &MultiSelect{ + Options: []string{"foo", "bar", "baz", "buz"}, + Default: []string{"bar", "buz"}, + } + + actual, err := core.RunTemplate( + MultiSelectOptionsTemplate, + MultiSelectTemplateData{ + MultiSelect: *prompt, + SelectedIndex: 2, + Checked: map[int]bool{1: true, 3: true}, + }, + ) + + if err != nil { + t.Errorf("Failed to run template to format checkbox options: %s", err) + } + + expected := ` ◯ foo + ◉ bar +❯ ◯ baz + ◉ buz +` + + if actual != expected { + t.Errorf("Formatted checkbox options were not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) + } +} + +func TestMultiSelectFormatQuestion(t *testing.T) { + + prompt := &MultiSelect{ + Message: "Pick your words:", + Options: []string{"foo", "bar", "baz", "buz"}, + Default: []string{"bar", "buz"}, + } + + actual, err := core.RunTemplate( + MultiSelectQuestionTemplate, + MultiSelectTemplateData{MultiSelect: *prompt}, + ) + if err != nil { + t.Errorf("Failed to run template to format checkbox question: %s", err) + } + + expected := `? Pick your words: ` + + if actual != expected { + t.Errorf("Formatted checkbox question was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) + } +} + +func TestMultiSelectFormatAnswer(t *testing.T) { + + prompt := &MultiSelect{ + Message: "Pick your words:", + Options: []string{"foo", "bar", "baz", "buz"}, + Default: []string{"bar", "buz"}, + } + + actual, err := core.RunTemplate( + MultiSelectQuestionTemplate, + MultiSelectTemplateData{MultiSelect: *prompt, Answer: strings.Join([]string{"foo", "buz"}, ", ")}, + ) + if err != nil { + t.Errorf("Failed to run template to format checkbox answer: %s", err) + } + + expected := `? Pick your words: foo, buz` + + if actual != expected { + t.Errorf("Formatted checkbox answer was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) + } +} diff --git a/password.go b/password.go index 1c9f26c0..08531aa5 100644 --- a/password.go +++ b/password.go @@ -2,6 +2,8 @@ package survey import ( "github.com/chzyer/readline" + + "github.com/AlecAivazis/survey/core" ) // Password is like a normal Input but the text shows up as *'s and @@ -15,9 +17,9 @@ var PasswordQuestionTemplate = ` {{- color "green+hb"}}? {{color "reset"}} {{- color "default+hb"}}{{ .Message }} {{color "reset"}}` -func (p *Password) Prompt(rl *readline.Instance) (line string, err error) { +func (p *Password) Prompt(rl *readline.Instance) (line interface{}, err error) { // render the question template - out, err := RunTemplate( + out, err := core.RunTemplate( PasswordQuestionTemplate, *p, ) @@ -39,6 +41,6 @@ func (p *Password) Prompt(rl *readline.Instance) (line string, err error) { } // Cleanup hides the string with a fixed number of characters. -func (prompt *Password) Cleanup(rl *readline.Instance, val string) error { +func (prompt *Password) Cleanup(rl *readline.Instance, val interface{}) error { return nil } diff --git a/password_test.go b/password_test.go index 3fd7b7c3..3d1d049d 100644 --- a/password_test.go +++ b/password_test.go @@ -2,11 +2,13 @@ package survey import ( "testing" + + "github.com/AlecAivazis/survey/core" ) func init() { // disable color output for all prompts to simplify testing - DisableColor = true + core.DisableColor = true } func TestPasswordFormatQuestion(t *testing.T) { @@ -15,7 +17,7 @@ func TestPasswordFormatQuestion(t *testing.T) { Message: "Tell me your secret:", } - actual, err := RunTemplate( + actual, err := core.RunTemplate( PasswordQuestionTemplate, *prompt, ) diff --git a/choice.go b/select.go similarity index 63% rename from choice.go rename to select.go index 2a5f2e96..109a9df7 100644 --- a/choice.go +++ b/select.go @@ -5,71 +5,73 @@ import ( "io/ioutil" "strings" - "github.com/alecaivazis/survey/terminal" + "github.com/AlecAivazis/survey/core" + "github.com/AlecAivazis/survey/terminal" "github.com/chzyer/readline" ) -// Choice is a prompt that presents a list of various options to the user +// Select is a prompt that presents a list of various options to the user // for them to select using the arrow keys and enter. -type Choice struct { +type Select struct { Message string - Choices []string + Options []string Default string - SelectedIndex int + selectedIndex int } // the data available to the templates when processing type SelectTemplateData struct { - Select Choice - Answer string + Select + SelectedIndex int + Answer string } const ( SelectQuestionTemplate = ` {{- color "green+hb"}}? {{color "reset"}} -{{- color "default+hb"}}{{ $.Select.Message }} {{color "reset"}} +{{- color "default+hb"}}{{ .Message }} {{color "reset"}} {{- if .Answer}}{{color "cyan"}}{{.Answer}}{{color "reset"}}{{end}}` // the template used to show the list of Selects SelectChoicesTemplate = ` -{{- range $ix, $choice := $.Select.Choices}} - {{- if eq $ix $.Select.SelectedIndex}}{{color "cyan+b"}}> {{else}}{{color "default+hb"}} {{end}} +{{- range $ix, $choice := .Options}} + {{- if eq $ix $.SelectedIndex}}{{color "cyan+b"}}> {{else}}{{color "default+hb"}} {{end}} {{- $choice}} {{- color "reset"}} {{end}}` ) // OnChange is called on every keypress. -func (s *Choice) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { +func (s *Select) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) { // if the user pressed the enter key if key == terminal.KeyEnter { - return []rune(s.Choices[s.SelectedIndex]), 0, true + return []rune(s.Options[s.selectedIndex]), 0, true // if the user pressed the up arrow - } else if key == terminal.KeyArrowUp && s.SelectedIndex > 0 { + } else if key == terminal.KeyArrowUp && s.selectedIndex > 0 { // decrement the selected index - s.SelectedIndex-- + s.selectedIndex-- // if the user pressed down and there is room to move - } else if key == terminal.KeyArrowDown && s.SelectedIndex < len(s.Choices)-1 { + } else if key == terminal.KeyArrowDown && s.selectedIndex < len(s.Options)-1 { // increment the selected index - s.SelectedIndex++ + s.selectedIndex++ } // render the options s.render() // if we are not pressing ent - return []rune(s.Choices[s.SelectedIndex]), 0, true + return []rune(s.Options[s.selectedIndex]), 0, true } -func (s *Choice) render() error { - for range s.Choices { +func (s *Select) render() error { + for range s.Options { terminal.CursorPreviousLine(1) - terminal.EraseInLine(1) + terminal.EraseInLine(0) } // the formatted response - out, err := RunTemplate( + out, err := core.RunTemplate( SelectChoicesTemplate, - SelectTemplateData{Select: *s}, + SelectTemplateData{Select: *s, SelectedIndex: s.selectedIndex}, ) if err != nil { return err @@ -81,7 +83,7 @@ func (s *Choice) render() error { return nil } -func (s *Choice) Prompt(rl *readline.Instance) (string, error) { +func (s *Select) Prompt(rl *readline.Instance) (interface{}, error) { config := &readline.Config{ Listener: s, Stdout: ioutil.Discard, @@ -89,7 +91,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) { rl.SetConfig(config) // if there are no options to render - if len(s.Choices) == 0 { + if len(s.Options) == 0 { // we failed return "", errors.New("please provide options to select from") } @@ -99,7 +101,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) { // if there is a default if s.Default != "" { // find the choice - for i, opt := range s.Choices { + for i, opt := range s.Options { // if the option correponds to the default if opt == s.Default { // we found our initial value @@ -110,12 +112,12 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) { } } // save the selected index - s.SelectedIndex = sel + s.selectedIndex = sel // render the initial question - out, err := RunTemplate( + out, err := core.RunTemplate( SelectQuestionTemplate, - SelectTemplateData{Select: *s}, + SelectTemplateData{Select: *s, SelectedIndex: sel}, ) if err != nil { return "", err @@ -125,7 +127,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) { terminal.CursorHide() // ask the question terminal.Println(out) - for range s.Choices { + for range s.Options { terminal.Println() } // start waiting for input @@ -141,7 +143,7 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) { val = s.Default } else { // there is no default value so use the first - val = s.Choices[0] + val = s.Options[0] } } @@ -149,18 +151,18 @@ func (s *Choice) Prompt(rl *readline.Instance) (string, error) { return val, err } -func (s *Choice) Cleanup(rl *readline.Instance, val string) error { +func (s *Select) Cleanup(rl *readline.Instance, val interface{}) error { terminal.CursorPreviousLine(1) - terminal.EraseInLine(1) - for range s.Choices { + terminal.EraseInLine(0) + for range s.Options { terminal.CursorPreviousLine(1) - terminal.EraseInLine(1) + terminal.EraseInLine(0) } // execute the output summary template with the answer - output, err := RunTemplate( + output, err := core.RunTemplate( SelectQuestionTemplate, - SelectTemplateData{Select: *s, Answer: val}, + SelectTemplateData{Select: *s, Answer: val.(string)}, ) if err != nil { return err diff --git a/choice_test.go b/select_test.go similarity index 72% rename from choice_test.go rename to select_test.go index d4478c91..861f0045 100644 --- a/choice_test.go +++ b/select_test.go @@ -2,29 +2,31 @@ package survey import ( "testing" + + "github.com/AlecAivazis/survey/core" ) func init() { // disable color output for all prompts to simplify testing - DisableColor = true + core.DisableColor = true } func TestCanFormatSelectOptions(t *testing.T) { - prompt := &Choice{ - Choices: []string{"foo", "bar", "baz", "buz"}, + prompt := &Select{ + Options: []string{"foo", "bar", "baz", "buz"}, Default: "baz", } // TODO: figure out a way for the test to actually test this bit of code - prompt.SelectedIndex = 2 + prompt.selectedIndex = 2 - actual, err := RunTemplate( + actual, err := core.RunTemplate( SelectChoicesTemplate, - SelectTemplateData{Select: *prompt}, + SelectTemplateData{Select: *prompt, SelectedIndex: 2}, ) if err != nil { - t.Errorf("Failed to run template to format choice choices: %s", err) + t.Errorf("Failed to run template to format choice options: %s", err) } expected := ` foo @@ -40,13 +42,13 @@ func TestCanFormatSelectOptions(t *testing.T) { func TestSelectFormatQuestion(t *testing.T) { - prompt := &Choice{ + prompt := &Select{ Message: "Pick your word:", - Choices: []string{"foo", "bar", "baz", "buz"}, + Options: []string{"foo", "bar", "baz", "buz"}, Default: "baz", } - actual, err := RunTemplate( + actual, err := core.RunTemplate( SelectQuestionTemplate, SelectTemplateData{Select: *prompt}, ) @@ -63,13 +65,13 @@ func TestSelectFormatQuestion(t *testing.T) { func TestSelectFormatAnswer(t *testing.T) { - prompt := &Choice{ + prompt := &Select{ Message: "Pick your word:", - Choices: []string{"foo", "bar", "baz", "buz"}, + Options: []string{"foo", "bar", "baz", "buz"}, Default: "baz", } - actual, err := RunTemplate( + actual, err := core.RunTemplate( SelectQuestionTemplate, SelectTemplateData{Select: *prompt, Answer: "buz"}, ) diff --git a/survey.go b/survey.go index d1c20d90..6639176c 100644 --- a/survey.go +++ b/survey.go @@ -1,15 +1,16 @@ package survey import ( + "errors" "fmt" - "os" - "github.com/alecaivazis/survey/terminal" + "github.com/AlecAivazis/survey/core" + "github.com/AlecAivazis/survey/terminal" "github.com/chzyer/readline" ) // Validator is a function passed to a Question in order to redefine -type Validator func(string) error +type Validator func(interface{}) error // Question is the core data structure for a survey questionnaire. type Question struct { @@ -21,61 +22,53 @@ type Question struct { // Prompt is the primary interface for the objects that can take user input // and return a string value. type Prompt interface { - Prompt(*readline.Instance) (string, error) - Cleanup(*readline.Instance, string) error + Prompt(*readline.Instance) (interface{}, error) + Cleanup(*readline.Instance, interface{}) error } var ErrorTemplate = `{{color "red"}}✘ Sorry, your reply was invalid: {{.Error}}{{color "reset"}} ` // AskOne asks a single question without performing validation on the answer. -func AskOne(p Prompt) (string, error) { - answers, err := Ask([]*Question{{Name: "q1", Prompt: p}}) +func AskOne(p Prompt, t interface{}, v Validator) error { + err := Ask([]*Question{{Prompt: p, Validate: v}}, t) if err != nil { - return "", err + return err } - return answers["q1"], nil -} - -// AskOneValidate asks a single question and validates the answer with v. -func AskOneValidate(p Prompt, v Validator) (string, error) { - answers, err := Ask([]*Question{{Name: "q1", Prompt: p, Validate: v}}) - return answers["q1"], err -} -func handleError(err error) { - // tell the user what happened - fmt.Println(err.Error()) - // quit the survey - os.Exit(1) + return nil } // Ask performs the prompt loop -func Ask(qs []*Question) (map[string]string, error) { +func Ask(qs []*Question, t interface{}) error { // grab the readline instance rl, err := terminal.GetReadline() if err != nil { - handleError(err) + return err + } + + // if we weren't passed a place to record the answers + if t == nil { + // we can't go any further + return errors.New("cannot call Ask() with a nil reference to record the answers") } - // the response map - res := make(map[string]string) // go over every question for _, q := range qs { // grab the user input and save it ans, err := q.Prompt.Prompt(rl) // if there was a problem if err != nil { - handleError(err) + return err } // if there is a validate handler for this question if q.Validate != nil { // wait for a valid response for invalid := q.Validate(ans); invalid != nil; invalid = q.Validate(ans) { - out, err := RunTemplate(ErrorTemplate, invalid) + out, err := core.RunTemplate(ErrorTemplate, invalid) if err != nil { - return nil, err + return err } // send the message to the user fmt.Print(out) @@ -83,7 +76,7 @@ func Ask(qs []*Question) (map[string]string, error) { ans, err = q.Prompt.Prompt(rl) // if there was a problem if err != nil { - handleError(err) + return err } } } @@ -94,11 +87,16 @@ func Ask(qs []*Question) (map[string]string, error) { // if something went wrong if err != nil { // stop listening - return nil, err + return err } + // add it to the map - res[q.Name] = ans + err = core.WriteAnswer(t, q.Name, ans) + // if something went wrong + if err != nil { + return err + } } // return the response - return res, nil + return nil } diff --git a/survey_test.go b/survey_test.go index dc8219d8..e59b51f4 100644 --- a/survey_test.go +++ b/survey_test.go @@ -3,18 +3,20 @@ package survey import ( "fmt" "testing" + + "github.com/AlecAivazis/survey/core" ) func init() { // disable color output for all prompts to simplify testing - DisableColor = true + core.DisableColor = true } func TestValidationError(t *testing.T) { err := fmt.Errorf("Football is not a valid month") - actual, err := RunTemplate( + actual, err := core.RunTemplate( ErrorTemplate, err, ) @@ -29,3 +31,14 @@ func TestValidationError(t *testing.T) { t.Errorf("Formatted error was not formatted correctly. Found:\n%s\nExpected:\n%s", actual, expected) } } + +func TestAsk_returnsErrorIfTargetIsNil(t *testing.T) { + // pass an empty place to leave the answers + err := Ask([]*Question{}, nil) + + // if we didn't get an error + if err == nil { + // the test failed + t.Error("Did not encounter error when asking with no where to record.") + } +} diff --git a/tests/ask.go b/tests/ask.go index af179cb2..dcd74d0e 100644 --- a/tests/ask.go +++ b/tests/ask.go @@ -2,7 +2,8 @@ package main import ( "fmt" - "github.com/alecaivazis/survey" + + "github.com/AlecAivazis/survey" ) // the questions to ask @@ -16,9 +17,9 @@ var simpleQs = []*survey.Question{ }, { Name: "color", - Prompt: &survey.Choice{ + Prompt: &survey.Select{ Message: "Choose a color:", - Choices: []string{"red", "blue", "green", "yellow"}, + Options: []string{"red", "blue", "green", "yellow"}, Default: "yellow", }, Validate: survey.Required, @@ -28,16 +29,20 @@ var simpleQs = []*survey.Question{ func main() { fmt.Println("Asking many.") - - answers, err := survey.Ask(simpleQs) + // a place to store the answers + ans := struct { + Name string + Color string + }{} + err := survey.Ask(simpleQs, &ans) if err != nil { fmt.Println(err.Error()) return } - fmt.Printf("%s chose %s.\n", answers["name"], answers["color"]) fmt.Println("Asking one.") - answer, err := survey.AskOne(simpleQs[0].Prompt) + answer := "" + err = survey.AskOne(simpleQs[0].Prompt, &answer, nil) if err != nil { fmt.Println(err.Error()) return @@ -45,10 +50,11 @@ func main() { fmt.Printf("Answered with %v.\n", answer) fmt.Println("Asking one with validation.") - answer, err = survey.AskOneValidate(&survey.Input{"What is your name?", ""}, survey.Required) + vAns := "" + err = survey.AskOne(&survey.Input{"What is your name?", ""}, &vAns, survey.Required) if err != nil { fmt.Println(err.Error()) return } - fmt.Printf("Answered with %v.\n", answer) + fmt.Printf("Answered with %v.\n", vAns) } diff --git a/tests/confirm.go b/tests/confirm.go new file mode 100644 index 00000000..386b0a57 --- /dev/null +++ b/tests/confirm.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/AlecAivazis/survey" + "github.com/AlecAivazis/survey/tests/util" +) + +var answer = false + +var goodTable = []TestUtil.TestTableEntry{ + { + "Enter 'yes'", &survey.Confirm{ + Message: "yes:", + }, &answer, + }, + { + "Enter 'no'", &survey.Confirm{ + Message: "yes:", + }, &answer, + }, + { + "default", &survey.Confirm{ + Message: "yes:", + Default: true, + }, &answer, + }, + { + "not recognized (enter random letter)", &survey.Confirm{ + Message: "yes:", + Default: true, + }, &answer, + }, +} + +func main() { + TestUtil.RunTable(goodTable) +} diff --git a/tests/input.go b/tests/input.go index 9332ccc0..4c76189b 100644 --- a/tests/input.go +++ b/tests/input.go @@ -1,16 +1,18 @@ package main import ( - "github.com/alecaivazis/survey" - "github.com/alecaivazis/survey/tests/util" + "github.com/AlecAivazis/survey" + "github.com/AlecAivazis/survey/tests/util" ) +var val = "" + var table = []TestUtil.TestTableEntry{ { - "no default", &survey.Input{"Hello world", ""}, + "no default", &survey.Input{"Hello world", ""}, &val, }, { - "default", &survey.Input{"Hello world", "default"}, + "default", &survey.Input{"Hello world", "default"}, &val, }, } diff --git a/tests/multiselect.go b/tests/multiselect.go new file mode 100644 index 00000000..8d2d6926 --- /dev/null +++ b/tests/multiselect.go @@ -0,0 +1,35 @@ +package main + +import ( + "github.com/AlecAivazis/survey" + "github.com/AlecAivazis/survey/tests/util" +) + +var answer = []string{} + +var table = []TestUtil.TestTableEntry{ + { + "standard", &survey.MultiSelect{ + Message: "What days do you prefer:", + Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, + }, &answer, + }, + { + "default (sunday, tuesday)", &survey.MultiSelect{ + Message: "What days do you prefer:", + Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, + Default: []string{"Sunday", "Tuesday"}, + }, &answer, + }, + { + "default not found", &survey.MultiSelect{ + Message: "What days do you prefer:", + Options: []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}, + Default: []string{"Sundayaa"}, + }, &answer, + }, +} + +func main() { + TestUtil.RunTable(table) +} diff --git a/tests/password.go b/tests/password.go index 92fba2b7..e37b293b 100644 --- a/tests/password.go +++ b/tests/password.go @@ -1,16 +1,18 @@ package main import ( - "github.com/alecaivazis/survey" - "github.com/alecaivazis/survey/tests/util" + "github.com/AlecAivazis/survey" + "github.com/AlecAivazis/survey/tests/util" ) +var value = "" + var table = []TestUtil.TestTableEntry{ { - "standard", &survey.Password{"Please type your password:"}, + "standard", &survey.Password{"Please type your password:"}, &value, }, { - "please make sure paste works", &survey.Password{"Please paste your password:"}, + "please make sure paste works", &survey.Password{"Please paste your password:"}, &value, }, } diff --git a/tests/select.go b/tests/select.go index 6d62dd68..7a84a1eb 100644 --- a/tests/select.go +++ b/tests/select.go @@ -1,43 +1,45 @@ package main import ( - "github.com/alecaivazis/survey" - "github.com/alecaivazis/survey/tests/util" + "github.com/AlecAivazis/survey" + "github.com/AlecAivazis/survey/tests/util" ) +var answer = "" + var goodTable = []TestUtil.TestTableEntry{ { - "standard", &survey.Choice{ + "standard", &survey.Select{ Message: "Choose a color:", - Choices: []string{"red", "blue", "green"}, - }, + Options: []string{"red", "blue", "green"}, + }, &answer, }, { - "short", &survey.Choice{ + "short", &survey.Select{ Message: "Choose a color:", - Choices: []string{"red", "blue"}, - }, + Options: []string{"red", "blue"}, + }, &answer, }, { - "default", &survey.Choice{ + "default", &survey.Select{ Message: "Choose a color (should default blue):", - Choices: []string{"red", "blue", "green"}, + Options: []string{"red", "blue", "green"}, Default: "blue", - }, + }, &answer, }, { - "one", &survey.Choice{ + "one", &survey.Select{ Message: "Choose one:", - Choices: []string{"hello"}, - }, + Options: []string{"hello"}, + }, &answer, }, } var badTable = []TestUtil.TestTableEntry{ { - "no Choices", &survey.Choice{ + "no options", &survey.Select{ Message: "Choose one:", - }, + }, &answer, }, } diff --git a/tests/util/test.go b/tests/util/test.go index f70282a1..5b490206 100644 --- a/tests/util/test.go +++ b/tests/util/test.go @@ -2,18 +2,20 @@ package TestUtil import ( "fmt" + "reflect" - "github.com/alecaivazis/survey" + "github.com/AlecAivazis/survey" ) type TestTableEntry struct { Name string Prompt survey.Prompt + Value interface{} } -func formatAnswer(ans string) { +func formatAnswer(ans interface{}) { // show the answer to the user - fmt.Printf("Answered %v.\n", ans) + fmt.Printf("Answered %v.\n", reflect.ValueOf(ans).Elem()) fmt.Println("---------------------") } @@ -23,13 +25,13 @@ func RunTable(table []TestTableEntry) { // tell the user what we are going to ask them fmt.Println(entry.Name) // perform the ask - answer, err := survey.AskOne(entry.Prompt) + err := survey.AskOne(entry.Prompt, entry.Value, nil) if err != nil { fmt.Printf("AskOne on %v's prompt failed: %v.", entry.Name, err.Error()) break } // show the answer to the user - formatAnswer(answer) + formatAnswer(entry.Value) } } @@ -39,7 +41,7 @@ func RunErrorTable(table []TestTableEntry) { // tell the user what we are going to ask them fmt.Println(entry.Name) // perform the ask - _, err := survey.AskOne(entry.Prompt) + err := survey.AskOne(entry.Prompt, entry.Value, nil) if err == nil { fmt.Printf("AskOne on %v's prompt didn't fail.", entry.Name) break diff --git a/validate.go b/validate.go index d4693409..6e275beb 100644 --- a/validate.go +++ b/validate.go @@ -3,27 +3,33 @@ package survey import ( "errors" "fmt" + "reflect" ) // Required does not allow an empty value -func Required(str string) error { - // if the string is empty - if str == "" { - // return the error - return errors.New("Value is required.") +func Required(val interface{}) error { + // if the value passed in is the zero value of the appropriate type + if val == reflect.Zero(reflect.TypeOf(val)).Interface() { + return errors.New("Value is required") } - // nothing was wrong return nil } // MaxLength requires that the string is no longer than the specified value func MaxLength(length int) Validator { // return a validator that checks the length of the string - return func(str string) error { - // if the string is longer than the given value - if len(str) > length { - return fmt.Errorf("Value is too long. Max length is %v.", length) + return func(val interface{}) error { + if str, ok := val.(string); ok { + // if the string is longer than the given value + if len(str) > length { + // yell loudly + return fmt.Errorf("value is too long. Max length is %v", length) + } + } else { + // otherwise we cannot convert the value into a string and cannot enforce length + return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name()) } + // the input is fine return nil } @@ -32,11 +38,18 @@ func MaxLength(length int) Validator { // MinLength requires that the string is longer or equal in length to the specified value func MinLength(length int) Validator { // return a validator that checks the length of the string - return func(str string) error { - // if the string is longer than the given value - if len(str) < length { - return fmt.Errorf("Value is too short. Min length is %v.", length) + return func(val interface{}) error { + if str, ok := val.(string); ok { + // if the string is shorter than the given value + if len(str) < length { + // yell loudly + return fmt.Errorf("value is too short. Min length is %v", length) + } + } else { + // otherwise we cannot convert the value into a string and cannot enforce length + return fmt.Errorf("cannot enforce length on response of type %v", reflect.TypeOf(val).Name()) } + // the input is fine return nil } @@ -45,11 +58,11 @@ func MinLength(length int) Validator { // ComposeValidators is a variadic function used to create one validator from many. func ComposeValidators(validators ...Validator) Validator { // return a validator that calls each one sequentially - return func(str string) error { + return func(val interface{}) error { // execute each validator for _, validator := range validators { // if the string is not valid - if err := validator(str); err != nil { + if err := validator(val); err != nil { // return the error return err } diff --git a/validate_test.go b/validate_test.go index ed5f0369..601363c0 100644 --- a/validate_test.go +++ b/validate_test.go @@ -44,14 +44,26 @@ func TestMaxLength(t *testing.T) { } func TestMinLength(t *testing.T) { - // the string to test - testStr := randString(10) // validate the string - if err := MinLength(12)(testStr); err == nil { + if err := MinLength(12)(randString(10)); err == nil { t.Error("No error returned with input less than 12 characters.") } } +func TestMinLengthOnInt(t *testing.T) { + // validate the string + if err := MinLength(12)(1); err == nil { + t.Error("No error returned when enforcing length on int.") + } +} + +func TestMaxLengthOnInt(t *testing.T) { + // validate the string + if err := MaxLength(12)(1); err == nil { + t.Error("No error returned when enforcing length on int.") + } +} + func TestComposeValidatorsPasses(t *testing.T) { // create a validator that requires a string of no more than 10 characters valid := ComposeValidators(