diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md index e46cf86..934ae77 100644 --- a/docs/data-sources/parameter.md +++ b/docs/data-sources/parameter.md @@ -145,10 +145,12 @@ data "coder_parameter" "home_volume_size" { - `description` (String) Describe what this parameter does. - `display_name` (String) The displayed name of the parameter as it will appear in the interface. - `ephemeral` (Boolean) The value of an ephemeral parameter will not be preserved between consecutive workspace builds. +- `form_type` (String) The type of this parameter. Must be one of: [radio, slider, input, dropdown, checkbox, switch, multi-select, tag-select, textarea, error]. - `icon` (String) A URL to an icon that will display in the dashboard. View built-in icons [here](https://github.com/coder/coder/tree/main/site/static/icon). Use a built-in icon with `"${data.coder_workspace.me.access_url}/icon/"`. - `mutable` (Boolean) Whether this value can be changed after workspace creation. This can be destructive for values like region, so use with caution! - `option` (Block List) Each `option` block defines a value for a user to select from. (see [below for nested schema](#nestedblock--option)) - `order` (Number) The order determines the position of a template parameter in the UI/CLI presentation. The lowest order is shown first and parameters with equal order are sorted by name (ascending order). +- `styling` (String) JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI. This option is purely cosmetic and does not affect the function of the parameter in terraform. - `type` (String) The type of this parameter. Must be one of: `"number"`, `"string"`, `"bool"`, or `"list(string)"`. - `validation` (Block List, Max: 1) Validate the input of a parameter. (see [below for nested schema](#nestedblock--validation)) diff --git a/provider/formtype.go b/provider/formtype.go new file mode 100644 index 0000000..48fbeed --- /dev/null +++ b/provider/formtype.go @@ -0,0 +1,148 @@ +package provider + +import ( + "slices" + + "golang.org/x/xerrors" +) + +// OptionType is a type of option that can be used in the 'type' argument of +// a parameter. These should match types as defined in terraform: +// +// https://developer.hashicorp.com/terraform/language/expressions/types +// +// The value have to be string literals, as type constraint keywords are not +// supported in providers. +type OptionType string + +const ( + OptionTypeString OptionType = "string" + OptionTypeNumber OptionType = "number" + OptionTypeBoolean OptionType = "bool" + OptionTypeListString OptionType = "list(string)" +) + +func OptionTypes() []OptionType { + return []OptionType{ + OptionTypeString, + OptionTypeNumber, + OptionTypeBoolean, + OptionTypeListString, + } +} + +// ParameterFormType is the list of supported form types for display in +// the Coder "create workspace" form. These form types are functional as well +// as cosmetic. Refer to `formTypeTruthTable` for the allowed pairings. +// For example, "multi-select" has the type "list(string)" but the option +// values are "string". +type ParameterFormType string + +const ( + ParameterFormTypeDefault ParameterFormType = "" + ParameterFormTypeRadio ParameterFormType = "radio" + ParameterFormTypeSlider ParameterFormType = "slider" + ParameterFormTypeInput ParameterFormType = "input" + ParameterFormTypeDropdown ParameterFormType = "dropdown" + ParameterFormTypeCheckbox ParameterFormType = "checkbox" + ParameterFormTypeSwitch ParameterFormType = "switch" + ParameterFormTypeMultiSelect ParameterFormType = "multi-select" + ParameterFormTypeTagSelect ParameterFormType = "tag-select" + ParameterFormTypeTextArea ParameterFormType = "textarea" + ParameterFormTypeError ParameterFormType = "error" +) + +// ParameterFormTypes should be kept in sync with the enum list above. +func ParameterFormTypes() []ParameterFormType { + return []ParameterFormType{ + // Intentionally omit "ParameterFormTypeDefault" from this set. + // It is a valid enum, but will always be mapped to a real value when + // being used. + ParameterFormTypeRadio, + ParameterFormTypeSlider, + ParameterFormTypeInput, + ParameterFormTypeDropdown, + ParameterFormTypeCheckbox, + ParameterFormTypeSwitch, + ParameterFormTypeMultiSelect, + ParameterFormTypeTagSelect, + ParameterFormTypeTextArea, + ParameterFormTypeError, + } +} + +// formTypeTruthTable is a map of [`type`][`optionCount` > 0] to `form_type`. +// The first value in the slice is the default value assuming `form_type` is +// not specified. +// +// The boolean key indicates whether the `options` field is specified. +// | Type | Options | Specified Form Type | form_type | Notes | +// |-------------------|---------|---------------------|----------------|--------------------------------| +// | `string` `number` | Y | | `radio` | | +// | `string` `number` | Y | `dropdown` | `dropdown` | | +// | `string` `number` | N | | `input` | | +// | `string` | N | 'textarea' | `textarea` | | +// | `number` | N | 'slider' | `slider` | min/max validation | +// | `bool` | Y | | `radio` | | +// | `bool` | N | | `checkbox` | | +// | `bool` | N | `switch` | `switch` | | +// | `list(string)` | Y | | `radio` | | +// | `list(string)` | N | | `tag-select` | | +// | `list(string)` | Y | `multi-select` | `multi-select` | Option values will be `string` | +var formTypeTruthTable = map[OptionType]map[bool][]ParameterFormType{ + OptionTypeString: { + true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, + false: {ParameterFormTypeInput, ParameterFormTypeTextArea}, + }, + OptionTypeNumber: { + true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, + false: {ParameterFormTypeInput, ParameterFormTypeSlider}, + }, + OptionTypeBoolean: { + true: {ParameterFormTypeRadio}, + false: {ParameterFormTypeCheckbox, ParameterFormTypeSwitch}, + }, + OptionTypeListString: { + true: {ParameterFormTypeRadio, ParameterFormTypeMultiSelect}, + false: {ParameterFormTypeTagSelect}, + }, +} + +// ValidateFormType handles the truth table for the valid set of `type` and +// `form_type` options. +// The OptionType is also returned because it is possible the 'type' of the +// 'value' & 'default' fields is different from the 'type' of the options. +// The use case is when using multi-select. The options are 'string' and the +// value is 'list(string)'. +func ValidateFormType(paramType OptionType, optionCount int, specifiedFormType ParameterFormType) (OptionType, ParameterFormType, error) { + allowed, ok := formTypeTruthTable[paramType][optionCount > 0] + if !ok || len(allowed) == 0 { + return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType) + } + + if specifiedFormType == ParameterFormTypeDefault { + // handle the default case + specifiedFormType = allowed[0] + } + + if !slices.Contains(allowed, specifiedFormType) { + return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", specifiedFormType) + } + + // This is the only current special case. If 'multi-select' is selected, the type + // of 'value' and an options 'value' are different. The type of the parameter is + // `list(string)` but the type of the individual options is `string`. + if paramType == OptionTypeListString && specifiedFormType == ParameterFormTypeMultiSelect { + return OptionTypeString, ParameterFormTypeMultiSelect, nil + } + + return paramType, specifiedFormType, nil +} + +func toStrings[A ~string](l []A) []string { + var r []string + for _, v := range l { + r = append(r, string(v)) + } + return r +} diff --git a/provider/formtype_test.go b/provider/formtype_test.go new file mode 100644 index 0000000..599c2e4 --- /dev/null +++ b/provider/formtype_test.go @@ -0,0 +1,428 @@ +package provider_test + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/coder/terraform-provider-coder/v2/provider" +) + +// formTypeTestCase is the config for a single test case. +type formTypeTestCase struct { + name string + config formTypeCheck + assert paramAssert + expectError *regexp.Regexp +} + +// paramAssert is asserted on the provider's parsed terraform state. +type paramAssert struct { + FormType provider.ParameterFormType + Type provider.OptionType + Styling json.RawMessage +} + +// formTypeCheck is a struct that helps build the terraform config +type formTypeCheck struct { + formType provider.ParameterFormType + optionType provider.OptionType + options bool + + // optional to inform the assert + customOptions []string + defValue string + styling json.RawMessage +} + +func (c formTypeCheck) String() string { + return fmt.Sprintf("%s_%s_%t", c.formType, c.optionType, c.options) +} + +func TestValidateFormType(t *testing.T) { + t.Parallel() + + // formTypesChecked keeps track of all checks run. It will be used to + // ensure all combinations of form_type and option_type are tested. + // All untested options are assumed to throw an error. + formTypesChecked := make(map[string]struct{}) + + expectType := func(expected provider.ParameterFormType, opts formTypeCheck) formTypeTestCase { + ftname := opts.formType + if ftname == "" { + ftname = "default" + } + + if opts.styling == nil { + // Try passing arbitrary data in, as anything should be accepted + opts.styling, _ = json.Marshal(map[string]any{ + "foo": "bar", + "disabled": true, + "nested": map[string]any{ + "foo": "bar", + }, + }) + } + + return formTypeTestCase{ + name: fmt.Sprintf("%s_%s_%t", + ftname, + opts.optionType, + opts.options, + ), + config: opts, + assert: paramAssert{ + FormType: expected, + Type: opts.optionType, + Styling: opts.styling, + }, + expectError: nil, + } + } + + // expectSameFormType just assumes the FormType in the check is the expected + // FormType. Using `expectType` these fields can differ + expectSameFormType := func(opts formTypeCheck) formTypeTestCase { + return expectType(opts.formType, opts) + } + + cases := []formTypeTestCase{ + { + // When nothing is specified + name: "defaults", + config: formTypeCheck{}, + assert: paramAssert{ + FormType: provider.ParameterFormTypeInput, + Type: provider.OptionTypeString, + Styling: []byte("{}"), + }, + }, + // All default behaviors. Essentially legacy behavior. + // String + expectType(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeString, + }), + expectType(provider.ParameterFormTypeInput, formTypeCheck{ + options: false, + optionType: provider.OptionTypeString, + }), + // Number + expectType(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeNumber, + }), + expectType(provider.ParameterFormTypeInput, formTypeCheck{ + options: false, + optionType: provider.OptionTypeNumber, + }), + // Boolean + expectType(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeBoolean, + }), + expectType(provider.ParameterFormTypeCheckbox, formTypeCheck{ + options: false, + optionType: provider.OptionTypeBoolean, + }), + // List(string) + expectType(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeListString, + }), + expectType(provider.ParameterFormTypeTagSelect, formTypeCheck{ + options: false, + optionType: provider.OptionTypeListString, + }), + + // ---- New Behavior + // String + expectSameFormType(formTypeCheck{ + options: true, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeDropdown, + }), + expectSameFormType(formTypeCheck{ + options: true, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeRadio, + }), + expectSameFormType(formTypeCheck{ + options: false, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeInput, + }), + expectSameFormType(formTypeCheck{ + options: false, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeTextArea, + }), + // Number + expectSameFormType(formTypeCheck{ + options: true, + optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeDropdown, + }), + expectSameFormType(formTypeCheck{ + options: true, + optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeRadio, + }), + expectSameFormType(formTypeCheck{ + options: false, + optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeInput, + }), + expectSameFormType(formTypeCheck{ + options: false, + optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeSlider, + }), + // Boolean + expectSameFormType(formTypeCheck{ + options: true, + optionType: provider.OptionTypeBoolean, + formType: provider.ParameterFormTypeRadio, + }), + expectSameFormType(formTypeCheck{ + options: false, + optionType: provider.OptionTypeBoolean, + formType: provider.ParameterFormTypeSwitch, + }), + expectSameFormType(formTypeCheck{ + options: false, + optionType: provider.OptionTypeBoolean, + formType: provider.ParameterFormTypeCheckbox, + }), + // List(string) + expectSameFormType(formTypeCheck{ + options: true, + optionType: provider.OptionTypeListString, + formType: provider.ParameterFormTypeRadio, + }), + expectSameFormType(formTypeCheck{ + options: true, + optionType: provider.OptionTypeListString, + formType: provider.ParameterFormTypeMultiSelect, + customOptions: []string{"red", "blue", "green"}, + defValue: `["red", "blue"]`, + }), + expectSameFormType(formTypeCheck{ + options: false, + optionType: provider.OptionTypeListString, + formType: provider.ParameterFormTypeTagSelect, + }), + + // Some manual test cases + { + name: "list_string_bad_default", + config: formTypeCheck{ + formType: provider.ParameterFormTypeMultiSelect, + optionType: provider.OptionTypeListString, + customOptions: []string{"red", "blue", "green"}, + defValue: `["red", "yellow"]`, + styling: nil, + }, + expectError: regexp.MustCompile("is not a valid option"), + }, + } + + passed := t.Run("TabledTests", func(t *testing.T) { + // TabledCases runs through all the manual test cases + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + if _, ok := formTypesChecked[c.config.String()]; ok { + t.Log("Duplicated form type check, delete this extra test case") + t.Fatalf("form type %q already checked", c.config.String()) + } + + formTypesChecked[c.config.String()] = struct{}{} + formTypeTest(t, c) + }) + } + }) + + if !passed { + // Do not run additional tests and pollute the output + t.Log("Tests failed, will not run the assumed error cases") + return + } + + // AssumeErrorCases assumes any uncovered test will return an error. Not covered + // cases in the truth table are assumed to be invalid. So if the tests above + // cover all valid cases, this asserts all the invalid cases. + // + // This test consequentially ensures all valid cases are covered manually above. + t.Run("AssumeErrorCases", func(t *testing.T) { + // requiredChecks loops through all possible form_type and option_type + // combinations. + requiredChecks := make([]formTypeCheck, 0) + for _, ft := range append(provider.ParameterFormTypes(), "") { + for _, ot := range provider.OptionTypes() { + requiredChecks = append(requiredChecks, formTypeCheck{ + formType: ft, + optionType: ot, + options: false, + }) + requiredChecks = append(requiredChecks, formTypeCheck{ + formType: ft, + optionType: ot, + options: true, + }) + } + } + + for _, check := range requiredChecks { + if _, alreadyChecked := formTypesChecked[check.String()]; alreadyChecked { + continue + } + + ftName := check.formType + if ftName == "" { + ftName = "default" + } + fc := formTypeTestCase{ + name: fmt.Sprintf("%s_%s_%t", + ftName, + check.optionType, + check.options, + ), + config: check, + assert: paramAssert{}, + expectError: regexp.MustCompile("is not supported"), + } + + t.Run(fc.name, func(t *testing.T) { + t.Parallel() + + // This is just helpful log output to give the boilerplate + // to write the manual test. + tcText := fmt.Sprintf(` + expectSameFormType(%s, ezconfigOpts{ + Options: %t, + OptionType: %q, + FormType: %q, + }), + //`, "", check.options, check.optionType, check.formType) + + logDebugInfo := formTypeTest(t, fc) + if !logDebugInfo { + t.Logf("To construct this test case:\n%s", tcText) + } + }) + + } + }) +} + +// createTF converts a formTypeCheck into a terraform config string. +func createTF(paramName string, cfg formTypeCheck) (defaultValue string, tf string) { + options := cfg.customOptions + if cfg.options && len(cfg.customOptions) == 0 { + switch cfg.optionType { + case provider.OptionTypeString: + options = []string{"foo"} + defaultValue = "foo" + case provider.OptionTypeBoolean: + options = []string{"true", "false"} + defaultValue = "true" + case provider.OptionTypeNumber: + options = []string{"1"} + defaultValue = "1" + case provider.OptionTypeListString: + options = []string{`["red", "blue"]`} + defaultValue = `["red", "blue"]` + default: + panic(fmt.Sprintf("unknown option type %q when generating options", cfg.optionType)) + } + } + + if cfg.defValue == "" { + cfg.defValue = defaultValue + } + + var body strings.Builder + if cfg.defValue != "" { + body.WriteString(fmt.Sprintf("default = %q\n", cfg.defValue)) + } + if cfg.formType != "" { + body.WriteString(fmt.Sprintf("form_type = %q\n", cfg.formType)) + } + if cfg.optionType != "" { + body.WriteString(fmt.Sprintf("type = %q\n", cfg.optionType)) + } + if cfg.styling != nil { + body.WriteString(fmt.Sprintf("styling = %s\n", strconv.Quote(string(cfg.styling)))) + } + + for i, opt := range options { + body.WriteString("option {\n") + body.WriteString(fmt.Sprintf("name = \"val_%d\"\n", i)) + body.WriteString(fmt.Sprintf("value = %q\n", opt)) + body.WriteString("}\n") + } + + return cfg.defValue, fmt.Sprintf(` + provider "coder" { + } + data "coder_parameter" "%s" { + name = "%s" + %s + } + `, paramName, paramName, body.String()) +} + +func formTypeTest(t *testing.T, c formTypeTestCase) bool { + t.Helper() + const paramName = "test_param" + // logDebugInfo is just a guess used for logging. It's not important. It cannot + // determine for sure if the test passed because the terraform test runner is a + // black box. It does not indicate if the test passed or failed. Since this is + // just used for logging, this is good enough. + logDebugInfo := true + + def, tf := createTF(paramName, c.config) + checkFn := func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + require.Len(t, state.Modules[0].Resources, 1) + + key := strings.Join([]string{"data", "coder_parameter", paramName}, ".") + param := state.Modules[0].Resources[key] + + logDebugInfo = logDebugInfo && assert.Equal(t, def, param.Primary.Attributes["default"], "default value") + logDebugInfo = logDebugInfo && assert.Equal(t, string(c.assert.FormType), param.Primary.Attributes["form_type"], "form_type") + logDebugInfo = logDebugInfo && assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") + logDebugInfo = logDebugInfo && assert.JSONEq(t, string(c.assert.Styling), param.Primary.Attributes["styling"], "styling") + + return nil + } + if c.expectError != nil { + checkFn = nil + } + + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: coderFactory(), + Steps: []resource.TestStep{ + { + Config: tf, + Check: checkFn, + ExpectError: c.expectError, + }, + }, + }) + + if !logDebugInfo { + t.Logf("Terraform config:\n%s", tf) + } + return logDebugInfo +} diff --git a/provider/parameter.go b/provider/parameter.go index 1345f4d..4eb8a28 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -50,7 +50,8 @@ type Parameter struct { Name string DisplayName string `mapstructure:"display_name"` Description string - Type string + Type OptionType + FormType ParameterFormType Mutable bool Default string Icon string @@ -86,6 +87,7 @@ func parameterDataSource() *schema.Resource { DisplayName interface{} Description interface{} Type interface{} + FormType interface{} Mutable interface{} Default interface{} Icon interface{} @@ -100,6 +102,7 @@ func parameterDataSource() *schema.Resource { DisplayName: rd.Get("display_name"), Description: rd.Get("description"), Type: rd.Get("type"), + FormType: rd.Get("form_type"), Mutable: rd.Get("mutable"), Default: rd.Get("default"), Icon: rd.Get("icon"), @@ -149,6 +152,20 @@ func parameterDataSource() *schema.Resource { } } + // Validate options + + // optionType might differ from parameter.Type. This is ok, and parameter.Type + // should be used for the value type, and optionType for options. + var optionType OptionType + optionType, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType) + if err != nil { + return diag.FromErr(err) + } + // Set the form_type back in case the value was changed. + // Eg via a default. If a user does not specify, a default value + // is used and saved. + rd.Set("form_type", parameter.FormType) + if len(parameter.Option) > 0 { names := map[string]interface{}{} values := map[string]interface{}{} @@ -161,7 +178,7 @@ func parameterDataSource() *schema.Resource { if exists { return diag.Errorf("multiple options cannot have the same value %q", option.Value) } - err := valueIsType(parameter.Type, option.Value) + err := valueIsType(optionType, option.Value) if err != nil { return err } @@ -170,9 +187,36 @@ func parameterDataSource() *schema.Resource { } if parameter.Default != "" { - _, defaultIsValid := values[parameter.Default] - if !defaultIsValid { - return diag.Errorf("default value %q must be defined as one of options", parameter.Default) + if parameter.Type == OptionTypeListString && optionType == OptionTypeString { + // If the type is list(string) and optionType is string, we have + // to ensure all elements of the default exist as options. + var defaultValues []string + // TODO: We do this unmarshal in a few spots. It should be standardized. + err = json.Unmarshal([]byte(parameter.Default), &defaultValues) + if err != nil { + return diag.Errorf("default value %q is not a list of strings", parameter.Default) + } + + // missing is used to construct a more helpful error message + var missing []string + for _, defaultValue := range defaultValues { + _, defaultIsValid := values[defaultValue] + if !defaultIsValid { + missing = append(missing, defaultValue) + } + } + + if len(missing) > 0 { + return diag.Errorf( + "default value %q is not a valid option, values %q are missing from the option", + parameter.Default, strings.Join(missing, ", "), + ) + } + } else { + _, defaultIsValid := values[parameter.Default] + if !defaultIsValid { + return diag.Errorf("%q default value %q must be defined as one of options", parameter.FormType, parameter.Default) + } } } } @@ -203,9 +247,23 @@ func parameterDataSource() *schema.Resource { Type: schema.TypeString, Default: "string", Optional: true, - ValidateFunc: validation.StringInSlice([]string{"number", "string", "bool", "list(string)"}, false), + ValidateFunc: validation.StringInSlice(toStrings(OptionTypes()), false), Description: "The type of this parameter. Must be one of: `\"number\"`, `\"string\"`, `\"bool\"`, or `\"list(string)\"`.", }, + "form_type": { + Type: schema.TypeString, + Default: ParameterFormTypeDefault, + Optional: true, + ValidateFunc: validation.StringInSlice(toStrings(ParameterFormTypes()), false), + Description: fmt.Sprintf("The type of this parameter. Must be one of: [%s].", strings.Join(toStrings(ParameterFormTypes()), ", ")), + }, + "styling": { + Type: schema.TypeString, + Default: `{}`, + Description: "JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI. " + + "This option is purely cosmetic and does not affect the function of the parameter in terraform.", + Optional: true, + }, "mutable": { Type: schema.TypeBool, Optional: true, @@ -375,25 +433,25 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int return vArr, nil } -func valueIsType(typ, value string) diag.Diagnostics { +func valueIsType(typ OptionType, value string) diag.Diagnostics { switch typ { - case "number": + case OptionTypeNumber: _, err := strconv.ParseFloat(value, 64) if err != nil { return diag.Errorf("%q is not a number", value) } - case "bool": + case OptionTypeBoolean: _, err := strconv.ParseBool(value) if err != nil { return diag.Errorf("%q is not a bool", value) } - case "list(string)": + case OptionTypeListString: var items []string err := json.Unmarshal([]byte(value), &items) if err != nil { return diag.Errorf("%q is not an array of strings", value) } - case "string": + case OptionTypeString: // Anything is a string! default: return diag.Errorf("invalid type %q", typ) @@ -401,8 +459,8 @@ func valueIsType(typ, value string) diag.Diagnostics { return nil } -func (v *Validation) Valid(typ, value string) error { - if typ != "number" { +func (v *Validation) Valid(typ OptionType, value string) error { + if typ != OptionTypeNumber { if !v.MinDisabled { return fmt.Errorf("a min cannot be specified for a %s type", typ) } @@ -413,16 +471,16 @@ func (v *Validation) Valid(typ, value string) error { return fmt.Errorf("monotonic validation can only be specified for number types, not %s types", typ) } } - if typ != "string" && v.Regex != "" { + if typ != OptionTypeString && v.Regex != "" { return fmt.Errorf("a regex cannot be specified for a %s type", typ) } switch typ { - case "bool": + case OptionTypeBoolean: if value != "true" && value != "false" { return fmt.Errorf(`boolean value can be either "true" or "false"`) } return nil - case "string": + case OptionTypeString: if v.Regex == "" { return nil } @@ -437,7 +495,7 @@ func (v *Validation) Valid(typ, value string) error { if !matched { return fmt.Errorf("%s (value %q does not match %q)", v.Error, value, regex) } - case "number": + case OptionTypeNumber: num, err := strconv.Atoi(value) if err != nil { return takeFirstError(v.errorRendered(value), fmt.Errorf("value %q is not a number", value)) @@ -451,7 +509,7 @@ func (v *Validation) Valid(typ, value string) error { if v.Monotonic != "" && v.Monotonic != ValidationMonotonicIncreasing && v.Monotonic != ValidationMonotonicDecreasing { return fmt.Errorf("number monotonicity can be either %q or %q", ValidationMonotonicIncreasing, ValidationMonotonicDecreasing) } - case "list(string)": + case OptionTypeListString: var listOfStrings []string err := json.Unmarshal([]byte(value), &listOfStrings) if err != nil { diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 7bcea8f..f817280 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -27,6 +27,7 @@ func TestParameter(t *testing.T) { name = "region" display_name = "Region" type = "string" + form_type = "dropdown" description = <<-EOT # Select the machine image See the [registry](https://container.registry.blah/namespace) for options. @@ -56,6 +57,7 @@ func TestParameter(t *testing.T) { "name": "region", "display_name": "Region", "type": "string", + "form_type": "dropdown", "description": "# Select the machine image\nSee the [registry](https://container.registry.blah/namespace) for options.\n", "mutable": "true", "icon": "/icon/region.svg", @@ -137,6 +139,7 @@ func TestParameter(t *testing.T) { for key, expected := range map[string]string{ "name": "Region", "type": "number", + "form_type": "input", "validation.#": "1", "default": "2", "validation.0.min": "1", @@ -686,13 +689,13 @@ data "coder_parameter" "region" { func TestValueValidatesType(t *testing.T) { t.Parallel() for _, tc := range []struct { - Name, - Type, - Value, - Regex, - RegexError string - Min, - Max int + Name string + Type provider.OptionType + Value string + Regex string + RegexError string + Min int + Max int MinDisabled, MaxDisabled bool Monotonic string Error *regexp.Regexp