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