github.com/osievert/jfrog-cli-core@v1.2.7/artifactory/commands/utils/questionnaire.go (about) 1 package utils 2 3 import ( 4 "fmt" 5 "regexp" 6 "strconv" 7 "strings" 8 9 "github.com/c-bata/go-prompt" 10 11 "github.com/jfrog/jfrog-cli-core/utils/coreutils" 12 ) 13 14 const ( 15 InsertValuePromptMsg = "Insert the value for " 16 DummyDefaultAnswer = "-" 17 ) 18 19 // The interactive questionnaire works as follows: 20 // We have to provide a map of QuestionInfo which include all possible questions may be asked. 21 // 1. Mandatory Questions: 22 // * We will ask all the questions in MandatoryQuestionsKeys list one after the other. 23 // 2. Optional questions: 24 // * We have to provide a slice of prompt.Suggest, in which each suggest.Text is a key of a question in the map. 25 // * After a suggest was chosen from the list, the corresponding question from the map will be asked. 26 // * Each answer is written to to the configMap using its writer, under the MapKey specified in the questionInfo. 27 // * We will execute the previous step until the SaveAndExit string was inserted. 28 type InteractiveQuestionnaire struct { 29 QuestionsMap map[string]QuestionInfo 30 MandatoryQuestionsKeys []string 31 OptionalKeysSuggests []prompt.Suggest 32 AnswersMap map[string]interface{} 33 } 34 35 // Each question can have the following properties: 36 // * Msg - will be printed in separate line 37 // * PromptPrefix - will be printed before the input cursor in the answer line 38 // * Options - In case the answer must be selected from a predefined list 39 // * AllowVars - a flag indicates whether a variable (in form of ${var}) is an acceptable answer despite the predefined list 40 // * Writer - how to write the answer to the final config map 41 // * MapKey - the key under which the answer will be written to the configMap 42 // * Callback - optional function can be executed after the answer was inserted. Can be used to implement some dependencies between questions. 43 type AnswerWriter func(resultMap *map[string]interface{}, key, value string) error 44 type questionCallback func(*InteractiveQuestionnaire, string) (string, error) 45 46 type QuestionInfo struct { 47 Msg string 48 PromptPrefix string 49 Options []prompt.Suggest 50 AllowVars bool 51 Writer AnswerWriter 52 MapKey string 53 Callback questionCallback 54 } 55 56 const ( 57 PressTabMsg = " (press Tab for options):" 58 InvalidAnswerMsg = "Invalid answer. Please select value from the suggestions list." 59 VariableUseMsg = " You may use dynamic variable in the form of ${key}." 60 EmptyValueMsg = "The value cannot be empty. Please enter a valid value." 61 OptionalKey = "OptionalKey" 62 SaveAndExit = ":x" 63 64 // Boolean answers 65 True = "true" 66 False = "false" 67 68 CommaSeparatedListMsg = "The value should be a comma separated list" 69 ) 70 71 // Var can be inserted in the form of ${key} 72 var VarPattern = regexp.MustCompile(`^\$\{\w+\}+$`) 73 74 func prefixCompleter(options []prompt.Suggest) prompt.Completer { 75 return func(document prompt.Document) []prompt.Suggest { 76 return prompt.FilterHasPrefix(options, document.GetWordBeforeCursor(), true) 77 } 78 } 79 80 // Bind ctrl+c key to interrupt the command 81 func interruptKeyBind() prompt.Option { 82 interrupt := prompt.KeyBind{ 83 Key: prompt.ControlC, 84 Fn: func(buf *prompt.Buffer) { 85 panic("Interrupted") 86 }, 87 } 88 return prompt.OptionAddKeyBind(interrupt) 89 } 90 91 // Ask question with free string answer. 92 // If answer is empty and defaultValue isn't, return defaultValue. 93 // Otherwise, answer cannot be empty. 94 // Variable aren't checked and can be part of the answer. 95 func AskStringWithDefault(msg, promptPrefix, defaultValue string) string { 96 return askString(msg, promptPrefix, defaultValue, false, false) 97 } 98 99 // Ask question with free string answer, allow an empty string as an answer 100 func AskString(msg, promptPrefix string, allowEmpty bool, allowVars bool) string { 101 return askString(msg, promptPrefix, "", allowEmpty, allowVars) 102 } 103 104 // Ask question with free string answer. 105 // If an empty answer is allowed, the answer returned as is, 106 // if not and a default value was provided, the default value is returned. 107 func askString(msg, promptPrefix, defaultValue string, allowEmpty bool, allowVars bool) string { 108 if msg != "" { 109 fmt.Println(msg + ":") 110 } 111 errMsg := EmptyValueMsg 112 if allowVars { 113 errMsg += VariableUseMsg 114 } 115 promptPrefix = addDefaultValueToPrompt(promptPrefix, defaultValue) 116 for { 117 answer := prompt.Input(promptPrefix, prefixCompleter(nil), interruptKeyBind()) 118 answer = strings.TrimSpace(answer) 119 if allowEmpty || answer != "" { 120 return answer 121 } 122 // An empty answer wan given, default value wad provided. 123 if defaultValue != "" { 124 return defaultValue 125 } 126 fmt.Println(errMsg) 127 } 128 } 129 130 // Ask question with list of possible answers. 131 // If answer is empty and defaultValue isn't, return defaultValue. 132 // Otherwise, the answer must be chosen from the list, but can be a variable if allowVars set to true. 133 func AskFromList(msg, promptPrefix string, allowVars bool, options []prompt.Suggest, defaultValue string) string { 134 if msg != "" { 135 fmt.Println(msg + PressTabMsg) 136 } 137 errMsg := InvalidAnswerMsg 138 if allowVars { 139 errMsg += VariableUseMsg 140 } 141 promptPrefix = addDefaultValueToPrompt(promptPrefix, defaultValue) 142 for { 143 answer := prompt.Input(promptPrefix, prefixCompleter(options), interruptKeyBind()) 144 answer = strings.TrimSpace(answer) 145 if answer == "" && defaultValue != "" { 146 return defaultValue 147 } 148 if validateAnswer(answer, options, allowVars) { 149 return answer 150 } 151 fmt.Println(errMsg) 152 } 153 } 154 155 func addDefaultValueToPrompt(promptPrefix, defaultValue string) string { 156 if defaultValue != "" && defaultValue != DummyDefaultAnswer { 157 return promptPrefix + " [" + defaultValue + "]: " 158 } 159 return promptPrefix + " " 160 } 161 162 func validateAnswer(answer string, options []prompt.Suggest, allowVars bool) bool { 163 if allowVars { 164 if regexMatch := VarPattern.FindStringSubmatch(answer); regexMatch != nil { 165 return true 166 } 167 } 168 for _, option := range options { 169 if answer == option.Text { 170 return true 171 } 172 } 173 return false 174 } 175 176 // Ask question with list of possible answers. 177 // If the provided answer does not appear in list, confirm the choice. 178 func AskFromListWithMismatchConfirmation(promptPrefix, misMatchMsg string, options []prompt.Suggest) string { 179 for { 180 answer := prompt.Input(promptPrefix+" ", prefixCompleter(options), interruptKeyBind()) 181 if answer == "" { 182 fmt.Println(EmptyValueMsg) 183 } 184 for _, option := range options { 185 if answer == option.Text { 186 return answer 187 } 188 } 189 if coreutils.AskYesNo(misMatchMsg+" continue anyway?", false) { 190 return answer 191 } 192 } 193 } 194 195 // Ask question steps: 196 // 1. Ask for string/from list 197 // 2. Write the answer to answersMap (if writer provided) 198 // 3. Run callback (if provided) 199 func (iq *InteractiveQuestionnaire) AskQuestion(question QuestionInfo) (value string, err error) { 200 var answer string 201 if question.Options != nil { 202 answer = AskFromList(question.Msg, question.PromptPrefix, question.AllowVars, question.Options, "") 203 } else { 204 answer = AskString(question.Msg, question.PromptPrefix, false, question.AllowVars) 205 } 206 if question.Writer != nil { 207 err = question.Writer(&iq.AnswersMap, question.MapKey, answer) 208 if err != nil { 209 return "", err 210 } 211 } 212 if question.Callback != nil { 213 _, err = question.Callback(iq, answer) 214 if err != nil { 215 return "", err 216 } 217 } 218 return answer, nil 219 } 220 221 // The main function to perform the questionnaire 222 func (iq *InteractiveQuestionnaire) Perform() error { 223 iq.AnswersMap = make(map[string]interface{}) 224 for i := 0; i < len(iq.MandatoryQuestionsKeys); i++ { 225 iq.AskQuestion(iq.QuestionsMap[iq.MandatoryQuestionsKeys[i]]) 226 } 227 fmt.Println("You can type \":x\" at any time to save and exit.") 228 OptionalKeyQuestion := iq.QuestionsMap[OptionalKey] 229 OptionalKeyQuestion.Options = iq.OptionalKeysSuggests 230 for { 231 key, err := iq.AskQuestion(OptionalKeyQuestion) 232 if err != nil { 233 return err 234 } 235 if key == SaveAndExit { 236 break 237 } 238 } 239 return nil 240 } 241 242 // Common questions 243 var FreeStringQuestionInfo = QuestionInfo{ 244 Options: nil, 245 AllowVars: false, 246 Writer: WriteStringAnswer, 247 } 248 249 func GetBoolSuggests() []prompt.Suggest { 250 return []prompt.Suggest{ 251 {Text: True}, 252 {Text: False}, 253 } 254 } 255 256 var BoolQuestionInfo = QuestionInfo{ 257 Options: GetBoolSuggests(), 258 AllowVars: true, 259 Writer: WriteBoolAnswer, 260 } 261 262 var IntQuestionInfo = QuestionInfo{ 263 Options: nil, 264 AllowVars: true, 265 Writer: WriteIntAnswer, 266 } 267 268 var StringListQuestionInfo = QuestionInfo{ 269 Msg: CommaSeparatedListMsg, 270 Options: nil, 271 AllowVars: true, 272 Writer: WriteStringArrayAnswer, 273 } 274 275 // Common writers 276 func WriteStringAnswer(resultMap *map[string]interface{}, key, value string) error { 277 (*resultMap)[key] = value 278 return nil 279 } 280 281 func WriteBoolAnswer(resultMap *map[string]interface{}, key, value string) error { 282 if regexMatch := VarPattern.FindStringSubmatch(value); regexMatch != nil { 283 return WriteStringAnswer(resultMap, key, value) 284 } 285 boolValue, err := strconv.ParseBool(value) 286 if err != nil { 287 return err 288 } 289 (*resultMap)[key] = boolValue 290 return nil 291 } 292 293 func WriteIntAnswer(resultMap *map[string]interface{}, key, value string) error { 294 if regexMatch := VarPattern.FindStringSubmatch(value); regexMatch != nil { 295 return WriteStringAnswer(resultMap, key, value) 296 } 297 intValue, err := strconv.Atoi(value) 298 if err != nil { 299 return err 300 } 301 (*resultMap)[key] = intValue 302 return nil 303 } 304 305 func WriteStringArrayAnswer(resultMap *map[string]interface{}, key, value string) error { 306 if regexMatch := VarPattern.FindStringSubmatch(value); regexMatch != nil { 307 return WriteStringAnswer(resultMap, key, value) 308 } 309 arrValue := strings.Split(value, ",") 310 (*resultMap)[key] = arrValue 311 return nil 312 } 313 314 func GetSuggestsFromKeys(keys []string, SuggestionMap map[string]prompt.Suggest) []prompt.Suggest { 315 var suggests []prompt.Suggest 316 for _, key := range keys { 317 suggests = append(suggests, SuggestionMap[key]) 318 } 319 return suggests 320 } 321 322 func ConvertToSuggests(options []string) []prompt.Suggest { 323 var suggests []prompt.Suggest 324 for _, opt := range options { 325 suggests = append(suggests, prompt.Suggest{Text: opt}) 326 } 327 return suggests 328 } 329 330 // After an optional value was chosen we'll ask for its value. 331 func OptionalKeyCallback(iq *InteractiveQuestionnaire, key string) (value string, err error) { 332 if key != SaveAndExit { 333 valueQuestion := iq.QuestionsMap[key] 334 // Since we are using default question in most of the cases we set the map key here. 335 valueQuestion.MapKey = key 336 valueQuestion.PromptPrefix = InsertValuePromptMsg + key 337 if valueQuestion.Options != nil { 338 valueQuestion.PromptPrefix += PressTabMsg 339 } 340 valueQuestion.PromptPrefix += " >" 341 value, err = iq.AskQuestion(valueQuestion) 342 } 343 return value, err 344 }