Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd(core): prompt improvements #663

Merged
merged 3 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 73 additions & 85 deletions cmd/core/utils/input/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,20 @@ package input
import (
"errors"
"fmt"
"io"
"os"
"os/signal"
"strings"
"syscall"

"github.com/ubclaunchpad/inertia/cfg"
"github.com/ubclaunchpad/inertia/cmd/core/utils/out"
)

var (
errInvalidInput = errors.New("invalid input")
errEmptyInput = errors.New("empty input")

errInvalidUser = errors.New("invalid user")
errInvalidAddress = errors.New("invalid IP address")
errInvalidBuildType = errors.New("invalid build type")
errInvalidBuildFilePath = errors.New("invalid buildfile path")
// ErrEmptyInput is returned on empty imputs - toggle with AllowEmpty
ErrEmptyInput = errors.New("empty input")
// ErrInvalidInput is returned on disallowed inputs - toggle with AllowInvalid
ErrInvalidInput = errors.New("invalid input")
)

// CatchSigterm listens in the background for some kind of interrupt and calls
Expand All @@ -33,102 +30,93 @@ func CatchSigterm(cancelFunc func()) {
}()
}

// Prompt prints the given query and reads the response
func Prompt(query ...interface{}) (string, error) {
out.Println(query...)
var response string
if _, err := fmt.Fscanln(os.Stdin, &response); err != nil {
if strings.Contains(err.Error(), "unexpected newline") {
return "", nil
}
return "", err
}
return response, nil
// PromptConfig offers prompt configuration
type PromptConfig struct {
AllowEmpty bool
AllowInvalid bool
}

// Promptf prints the given query and reads the response
func Promptf(query string, args ...interface{}) (string, error) {
out.Printf(query+"\n", args...)
var response string
if _, err := fmt.Fscanln(os.Stdin, &response); err != nil {
return "", err
}
return response, nil
// PromptInteraction is a builder for interactions - use .PromptX followed by .GetX
type PromptInteraction struct {
in io.Reader
conf PromptConfig
resp string
err error
}

// AddProjectWalkthrough is the command line walkthrough that asks for details
// about the project the user intends to deploy
func AddProjectWalkthrough() (
buildType cfg.BuildType, buildFilePath string, err error,
) {
out.Println(out.C("Please enter the path to your build configuration file:", out.CY))
out.Println(" - docker-compose")
out.Println(" - dockerfile")

var response string
if _, err = fmt.Fscanln(os.Stdin, &response); err != nil {
return "", "", errInvalidBuildType
}
buildType, err = cfg.AsBuildType(response)
if err != nil {
return "", "", err
}
// NewPrompt instantiates a new prompt interaction on standard in
func NewPrompt(conf *PromptConfig) *PromptInteraction { return NewPromptOnInput(os.Stdin, conf) }

buildFilePath, err = Prompt(
out.C("Please enter the path to your build configuration file:", out.CY).String(),
)
if err != nil || buildFilePath == "" {
return "", "", errInvalidBuildFilePath
// NewPromptOnInput instantiates a new prompt on specified input
func NewPromptOnInput(in io.Reader, conf *PromptConfig) *PromptInteraction {
if conf == nil {
conf = &PromptConfig{}
}
return
return &PromptInteraction{in: in, conf: *conf}
}

// EnterEC2CredentialsWalkthrough prints promts to stdout and reads input from
// given reader
func EnterEC2CredentialsWalkthrough() (id, key string, err error) {
out.Print(`To get your credentials:
1. Open the IAM console (https://console.aws.amazon.com/iam/home?#home).
2. In the navigation pane of the console, choose Users. You may have to create a user.
3. Choose your IAM user name (not the check box).
4. Choose the Security credentials tab and then choose Create access key.
5. To see the new access key, choose Show. Your credentials will look something like this:

Access key ID: AKIAIOSFODNN7EXAMPLE
Secret access key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
`)

func (p *PromptInteraction) parse() {
var response string

out.Print("\nKey ID: ")
_, err = fmt.Fscanln(os.Stdin, &response)
if err != nil {
return
if _, err := fmt.Fscanln(p.in, &response); err != nil {
if strings.Contains(err.Error(), "unexpected newline") {
if !p.conf.AllowEmpty {
p.err = errors.New("empty response not allowed")
}
} else {
p.err = err
}
} else {
p.resp = response
}
id = response
}

out.Print("\nAccess Key: ")
_, err = fmt.Fscanln(os.Stdin, &response)
if err != nil {
return
}
key = response
return
// Prompt prints the given query and reads the response
func (p *PromptInteraction) Prompt(query ...interface{}) *PromptInteraction {
out.Println(query...)
p.parse()
return p
}

// ChooseFromListWalkthrough prints given options and reads in a choice from
// the given reader
func ChooseFromListWalkthrough(optionName string, options []string) (string, error) {
// Promptf prints the given query and reads the response
func (p *PromptInteraction) Promptf(query string, args ...interface{}) *PromptInteraction {
out.Printf(query+"\n", args...)
p.parse()
return p
}

// PromptFromList creates a choose-one-from-x prompt
func (p *PromptInteraction) PromptFromList(optionName string, options []string) *PromptInteraction {
out.Printf("Available %ss:\n", optionName)
for _, o := range options {
out.Println(" > " + o)
}
out.Print(out.C("Please enter your desired %s: ", out.CY).With(optionName))
p.parse()

// check option is valid
if p.err == nil && !p.conf.AllowInvalid {
for _, o := range options {
if o == p.resp {
return p
}
}
p.err = fmt.Errorf("illegal option '%s' chosen: %w", p.resp, ErrInvalidInput)
}
return p
}

var response string
_, err := fmt.Fscanln(os.Stdin, &response)
if err != nil {
return "", errInvalidInput
// GetBool retrieves a boolean response based on "y" or "yes"
func (p *PromptInteraction) GetBool() (bool, error) {
yes := p.resp == "y"
if !yes && !p.conf.AllowInvalid {
if p.resp != "N" && p.resp != "" {
return false, fmt.Errorf("illegal input '%s' provided: %w", p.resp, ErrInvalidInput)
}
}
return yes, p.err
}

return response, nil
// GetString retreives the raw string response from the prompt
func (p *PromptInteraction) GetString() (string, error) {
return p.resp, p.err
}
109 changes: 46 additions & 63 deletions cmd/core/utils/input/input_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,99 +5,80 @@ import (
"io"
"io/ioutil"
"os"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/ubclaunchpad/inertia/cfg"
)

func Test_addProjectWalkthrough(t *testing.T) {
func TestPromptInteraction_GetBool(t *testing.T) {
type fields struct {
conf PromptConfig
in string
}
tests := []struct {
name string
wantBuildType cfg.BuildType
wantBuildFilePath string
wantErr bool
name string
fields fields
want bool
wantErr bool
}{
{"invalid build type", "", "", true},
{"invalid build file path", "dockerfile", "", true},
{"docker-compose", "docker-compose", "docker-compose.yml", false},
{"y", fields{PromptConfig{}, "y\n"}, true, false},
{"N", fields{PromptConfig{}, "N\n"}, false, false},

{"disallowed empty", fields{PromptConfig{}, "\n"}, false, true},
{"allowed empty", fields{PromptConfig{AllowEmpty: true}, "\n"}, false, false},
{"disallowed invalid", fields{PromptConfig{}, "asdf\n"}, false, true},
{"allowed invalid", fields{PromptConfig{AllowInvalid: true}, "asdf\n"}, false, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
in, err := ioutil.TempFile("", "")
assert.NoError(t, err)
defer in.Close()

fmt.Fprintln(in, tt.wantBuildType)
fmt.Fprintln(in, tt.wantBuildFilePath)

_, err = in.Seek(0, io.SeekStart)
assert.NoError(t, err)

var old = os.Stdin
os.Stdin = in
defer func() { os.Stdin = old }()
gotBuildType, gotBuildFilePath, err := AddProjectWalkthrough()
got, err := NewPromptOnInput(strings.NewReader(tt.fields.in), &tt.fields.conf).
Prompt("test prompt (y/N)").
GetBool()
if (err != nil) != tt.wantErr {
t.Errorf("addProjectWalkthrough() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("PromptInteraction.GetBool() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if gotBuildType != tt.wantBuildType {
t.Errorf("addProjectWalkthrough() gotBuildType = %v, want %v", gotBuildType, tt.wantBuildType)
}
if gotBuildFilePath != tt.wantBuildFilePath {
t.Errorf("addProjectWalkthrough() gotBuildFilePath = %v, want %v", gotBuildFilePath, tt.wantBuildFilePath)
}
if got != tt.want {
t.Errorf("PromptInteraction.GetBool() = %v, want %v", got, tt.want)
}
})
}
}

func Test_enterEC2CredentialsWalkthrough(t *testing.T) {
func TestPromptInteraction_GetString(t *testing.T) {
type fields struct {
conf PromptConfig
in string
}
tests := []struct {
name string
wantID string
wantKey string
fields fields
want string
wantErr bool
}{
{"bad ID", "", "asdf", true},
{"bad key", "asdf", "", true},
{"good", "asdf", "asdf", false},
{"arbitrary string", fields{PromptConfig{}, "hello\n"}, "hello", false},

{"disallowed empty", fields{PromptConfig{}, "\n"}, "", true},
{"allowed empty", fields{PromptConfig{AllowEmpty: true}, "\n"}, "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
in, err := ioutil.TempFile("", "")
assert.NoError(t, err)
defer in.Close()

fmt.Fprintln(in, tt.wantID)
fmt.Fprintln(in, tt.wantKey)

_, err = in.Seek(0, io.SeekStart)
assert.NoError(t, err)

var old = os.Stdin
os.Stdin = in
defer func() { os.Stdin = old }()
gotID, gotKey, err := EnterEC2CredentialsWalkthrough()
got, err := NewPromptOnInput(strings.NewReader(tt.fields.in), &tt.fields.conf).
Promptf("test prompt %s", "hello").
GetString()
if (err != nil) != tt.wantErr {
t.Errorf("enterEC2CredentialsWalkthrough() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("PromptInteraction.GetString() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if gotID != tt.wantID {
t.Errorf("enterEC2CredentialsWalkthrough() gotId = %v, want %v", gotID, tt.wantID)
}
if gotKey != tt.wantKey {
t.Errorf("enterEC2CredentialsWalkthrough() gotKey = %v, want %v", gotKey, tt.wantKey)
}
if got != tt.want {
t.Errorf("PromptInteraction.GetString() = %v, want %v", got, tt.want)
}
})
}
}

func Test_chooseFromListWalkthrough(t *testing.T) {
func TestPromptInteraction_PromptFromList(t *testing.T) {
type args struct {
optionName string
options []string
Expand Down Expand Up @@ -125,14 +106,16 @@ func Test_chooseFromListWalkthrough(t *testing.T) {
os.Stdin = in
defer func() { os.Stdin = old }()
t.Run(tt.name, func(t *testing.T) {
got, err := ChooseFromListWalkthrough(tt.args.optionName, tt.args.options)
got, err := NewPrompt(nil).
PromptFromList(tt.args.optionName, tt.args.options).
GetString()
if (err != nil) != tt.wantErr {
t.Errorf("chooseFromListWalkthrough() error = %v, wantErr %v", err, tt.wantErr)
t.Errorf("PromptFromList() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if got != tt.want {
t.Errorf("chooseFromListWalkthrough() = %v, want %v", got, tt.want)
t.Errorf("PromptFromList() = %v, want %v", got, tt.want)
}
}
})
Expand Down
Loading