Skip to content

Commit cd0cc02

Browse files
authored
feat: implement multi-select variable type (#132)
1 parent 3f60714 commit cd0cc02

File tree

14 files changed

+307
-24
lines changed

14 files changed

+307
-24
lines changed

cmd/docs/templates/_schema.tmpl

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
| `confirm` | `bool` | `false` | If set to true, the prompt will be yes/no question, and the value type will be boolean. |
2828
| `optional` | `bool` | `false` | If set to true, the variable can be left empty. |
2929
| `options` | `[]string` | | The user selects the value from a list of options. |
30+
| `multi` | `bool` | `false` | If set to true, the user can select multiple options defined by the `options` property. |
3031
| `validators` | [`[]Validator`](#validator) | | Validators for the variable. |
3132
| `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. |
3233
| `columns` | `[]string` | | Set the variable as a table type with columns defined by this property. |

docs/site/docs/usage.mdx

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Recipe variables supports the following types:
128128
- [String](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L9-L11)
129129
- [Boolean](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L13-L15)
130130
- [Select (predefined options)](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L20-L22)
131+
- [Multi-select (predefined options)](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L29-L38)
131132
- [Table](https://github.com/futurice/jalapeno/blob/main/examples/variable-types/recipe.yml#L29-L38)
132133

133134
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).

examples/variable-types/recipe.yml

+11
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ vars:
2626
- option_1
2727
- option_2
2828

29+
- name: MULTI_SELECT_VAR
30+
description: |
31+
User chooses multiple values from the predefined values in `options` property.
32+
33+
Defined by: non-empty `options` property and `multi: true`.
34+
multi: true
35+
options:
36+
- option_1
37+
- option_2
38+
- option_3
39+
2940
- name: TABLE_VAR
3041
description: |
3142
On templates you can access the cells by getting the row by the index and column by the name, like:

examples/variable-types/templates/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

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

7+
# Multi-select variable: {{ .Variables.MULTI_SELECT_VAR | join ", " }}
8+
79
# Table variable
810

911
| COLUMN_1 | COLUMN_2 | COLUMN_3 |

internal/cliutil/retry.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ func MakeRetryMessage(args []string, values recipe.VariableValues) string {
4444
switch value := values[key].(type) {
4545
case bool:
4646
commandline.WriteString(fmt.Sprintf("\"%s=%t\" ", key, value))
47-
case recipe.TableValue: // serialize to CSV
47+
case recipe.MultiSelectValue:
48+
commandline.WriteString(fmt.Sprintf("\"%s=%s\" ", key, value.ToString(',')))
49+
case recipe.TableValue:
4850
csv, err := value.ToCSV(',')
4951
if err != nil {
5052
panic(err)

pkg/recipe/variable.go

+32-7
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ type Variable struct {
2828
// The user selects the value from a list of options
2929
Options []string `yaml:"options,omitempty"`
3030

31+
// If set to true, the user can select multiple values from the list of options
32+
Multi bool `yaml:"multi,omitempty"`
33+
3134
// Validators for the variable
3235
Validators []VariableValidator `yaml:"validators,omitempty"`
3336

@@ -41,11 +44,12 @@ type Variable struct {
4144
type VariableType uint8
4245

4346
const (
44-
VariableTypeUndefined VariableType = iota
47+
VariableTypeUnknown VariableType = iota
4548
VariableTypeString
46-
VariableTypeTable
47-
VariableTypeSelect
4849
VariableTypeBoolean
50+
VariableTypeSelect
51+
VariableTypeMultiSelect
52+
VariableTypeTable
4953
)
5054

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

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

84-
if v.DetermineType() == VariableTypeUndefined {
90+
if v.DetermineType() == VariableTypeUnknown {
8591
return errors.New("internal error: variable type could not be determined")
8692
}
8793

@@ -103,6 +109,10 @@ func (v *Variable) Validate() error {
103109
}
104110
}
105111

112+
if v.Multi && len(v.Options) == 0 {
113+
return errors.New("multiselect variables need to have options defined")
114+
}
115+
106116
for i, validator := range v.Validators {
107117
validatorIndex := fmt.Sprintf("validator %d", i+1)
108118
if v.Confirm {
@@ -162,15 +172,16 @@ func (v *Variable) Validate() error {
162172
return nil
163173
}
164174

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

171183
switch v.(type) {
172-
// List allowed types
173-
case string, bool, TableValue:
184+
case string, bool, MultiSelectValue, TableValue:
174185
break
175186
default:
176187
return fmt.Errorf("unsupported variable value type")
@@ -352,7 +363,11 @@ func (v Variable) DetermineType() VariableType {
352363
case v.Confirm:
353364
return VariableTypeBoolean
354365
case len(v.Options) > 0:
355-
return VariableTypeSelect
366+
if v.Multi {
367+
return VariableTypeMultiSelect
368+
} else {
369+
return VariableTypeSelect
370+
}
356371
case len(v.Columns) > 0:
357372
return VariableTypeTable
358373
default:
@@ -366,6 +381,8 @@ func (v Variable) ParseDefaultValue() (interface{}, error) {
366381
return v.Default == "true", nil
367382
case VariableTypeSelect:
368383
return v.Default, nil
384+
case VariableTypeMultiSelect:
385+
return strings.Split(v.Default, ","), nil
369386
case VariableTypeTable:
370387
t := TableValue{}
371388
err := t.FromCSV(v.Columns, v.Default, ',')
@@ -379,3 +396,11 @@ func (v Variable) ParseDefaultValue() (interface{}, error) {
379396
return nil, errors.New("unknown variable type")
380397
}
381398
}
399+
400+
func (v MultiSelectValue) ToString(delimiter rune) string {
401+
return strings.Join(v, string(delimiter))
402+
}
403+
404+
func (v *MultiSelectValue) FromString(s string, delimiter rune) {
405+
*v = strings.Split(s, string(delimiter))
406+
}

pkg/recipe/variable_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ func TestVariableValidation(t *testing.T) {
5050
},
5151
"`options` and `columns` properties can not be defined",
5252
},
53+
{
54+
"only multi is defined",
55+
Variable{
56+
Name: "foo",
57+
Multi: true,
58+
},
59+
"multiselect variables need to have options defined",
60+
},
5361
}
5462

5563
for _, scenario := range scenarios {

pkg/recipeutil/values.go

+16
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ func ParseProvidedValues(variables []recipe.Variable, flags []string, delimiter
5252
return nil, fmt.Errorf("%w: %s", ErrVarNotDefinedInRecipe, varName)
5353
}
5454

55+
if !targetedVariable.Optional && varValue == "" {
56+
return nil, fmt.Errorf("predefined value for variable '%s' can not be empty as it is not optional", varName)
57+
}
58+
5559
switch {
5660
case targetedVariable.Confirm:
5761
if varValue == "true" {
@@ -61,6 +65,7 @@ func ParseProvidedValues(variables []recipe.Variable, flags []string, delimiter
6165
} else {
6266
return nil, fmt.Errorf("value provided for variable '%s' was not a boolean", varName)
6367
}
68+
6469
case len(targetedVariable.Columns) > 0:
6570
varValue = strings.ReplaceAll(varValue, "\\n", "\n")
6671
table := recipe.TableValue{}
@@ -96,6 +101,17 @@ func ParseProvidedValues(variables []recipe.Variable, flags []string, delimiter
96101
}
97102
values[varName] = table
98103

104+
case targetedVariable.Multi:
105+
vals := recipe.MultiSelectValue{}
106+
vals.FromString(varValue, delimiter)
107+
108+
for _, val := range vals {
109+
if !slices.Contains(targetedVariable.Options, val) {
110+
return nil, fmt.Errorf("provided value '%s' is not in the list of options for variable '%s'", val, varName)
111+
}
112+
}
113+
values[varName] = vals
114+
99115
default:
100116
for i := range targetedVariable.Validators {
101117
validatorFunc, err := targetedVariable.Validators[i].CreateValidatorFunc()

0 commit comments

Comments
 (0)