Skip to content

Commit

Permalink
feat: implement multi-select variable type (#132)
Browse files Browse the repository at this point in the history
  • Loading branch information
majori authored Aug 1, 2024
1 parent 3f60714 commit cd0cc02
Show file tree
Hide file tree
Showing 14 changed files with 307 additions and 24 deletions.
1 change: 1 addition & 0 deletions cmd/docs/templates/_schema.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
| `confirm` | `bool` | `false` | If set to true, the prompt will be yes/no question, and the value type will be boolean. |
| `optional` | `bool` | `false` | If set to true, the variable can be left empty. |
| `options` | `[]string` | | The user selects the value from a list of options. |
| `multi` | `bool` | `false` | If set to true, the user can select multiple options defined by the `options` property. |
| `validators` | [`[]Validator`](#validator) | | Validators for the variable. |
| `if` | `string` | | Makes the variable conditional based on the result of the expression. The result of the evaluation needs to be a boolean value. Uses https://github.com/expr-lang/expr. |
| `columns` | `[]string` | | Set the variable as a table type with columns defined by this property. |
Expand Down
1 change: 1 addition & 0 deletions docs/site/docs/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Recipe variables supports the following types:
- [String](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L9-L11)
- [Boolean](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L13-L15)
- [Select (predefined options)](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L20-L22)
- [Multi-select (predefined options)](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L29-L38)
- [Table](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L29-L38)

You can see examples of all the possible variables in the [example recipe](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml).
Expand Down
11 changes: 11 additions & 0 deletions examples/variable-types/recipe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ vars:
- option_1
- option_2

- name: MULTI_SELECT_VAR
description: |
User chooses multiple values from the predefined values in `options` property.
Defined by: non-empty `options` property and `multi: true`.
multi: true
options:
- option_1
- option_2
- option_3

- name: TABLE_VAR
description: |
On templates you can access the cells by getting the row by the index and column by the name, like:
Expand Down
2 changes: 2 additions & 0 deletions examples/variable-types/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

# Select variable: {{ .Variables.SELECT_VAR }}

# Multi-select variable: {{ .Variables.MULTI_SELECT_VAR | join ", " }}

# Table variable

| COLUMN_1 | COLUMN_2 | COLUMN_3 |
Expand Down
4 changes: 3 additions & 1 deletion internal/cliutil/retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ func MakeRetryMessage(args []string, values recipe.VariableValues) string {
switch value := values[key].(type) {
case bool:
commandline.WriteString(fmt.Sprintf("\"%s=%t\" ", key, value))
case recipe.TableValue: // serialize to CSV
case recipe.MultiSelectValue:
commandline.WriteString(fmt.Sprintf("\"%s=%s\" ", key, value.ToString(',')))
case recipe.TableValue:
csv, err := value.ToCSV(',')
if err != nil {
panic(err)
Expand Down
39 changes: 32 additions & 7 deletions pkg/recipe/variable.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type Variable struct {
// The user selects the value from a list of options
Options []string `yaml:"options,omitempty"`

// If set to true, the user can select multiple values from the list of options
Multi bool `yaml:"multi,omitempty"`

// Validators for the variable
Validators []VariableValidator `yaml:"validators,omitempty"`

Expand All @@ -41,11 +44,12 @@ type Variable struct {
type VariableType uint8

const (
VariableTypeUndefined VariableType = iota
VariableTypeUnknown VariableType = iota
VariableTypeString
VariableTypeTable
VariableTypeSelect
VariableTypeBoolean
VariableTypeSelect
VariableTypeMultiSelect
VariableTypeTable
)

type VariableValidator struct {
Expand All @@ -65,6 +69,8 @@ type VariableValidator struct {
// VariableValues stores values for each variable
type VariableValues map[string]interface{}

type MultiSelectValue []string

type TableValue struct {
Columns []string `yaml:"columns"`
Rows [][]string `yaml:"rows,flow"`
Expand All @@ -81,7 +87,7 @@ func (v *Variable) Validate() error {
return errors.New("variable name can not start with a number")
}

if v.DetermineType() == VariableTypeUndefined {
if v.DetermineType() == VariableTypeUnknown {
return errors.New("internal error: variable type could not be determined")
}

Expand All @@ -103,6 +109,10 @@ func (v *Variable) Validate() error {
}
}

if v.Multi && len(v.Options) == 0 {
return errors.New("multiselect variables need to have options defined")
}

for i, validator := range v.Validators {
validatorIndex := fmt.Sprintf("validator %d", i+1)
if v.Confirm {
Expand Down Expand Up @@ -162,15 +172,16 @@ func (v *Variable) Validate() error {
return nil
}

// NOTE: This function does note validate the values against the variable definitions.
// It only checks if the name of the values are not empty and the values are of supported types.
func (val VariableValues) Validate() error {
for name, v := range val {
if name == "" {
return errors.New("variable name can not be empty")
}

switch v.(type) {
// List allowed types
case string, bool, TableValue:
case string, bool, MultiSelectValue, TableValue:
break
default:
return fmt.Errorf("unsupported variable value type")
Expand Down Expand Up @@ -352,7 +363,11 @@ func (v Variable) DetermineType() VariableType {
case v.Confirm:
return VariableTypeBoolean
case len(v.Options) > 0:
return VariableTypeSelect
if v.Multi {
return VariableTypeMultiSelect
} else {
return VariableTypeSelect
}
case len(v.Columns) > 0:
return VariableTypeTable
default:
Expand All @@ -366,6 +381,8 @@ func (v Variable) ParseDefaultValue() (interface{}, error) {
return v.Default == "true", nil
case VariableTypeSelect:
return v.Default, nil
case VariableTypeMultiSelect:
return strings.Split(v.Default, ","), nil
case VariableTypeTable:
t := TableValue{}
err := t.FromCSV(v.Columns, v.Default, ',')
Expand All @@ -379,3 +396,11 @@ func (v Variable) ParseDefaultValue() (interface{}, error) {
return nil, errors.New("unknown variable type")
}
}

func (v MultiSelectValue) ToString(delimiter rune) string {
return strings.Join(v, string(delimiter))
}

func (v *MultiSelectValue) FromString(s string, delimiter rune) {
*v = strings.Split(s, string(delimiter))
}
8 changes: 8 additions & 0 deletions pkg/recipe/variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ func TestVariableValidation(t *testing.T) {
},
"`options` and `columns` properties can not be defined",
},
{
"only multi is defined",
Variable{
Name: "foo",
Multi: true,
},
"multiselect variables need to have options defined",
},
}

for _, scenario := range scenarios {
Expand Down
16 changes: 16 additions & 0 deletions pkg/recipeutil/values.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ func ParseProvidedValues(variables []recipe.Variable, flags []string, delimiter
return nil, fmt.Errorf("%w: %s", ErrVarNotDefinedInRecipe, varName)
}

if !targetedVariable.Optional && varValue == "" {
return nil, fmt.Errorf("predefined value for variable '%s' can not be empty as it is not optional", varName)
}

switch {
case targetedVariable.Confirm:
if varValue == "true" {
Expand All @@ -61,6 +65,7 @@ func ParseProvidedValues(variables []recipe.Variable, flags []string, delimiter
} else {
return nil, fmt.Errorf("value provided for variable '%s' was not a boolean", varName)
}

case len(targetedVariable.Columns) > 0:
varValue = strings.ReplaceAll(varValue, "\\n", "\n")
table := recipe.TableValue{}
Expand Down Expand Up @@ -96,6 +101,17 @@ func ParseProvidedValues(variables []recipe.Variable, flags []string, delimiter
}
values[varName] = table

case targetedVariable.Multi:
vals := recipe.MultiSelectValue{}
vals.FromString(varValue, delimiter)

for _, val := range vals {
if !slices.Contains(targetedVariable.Options, val) {
return nil, fmt.Errorf("provided value '%s' is not in the list of options for variable '%s'", val, varName)
}
}
values[varName] = vals

default:
for i := range targetedVariable.Validators {
validatorFunc, err := targetedVariable.Validators[i].CreateValidatorFunc()
Expand Down
Loading

0 comments on commit cd0cc02

Please sign in to comment.