diff --git a/cmd/main.go b/cmd/main.go index 4a23c2d41..a5fb1cc97 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -141,7 +141,7 @@ func buildCommands() *cobra.Command { formulaLocalBuilder := builder.NewBuildLocal(ritchieHomeDir, dirManager, fileManager, treeGen) postRunner := runner.NewPostRunner(fileManager, dirManager) - inputManager := runner.NewInput(envResolvers, fileManager, inputList, inputText, inputBool, inputPassword) + inputManager := runner.NewInput(envResolvers, fileManager, inputList, inputText, inputTextValidator, inputBool, inputPassword) formulaLocalPreRun := local.NewPreRun(ritchieHomeDir, formBuildMake, formBuildBat, formBuildSh, dirManager, fileManager) formulaLocalRun := local.NewRunner(postRunner, inputManager, formulaLocalPreRun, fileManager, ctxFinder, userHomeDir) diff --git a/pkg/formula/formula.go b/pkg/formula/formula.go index f56cd3752..03f8adc66 100644 --- a/pkg/formula/formula.go +++ b/pkg/formula/formula.go @@ -49,10 +49,15 @@ type ( Items []string `json:"items"` Cache Cache `json:"cache"` Condition Condition `json:"condition"` + Pattern Pattern `json:"pattern"` Tutorial string `json:"tutorial"` Required *bool `json:"required"` } + Pattern struct { + Regex string `json:"regex"` + MismatchText string `json:"mismatchText"` + } Cache struct { Active bool `json:"active"` Qty int `json:"qty"` diff --git a/pkg/formula/runner/docker/runner_test.go b/pkg/formula/runner/docker/runner_test.go index d7b2f0513..f2584e068 100644 --- a/pkg/formula/runner/docker/runner_test.go +++ b/pkg/formula/runner/docker/runner_test.go @@ -52,7 +52,7 @@ func TestRun(t *testing.T) { ctxFinder := rcontext.NewFinder(ritHome, fileManager) preRunner := NewPreRun(ritHome, dockerBuilder, dirManager, fileManager) postRunner := runner.NewPostRunner(fileManager, dirManager) - inputRunner := runner.NewInput(env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, fileManager, inputMock{}, inputMock{}, inputMock{}, inputMock{}) + inputRunner := runner.NewInput(env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, fileManager, inputMock{}, inputMock{}, inputTextValidatorMock{str: "test"}, inputMock{}, inputMock{}) type in struct { def formula.Definition @@ -220,6 +220,14 @@ func (e envResolverMock) Resolve(string) (string, error) { return e.in, e.err } +type inputTextValidatorMock struct { + str string +} + +func (i inputTextValidatorMock) Text(name string, validate func(interface{}) error, helper ...string) (string, error) { + return i.str, nil +} + type inputMock struct { text string boolean bool diff --git a/pkg/formula/runner/inputs.go b/pkg/formula/runner/inputs.go index d5e9a1203..2e62aece6 100644 --- a/pkg/formula/runner/inputs.go +++ b/pkg/formula/runner/inputs.go @@ -18,8 +18,10 @@ package runner import ( "encoding/json" + "errors" "fmt" "os/exec" + "regexp" "strconv" "strings" @@ -45,6 +47,7 @@ type InputManager struct { file stream.FileWriteReadExister prompt.InputList prompt.InputText + prompt.InputTextValidator prompt.InputBool prompt.InputPassword } @@ -54,16 +57,18 @@ func NewInput( file stream.FileWriteReadExister, inList prompt.InputList, inText prompt.InputText, + inTextValidator prompt.InputTextValidator, inBool prompt.InputBool, inPass prompt.InputPassword, ) formula.InputRunner { return InputManager{ - envResolvers: env, - file: file, - InputList: inList, - InputText: inText, - InputBool: inBool, - InputPassword: inPass, + envResolvers: env, + file: file, + InputList: inList, + InputText: inText, + InputTextValidator: inTextValidator, + InputBool: inBool, + InputPassword: inPass, } } @@ -134,12 +139,7 @@ func (in InputManager) fromPrompt(cmd *exec.Cmd, setup formula.Setup) error { if items != nil { inputVal, err = in.loadInputValList(items, input) } else { - validate := isRequired(input) - inputVal, err = in.Text(input.Label, validate, input.Tutorial) - - if inputVal == "" { - inputVal = input.Default - } + inputVal, err = in.textValidator(input) } case "bool": valBool, err = in.Bool(input.Label, items, input.Tutorial) @@ -207,14 +207,12 @@ func (in InputManager) loadInputValList(items []string, input formula.Input) (st } items = append(items, newLabel) } + inputVal, err := in.List(input.Label, items, input.Tutorial) if inputVal == newLabel { - validate := isRequired(input) - inputVal, err = in.Text(input.Label, validate, input.Tutorial) - if len(inputVal) == 0 { - inputVal = input.Default - } + return in.textValidator(input) } + return inputVal, err } @@ -256,6 +254,24 @@ func (in InputManager) resolveIfReserved(input formula.Input) (string, error) { return "", nil } +func (in InputManager) textValidator(input formula.Input) (string, error) { + required := isRequired(input) + var inputVal string + var err error + + if in.hasRegex(input) { + inputVal, err = in.textRegexValidator(input, required) + } else { + inputVal, err = in.InputText.Text(input.Label, required, input.Tutorial) + } + + if inputVal == "" { + inputVal = input.Default + } + + return inputVal, err +} + func isRequired(input formula.Input) bool { if input.Required == nil { return input.Default == "" @@ -305,3 +321,18 @@ func (in InputManager) verifyConditional(cmd *exec.Cmd, input formula.Input) (bo ) } } + +func (in InputManager) hasRegex(input formula.Input) bool { + return len(input.Pattern.Regex) > 0 +} + +func (in InputManager) textRegexValidator(input formula.Input, required bool) (string, error) { + return in.InputTextValidator.Text(input.Label, func(text interface{}) error { + re := regexp.MustCompile(input.Pattern.Regex) + if re.MatchString(text.(string)) || (!required && text.(string) == "") { + return nil + } + + return errors.New(input.Pattern.MismatchText) + }) +} diff --git a/pkg/formula/runner/inputs_test.go b/pkg/formula/runner/inputs_test.go index b99fc0127..0effca29c 100644 --- a/pkg/formula/runner/inputs_test.go +++ b/pkg/formula/runner/inputs_test.go @@ -111,14 +111,15 @@ func TestInputManager_Inputs(t *testing.T) { fileManager := stream.NewFileManager() type in struct { - iText inputMock - iList inputMock - iBool inputMock - iPass inputMock - inType api.TermInputType - creResolver env.Resolvers - file stream.FileWriteReadExister - stdin string + iText inputMock + iTextValidator inputTextValidatorMock + iList inputMock + iBool inputMock + iPass inputMock + inType api.TermInputType + creResolver env.Resolvers + file stream.FileWriteReadExister + stdin string } tests := []struct { @@ -129,146 +130,157 @@ func TestInputManager_Inputs(t *testing.T) { { name: "success stdin", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Stdin, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - stdin: `{"sample_text":"test_text","sample_list":"test_list","sample_bool": false}`, - file: fileManager, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Stdin, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + stdin: `{"sample_text":"test_text","sample_list":"test_list","sample_bool": false}`, + file: fileManager, }, want: nil, }, { name: "error stdin", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Stdin, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - stdin: `"sample_text"`, - file: fileManager, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Stdin, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + stdin: `"sample_text"`, + file: fileManager, }, want: stdin.ErrInvalidInput, }, { name: "success prompt", in: in{ - iText: inputMock{text: ""}, - iList: inputMock{text: "Type new value?"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Prompt, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - file: fileManager, + iText: inputMock{text: ""}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "Type new value?"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Prompt, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + file: fileManager, }, want: nil, }, { name: "error read file load items", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Prompt, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - file: fileManagerMock{rErr: errors.New("error to read file"), exist: true}, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Prompt, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + file: fileManagerMock{rErr: errors.New("error to read file"), exist: true}, }, want: errors.New("error to read file"), }, { name: "error unmarshal load items", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Prompt, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - file: fileManagerMock{rBytes: []byte("error"), exist: true}, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Prompt, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + file: fileManagerMock{rBytes: []byte("error"), exist: true}, }, want: errors.New("invalid character 'e' looking for beginning of value"), }, { name: "cache file doesn't exist success", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Prompt, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - file: fileManagerMock{exist: false}, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Prompt, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + file: fileManagerMock{exist: false}, }, want: nil, }, { name: "cache file doesn't exist error file write", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Prompt, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - file: fileManagerMock{wErr: errors.New("error to write file"), exist: false}, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Prompt, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + file: fileManagerMock{wErr: errors.New("error to write file"), exist: false}, }, want: errors.New("error to write file"), }, { name: "persist cache file write error", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Prompt, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - file: fileManagerMock{wErr: errors.New("error to write file"), rBytes: []byte(`["in_list1","in_list2"]`), exist: true}, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Prompt, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + file: fileManagerMock{wErr: errors.New("error to write file"), rBytes: []byte(`["in_list1","in_list2"]`), exist: true}, }, want: nil, }, { name: "error unknown prompt", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.TermInputType(3), - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, - file: fileManager, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.TermInputType(3), + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, + file: fileManager, }, want: ErrInputNotRecognized, }, { name: "error env resolver prompt", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Prompt, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test", err: errors.New("credential not found")}}, - file: fileManager, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Prompt, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test", err: errors.New("credential not found")}}, + file: fileManager, }, want: errors.New("credential not found"), }, { name: "error env resolver stdin", in: in{ - iText: inputMock{text: DefaultCacheNewLabel}, - iList: inputMock{text: "test"}, - iBool: inputMock{boolean: false}, - iPass: inputMock{text: "******"}, - inType: api.Stdin, - stdin: `{"sample_text":"test_text","sample_list":"test_list","sample_bool": false}`, - creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test", err: errors.New("credential not found")}}, - file: fileManager, + iText: inputMock{text: DefaultCacheNewLabel}, + iTextValidator: inputTextValidatorMock{}, + iList: inputMock{text: "test"}, + iBool: inputMock{boolean: false}, + iPass: inputMock{text: "******"}, + inType: api.Stdin, + stdin: `{"sample_text":"test_text","sample_list":"test_list","sample_bool": false}`, + creResolver: env.Resolvers{"CREDENTIAL": envResolverMock{in: "test", err: errors.New("credential not found")}}, + file: fileManager, }, want: errors.New("credential not found"), }, @@ -277,11 +289,12 @@ func TestInputManager_Inputs(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { iText := tt.in.iText + iTextValidator := tt.in.iTextValidator iList := tt.in.iList iBool := tt.in.iBool iPass := tt.in.iPass - inputManager := NewInput(tt.in.creResolver, tt.in.file, iList, iText, iBool, iPass) + inputManager := NewInput(tt.in.creResolver, tt.in.file, iList, iText, iTextValidator, iBool, iPass) cmd := &exec.Cmd{} if tt.in.inType == api.Stdin { @@ -421,11 +434,12 @@ func TestInputManager_ConditionalInputs(t *testing.T) { } iText := inputMock{text: DefaultCacheNewLabel} + iTextValidator := inputTextValidatorMock{} iList := inputMock{text: "in_list1"} iBool := inputMock{boolean: false} iPass := inputMock{text: "******"} - inputManager := NewInput(env.Resolvers{}, fileManager, iList, iText, iBool, iPass) + inputManager := NewInput(env.Resolvers{}, fileManager, iList, iText, iTextValidator, iBool, iPass) cmd := &exec.Cmd{} @@ -438,6 +452,121 @@ func TestInputManager_ConditionalInputs(t *testing.T) { } } +func TestInputManager_RegexType(t *testing.T) { + type in struct { + inputJson string + inText inputMock + iTextValidator inputTextValidatorMock + } + + tests := []struct { + name string + in in + want error + }{ + { + name: "Success regex test", + in: in{ + inputJson: `[ + { + "name": "sample_text", + "type": "text", + "label": "Type : ", + "pattern": { + "regex": "a|b", + "mismatchText": "mismatch" + } + } + ]`, + inText: inputMock{text: "a"}, + iTextValidator: inputTextValidatorMock{str: "a"}, + }, + want: nil, + }, + { + name: "Failed regex test", + in: in{ + inputJson: `[ + { + "name": "sample_text", + "type": "text", + "label": "Type : ", + "pattern": { + "regex": "c|d", + "mismatchText": "mismatch" + } + } + ]`, + inText: inputMock{text: "a"}, + iTextValidator: inputTextValidatorMock{str: "a"}, + }, + want: errors.New("Regex error, mismatch"), + }, + { + name: "Success regex test", + in: in{ + inputJson: `[ + { + "name": "sample_text", + "type": "text", + "label": "Type : ", + "pattern": { + "regex": "abcc", + "mismatchText": "mismatch" + } + } + ]`, + inText: inputMock{text: "abcc"}, + iTextValidator: inputTextValidatorMock{str: "abcc"}, + }, + want: nil, + }, + } + + fileManager := stream.NewFileManager() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var inputs []formula.Input + _ = json.Unmarshal([]byte(tt.in.inputJson), &inputs) + + setup := formula.Setup{ + Config: formula.Config{ + Inputs: inputs, + }, + FormulaPath: os.TempDir(), + } + + iText := tt.in.inText + iTextValidator := tt.in.iTextValidator + iList := inputMock{text: "in_list1"} + iBool := inputMock{boolean: false} + iPass := inputMock{text: "******"} + + inputManager := NewInput(env.Resolvers{}, fileManager, iList, iText, iTextValidator, iBool, iPass) + + cmd := &exec.Cmd{} + + got := inputManager.Inputs(cmd, setup, api.Prompt) + + if tt.want != nil && got == nil { + t.Errorf("Inputs regex(%s): got %v, want %v", tt.name, nil, tt.want) + } + + if tt.want == nil && got != nil { + t.Errorf("Inputs regex(%s): got %v, want %v", tt.name, got, nil) + } + }) + } +} + +type inputTextValidatorMock struct { + str string +} + +func (i inputTextValidatorMock) Text(name string, validate func(interface{}) error, helper ...string) (string, error) { + return i.str, validate(i.str) +} + type inputMock struct { text string boolean bool diff --git a/pkg/formula/runner/local/runner_test.go b/pkg/formula/runner/local/runner_test.go index a69f54317..2a7d5f8c8 100644 --- a/pkg/formula/runner/local/runner_test.go +++ b/pkg/formula/runner/local/runner_test.go @@ -54,7 +54,7 @@ func TestRun(t *testing.T) { ctxFinder := rcontext.NewFinder(ritHome, fileManager) preRunner := NewPreRun(ritHome, makeBuilder, batBuilder, shellBuilder, dirManager, fileManager) postRunner := runner.NewPostRunner(fileManager, dirManager) - inputRunner := runner.NewInput(env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, fileManager, inputMock{}, inputMock{}, inputMock{}, inputMock{}) + inputRunner := runner.NewInput(env.Resolvers{"CREDENTIAL": envResolverMock{in: "test"}}, fileManager, inputMock{}, inputMock{}, inputTextValidatorMock{}, inputMock{}, inputMock{}) type in struct { def formula.Definition @@ -194,6 +194,12 @@ func (e envResolverMock) Resolve(string) (string, error) { return e.in, e.err } +type inputTextValidatorMock struct{} + +func (inputTextValidatorMock) Text(name string, validate func(interface{}) error, helper ...string) (string, error) { + return "mocked text", nil +} + type inputMock struct { text string boolean bool