From 43253f7d29f5c083c8927c7c63876fc4cf0c107b Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Mar 2025 17:55:40 -0500 Subject: [PATCH 01/34] feat: add form_type argument to parameters --- provider/formtype.go | 110 ++++++++++++++++++++++++++++++++++++++++++ provider/parameter.go | 16 +++++- 2 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 provider/formtype.go diff --git a/provider/formtype.go b/provider/formtype.go new file mode 100644 index 0000000..5912721 --- /dev/null +++ b/provider/formtype.go @@ -0,0 +1,110 @@ +package provider + +import ( + "slices" + + "golang.org/x/xerrors" +) + +type ParameterFormType string + +const ( + ParameterFormTypeDefault ParameterFormType = "" + ParameterFormTypeRadio ParameterFormType = "radio" + ParameterFormTypeInput ParameterFormType = "input" + ParameterFormTypeDropdown ParameterFormType = "dropdown" + ParameterFormTypeCheckbox ParameterFormType = "checkbox" + ParameterFormTypeSwitch ParameterFormType = "switch" + ParameterFormTypeMultiSelect ParameterFormType = "multi-select" + ParameterFormTypeTagInput ParameterFormType = "tag-input" +) + +func ParameterFormTypes() []ParameterFormType { + return []ParameterFormType{ + ParameterFormTypeDefault, + ParameterFormTypeRadio, + ParameterFormTypeInput, + ParameterFormTypeDropdown, + ParameterFormTypeCheckbox, + ParameterFormTypeSwitch, + ParameterFormTypeMultiSelect, + ParameterFormTypeTagInput, + } +} + +// 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. +// | Type | Options | Specified Form Type | form_type | Notes | +// |-------------------|---------|---------------------|----------------|--------------------------------| +// | `string` `number` | Y | | `radio` | | +// | `string` `number` | Y | `dropdown` | `dropdown` | | +// | `string` `number` | N | | `input` | | +// | `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[string]map[bool][]ParameterFormType{ + "string": { + true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, + false: {ParameterFormTypeInput}, + }, + "number": { + true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, + false: {ParameterFormTypeInput}, + }, + "bool": { + true: {ParameterFormTypeRadio}, + false: {ParameterFormTypeCheckbox, ParameterFormTypeSwitch}, + }, + "list(string)": { + true: {ParameterFormTypeRadio, ParameterFormTypeMultiSelect}, + false: {ParameterFormTypeTagInput}, + }, +} + +// ValidateFormType handles the truth table for the valid set of `type` and +// `form_type` options. +// | Type | Options | Specified Form Type | form_type | Notes | +// |-------------------|---------|---------------------|----------------|--------------------------------| +// | `string` `number` | Y | | `radio` | | +// | `string` `number` | Y | `dropdown` | `dropdown` | | +// | `string` `number` | N | | `input` | | +// | `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` | +func ValidateFormType(paramType string, optionCount int, specifiedFormType ParameterFormType) (ParameterFormType, string, error) { + allowed, ok := formTypeTruthTable[paramType][optionCount > 0] + if !ok || len(allowed) == 0 { + return specifiedFormType, paramType, 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 specifiedFormType, paramType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType) + } + + // Special case + if paramType == "list(string)" && specifiedFormType == ParameterFormTypeMultiSelect { + return ParameterFormTypeMultiSelect, "string", nil + } + + return specifiedFormType, paramType, 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/parameter.go b/provider/parameter.go index 1345f4d..d0ff476 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -51,6 +51,7 @@ type Parameter struct { DisplayName string `mapstructure:"display_name"` Description string Type string + FormType ParameterFormType `mapstructure:"form_type"` 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,10 @@ func parameterDataSource() *schema.Resource { } } + // Validate options + var optionType string + parameter.FormType, optionType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType) + if len(parameter.Option) > 0 { names := map[string]interface{}{} values := map[string]interface{}{} @@ -161,7 +168,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 } @@ -206,6 +213,13 @@ func parameterDataSource() *schema.Resource { ValidateFunc: validation.StringInSlice([]string{"number", "string", "bool", "list(string)"}, 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()), ", ")), + }, "mutable": { Type: schema.TypeBool, Optional: true, From 2d79a973652edf314003ec28260db667c11296fc Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Mar 2025 18:08:46 -0500 Subject: [PATCH 02/34] fix param order --- provider/formtype.go | 10 +++++----- provider/parameter.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/provider/formtype.go b/provider/formtype.go index 5912721..4eb8f9d 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -78,10 +78,10 @@ var formTypeTruthTable = map[string]map[bool][]ParameterFormType{ // | `list(string)` | Y | | `radio` | | // | `list(string)` | N | | `tag-select` | | // | `list(string)` | Y | `multi-select` | `multi-select` | Option values will be `string` | -func ValidateFormType(paramType string, optionCount int, specifiedFormType ParameterFormType) (ParameterFormType, string, error) { +func ValidateFormType(paramType string, optionCount int, specifiedFormType ParameterFormType) (string, ParameterFormType, error) { allowed, ok := formTypeTruthTable[paramType][optionCount > 0] if !ok || len(allowed) == 0 { - return specifiedFormType, paramType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType) + return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType) } if specifiedFormType == ParameterFormTypeDefault { @@ -90,15 +90,15 @@ func ValidateFormType(paramType string, optionCount int, specifiedFormType Param } if !slices.Contains(allowed, specifiedFormType) { - return specifiedFormType, paramType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType) + return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType) } // Special case if paramType == "list(string)" && specifiedFormType == ParameterFormTypeMultiSelect { - return ParameterFormTypeMultiSelect, "string", nil + return "string", ParameterFormTypeMultiSelect, nil } - return specifiedFormType, paramType, nil + return paramType, specifiedFormType, nil } func toStrings[A ~string](l []A) []string { diff --git a/provider/parameter.go b/provider/parameter.go index d0ff476..5e6c866 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -154,7 +154,7 @@ func parameterDataSource() *schema.Resource { // Validate options var optionType string - parameter.FormType, optionType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType) + optionType, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType) if len(parameter.Option) > 0 { names := map[string]interface{}{} From 4bafcc632401bbdec9db69524e82dbb2fbf92bf4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Mar 2025 18:10:42 -0500 Subject: [PATCH 03/34] add error type --- provider/formtype.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/provider/formtype.go b/provider/formtype.go index 4eb8f9d..85dbc16 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -17,6 +17,8 @@ const ( ParameterFormTypeSwitch ParameterFormType = "switch" ParameterFormTypeMultiSelect ParameterFormType = "multi-select" ParameterFormTypeTagInput ParameterFormType = "tag-input" + //ParameterFormTypeTextArea ParameterFormType = "textarea" + ParameterFormTypeError ParameterFormType = "error" ) func ParameterFormTypes() []ParameterFormType { @@ -29,6 +31,8 @@ func ParameterFormTypes() []ParameterFormType { ParameterFormTypeSwitch, ParameterFormTypeMultiSelect, ParameterFormTypeTagInput, + //ParameterFormTypeTextArea, + ParameterFormTypeError, } } From 19d6749f0ff0794c8504f7acefb410b7ab7b6cca Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 13 Mar 2025 18:21:18 -0500 Subject: [PATCH 04/34] apply default form_type to state --- provider/parameter.go | 4 ++++ provider/parameter_test.go | 3 +++ 2 files changed, 7 insertions(+) diff --git a/provider/parameter.go b/provider/parameter.go index 5e6c866..6a9cb65 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -155,6 +155,10 @@ func parameterDataSource() *schema.Resource { // Validate options var optionType string optionType, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType) + if err != nil { + return diag.FromErr(err) + } + rd.Set("form_type", parameter.FormType) if len(parameter.Option) > 0 { names := map[string]interface{}{} diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 7bcea8f..bbc601a 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 = "radio" 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": "radio", "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", From 9fc221c149fb5574b086e8a8e312e42b2e3202fa Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Mar 2025 10:18:29 -0500 Subject: [PATCH 05/34] fix parsing form_type from terraform --- provider/parameter.go | 2 +- provider/parameter_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/provider/parameter.go b/provider/parameter.go index 6a9cb65..0acebb6 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -51,7 +51,7 @@ type Parameter struct { DisplayName string `mapstructure:"display_name"` Description string Type string - FormType ParameterFormType `mapstructure:"form_type"` + FormType ParameterFormType Mutable bool Default string Icon string diff --git a/provider/parameter_test.go b/provider/parameter_test.go index bbc601a..e3b2f36 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -27,7 +27,7 @@ func TestParameter(t *testing.T) { name = "region" display_name = "Region" type = "string" - form_type = "radio" + form_type = "dropdown" description = <<-EOT # Select the machine image See the [registry](https://container.registry.blah/namespace) for options. @@ -57,7 +57,7 @@ func TestParameter(t *testing.T) { "name": "region", "display_name": "Region", "type": "string", - "form_type": "radio", + "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", From b80ab8f6c3fb0f049d6a3e7317322eb7fbf4165c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 14 Mar 2025 10:23:15 -0500 Subject: [PATCH 06/34] chore: multi select form control allows options to be string --- provider/parameter.go | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/provider/parameter.go b/provider/parameter.go index 0acebb6..94d6c03 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -181,9 +181,35 @@ 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 == "list(string)" && optionType == "string" { + // 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 + err = json.Unmarshal([]byte(parameter.Default), &defaultValues) + if err != nil { + return diag.Errorf("default value %q is not a list of strings", parameter.Default) + } + + 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) + } } } } From 50c6a0d6c003720c6ee5501c1c610817126b8018 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Mar 2025 10:23:47 -0500 Subject: [PATCH 07/34] chore: add slider and 'form_type_metadata' argument to parameter --- provider/formtype.go | 25 +++++++++---------------- provider/parameter.go | 6 ++++++ 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/provider/formtype.go b/provider/formtype.go index 85dbc16..956a227 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -11,14 +11,15 @@ 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" ParameterFormTypeTagInput ParameterFormType = "tag-input" - //ParameterFormTypeTextArea ParameterFormType = "textarea" - ParameterFormTypeError ParameterFormType = "error" + ParameterFormTypeTextArea ParameterFormType = "textarea" + ParameterFormTypeError ParameterFormType = "error" ) func ParameterFormTypes() []ParameterFormType { @@ -26,12 +27,13 @@ func ParameterFormTypes() []ParameterFormType { ParameterFormTypeDefault, ParameterFormTypeRadio, ParameterFormTypeInput, + ParameterFormTypeSlider, ParameterFormTypeDropdown, ParameterFormTypeCheckbox, ParameterFormTypeSwitch, ParameterFormTypeMultiSelect, ParameterFormTypeTagInput, - //ParameterFormTypeTextArea, + ParameterFormTypeTextArea, ParameterFormTypeError, } } @@ -44,6 +46,8 @@ func ParameterFormTypes() []ParameterFormType { // | `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` | | @@ -53,11 +57,11 @@ func ParameterFormTypes() []ParameterFormType { var formTypeTruthTable = map[string]map[bool][]ParameterFormType{ "string": { true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, - false: {ParameterFormTypeInput}, + false: {ParameterFormTypeInput, ParameterFormTypeTextArea}, }, "number": { true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, - false: {ParameterFormTypeInput}, + false: {ParameterFormTypeInput, ParameterFormTypeSlider}, }, "bool": { true: {ParameterFormTypeRadio}, @@ -71,17 +75,6 @@ var formTypeTruthTable = map[string]map[bool][]ParameterFormType{ // ValidateFormType handles the truth table for the valid set of `type` and // `form_type` options. -// | Type | Options | Specified Form Type | form_type | Notes | -// |-------------------|---------|---------------------|----------------|--------------------------------| -// | `string` `number` | Y | | `radio` | | -// | `string` `number` | Y | `dropdown` | `dropdown` | | -// | `string` `number` | N | | `input` | | -// | `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` | func ValidateFormType(paramType string, optionCount int, specifiedFormType ParameterFormType) (string, ParameterFormType, error) { allowed, ok := formTypeTruthTable[paramType][optionCount > 0] if !ok || len(allowed) == 0 { diff --git a/provider/parameter.go b/provider/parameter.go index 94d6c03..dbde34b 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -276,6 +276,12 @@ func parameterDataSource() *schema.Resource { return nil, nil }, }, + "form_type_metadata": { + Type: schema.TypeString, + Default: `{}`, + Description: "JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI.", + Optional: true, + }, "option": { Type: schema.TypeList, Description: "Each `option` block defines a value for a user to select from.", From c0d38d342812d81d629d6962a11c81231debde8a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 17 Mar 2025 10:24:24 -0500 Subject: [PATCH 08/34] move code block --- provider/parameter.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/provider/parameter.go b/provider/parameter.go index dbde34b..62ef347 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -250,6 +250,12 @@ func parameterDataSource() *schema.Resource { ValidateFunc: validation.StringInSlice(toStrings(ParameterFormTypes()), false), Description: fmt.Sprintf("The type of this parameter. Must be one of: [%s].", strings.Join(toStrings(ParameterFormTypes()), ", ")), }, + "form_type_metadata": { + Type: schema.TypeString, + Default: `{}`, + Description: "JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI.", + Optional: true, + }, "mutable": { Type: schema.TypeBool, Optional: true, @@ -276,12 +282,6 @@ func parameterDataSource() *schema.Resource { return nil, nil }, }, - "form_type_metadata": { - Type: schema.TypeString, - Default: `{}`, - Description: "JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI.", - Optional: true, - }, "option": { Type: schema.TypeList, Description: "Each `option` block defines a value for a user to select from.", From 0b9495519201040030702621d68ba8f20fbce410 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Mar 2025 11:05:06 -0500 Subject: [PATCH 09/34] fixup error message --- provider/formtype.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/formtype.go b/provider/formtype.go index 956a227..f4d8d2a 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -87,7 +87,7 @@ func ValidateFormType(paramType string, optionCount int, specifiedFormType Param } if !slices.Contains(allowed, specifiedFormType) { - return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType) + return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", specifiedFormType) } // Special case From 05e682d83bf835d99ef376295606f84207eb3f15 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 20 Mar 2025 14:32:16 -0500 Subject: [PATCH 10/34] add invalid boolean validation field --- provider/parameter.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/provider/parameter.go b/provider/parameter.go index 62ef347..efdbe05 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -34,6 +34,7 @@ type Validation struct { Max int MaxDisabled bool `mapstructure:"max_disabled"` + Invalid bool Monotonic string Regex string @@ -363,6 +364,11 @@ func parameterDataSource() *schema.Resource { Description: "A regex for the input parameter to match against.", Optional: true, }, + "invalid": { + Type: schema.TypeBool, + Optional: true, + Description: "If invalid is 'true', the error will be shown.", + }, "error": { Type: schema.TypeString, Optional: true, @@ -452,6 +458,10 @@ func valueIsType(typ, value string) diag.Diagnostics { } func (v *Validation) Valid(typ, value string) error { + if v.Invalid { + return v.errorRendered(value) + } + if typ != "number" { if !v.MinDisabled { return fmt.Errorf("a min cannot be specified for a %s type", typ) From 0dfab5880f2115c36ab4d95e15f4b0022fee7e04 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 24 Mar 2025 08:56:03 -0500 Subject: [PATCH 11/34] tag-input -> tag-select --- provider/formtype.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/provider/formtype.go b/provider/formtype.go index f4d8d2a..30f3410 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -17,7 +17,7 @@ const ( ParameterFormTypeCheckbox ParameterFormType = "checkbox" ParameterFormTypeSwitch ParameterFormType = "switch" ParameterFormTypeMultiSelect ParameterFormType = "multi-select" - ParameterFormTypeTagInput ParameterFormType = "tag-input" + ParameterFormTypeTagSelect ParameterFormType = "tag-select" ParameterFormTypeTextArea ParameterFormType = "textarea" ParameterFormTypeError ParameterFormType = "error" ) @@ -32,7 +32,7 @@ func ParameterFormTypes() []ParameterFormType { ParameterFormTypeCheckbox, ParameterFormTypeSwitch, ParameterFormTypeMultiSelect, - ParameterFormTypeTagInput, + ParameterFormTypeTagSelect, ParameterFormTypeTextArea, ParameterFormTypeError, } @@ -69,7 +69,7 @@ var formTypeTruthTable = map[string]map[bool][]ParameterFormType{ }, "list(string)": { true: {ParameterFormTypeRadio, ParameterFormTypeMultiSelect}, - false: {ParameterFormTypeTagInput}, + false: {ParameterFormTypeTagSelect}, }, } From e971837ce57776d5d58bce071f43dc69564bb484 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 3 Apr 2025 12:02:51 -0500 Subject: [PATCH 12/34] chore: use constants over string literals for option type --- provider/formtype.go | 45 ++++++++++++++++++++++++++++++++++--------- provider/parameter.go | 32 +++++++++++++++--------------- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/provider/formtype.go b/provider/formtype.go index 30f3410..686e305 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -6,6 +6,26 @@ import ( "golang.org/x/xerrors" ) +// OptionType is a type of option that can be used in the 'type' argument of +// a parameter. +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, + } +} + type ParameterFormType string const ( @@ -22,12 +42,13 @@ const ( ParameterFormTypeError ParameterFormType = "error" ) +// ParameterFormTypes should be kept in sync with the enum list above. func ParameterFormTypes() []ParameterFormType { return []ParameterFormType{ ParameterFormTypeDefault, ParameterFormTypeRadio, - ParameterFormTypeInput, ParameterFormTypeSlider, + ParameterFormTypeInput, ParameterFormTypeDropdown, ParameterFormTypeCheckbox, ParameterFormTypeSwitch, @@ -41,6 +62,8 @@ func ParameterFormTypes() []ParameterFormType { // 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` | | @@ -54,20 +77,20 @@ func ParameterFormTypes() []ParameterFormType { // | `list(string)` | Y | | `radio` | | // | `list(string)` | N | | `tag-select` | | // | `list(string)` | Y | `multi-select` | `multi-select` | Option values will be `string` | -var formTypeTruthTable = map[string]map[bool][]ParameterFormType{ - "string": { +var formTypeTruthTable = map[OptionType]map[bool][]ParameterFormType{ + OptionTypeString: { true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, false: {ParameterFormTypeInput, ParameterFormTypeTextArea}, }, - "number": { + OptionTypeNumber: { true: {ParameterFormTypeRadio, ParameterFormTypeDropdown}, false: {ParameterFormTypeInput, ParameterFormTypeSlider}, }, - "bool": { + OptionTypeBoolean: { true: {ParameterFormTypeRadio}, false: {ParameterFormTypeCheckbox, ParameterFormTypeSwitch}, }, - "list(string)": { + OptionTypeListString: { true: {ParameterFormTypeRadio, ParameterFormTypeMultiSelect}, false: {ParameterFormTypeTagSelect}, }, @@ -75,7 +98,11 @@ var formTypeTruthTable = map[string]map[bool][]ParameterFormType{ // ValidateFormType handles the truth table for the valid set of `type` and // `form_type` options. -func ValidateFormType(paramType string, optionCount int, specifiedFormType ParameterFormType) (string, ParameterFormType, error) { +// 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) @@ -91,8 +118,8 @@ func ValidateFormType(paramType string, optionCount int, specifiedFormType Param } // Special case - if paramType == "list(string)" && specifiedFormType == ParameterFormTypeMultiSelect { - return "string", ParameterFormTypeMultiSelect, nil + if paramType == OptionTypeListString && specifiedFormType == ParameterFormTypeMultiSelect { + return OptionTypeListString, ParameterFormTypeMultiSelect, nil } return paramType, specifiedFormType, nil diff --git a/provider/parameter.go b/provider/parameter.go index efdbe05..471dbe5 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -51,7 +51,7 @@ type Parameter struct { Name string DisplayName string `mapstructure:"display_name"` Description string - Type string + Type OptionType FormType ParameterFormType Mutable bool Default string @@ -154,7 +154,7 @@ func parameterDataSource() *schema.Resource { } // Validate options - var optionType string + var optionType OptionType optionType, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType) if err != nil { return diag.FromErr(err) @@ -182,7 +182,7 @@ func parameterDataSource() *schema.Resource { } if parameter.Default != "" { - if parameter.Type == "list(string)" && optionType == "string" { + 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 @@ -241,7 +241,7 @@ 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": { @@ -431,25 +431,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) @@ -457,12 +457,12 @@ func valueIsType(typ, value string) diag.Diagnostics { return nil } -func (v *Validation) Valid(typ, value string) error { +func (v *Validation) Valid(typ OptionType, value string) error { if v.Invalid { return v.errorRendered(value) } - if typ != "number" { + if typ != OptionTypeNumber { if !v.MinDisabled { return fmt.Errorf("a min cannot be specified for a %s type", typ) } @@ -473,16 +473,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 } @@ -497,7 +497,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)) @@ -511,7 +511,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 { From 12aec75e712e02103538902ee3eb0bd9e67f5ba4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 3 Apr 2025 13:57:21 -0500 Subject: [PATCH 13/34] remove invalid option --- provider/parameter.go | 12 ++---------- provider/parameter_test.go | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/provider/parameter.go b/provider/parameter.go index 471dbe5..193b3de 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -34,7 +34,6 @@ type Validation struct { Max int MaxDisabled bool `mapstructure:"max_disabled"` - Invalid bool Monotonic string Regex string @@ -159,6 +158,8 @@ func parameterDataSource() *schema.Resource { if err != nil { return diag.FromErr(err) } + // Set the form_type back in case the value was changed, eg via a + // default. rd.Set("form_type", parameter.FormType) if len(parameter.Option) > 0 { @@ -364,11 +365,6 @@ func parameterDataSource() *schema.Resource { Description: "A regex for the input parameter to match against.", Optional: true, }, - "invalid": { - Type: schema.TypeBool, - Optional: true, - Description: "If invalid is 'true', the error will be shown.", - }, "error": { Type: schema.TypeString, Optional: true, @@ -458,10 +454,6 @@ func valueIsType(typ OptionType, value string) diag.Diagnostics { } func (v *Validation) Valid(typ OptionType, value string) error { - if v.Invalid { - return v.errorRendered(value) - } - if typ != OptionTypeNumber { if !v.MinDisabled { return fmt.Errorf("a min cannot be specified for a %s type", typ) diff --git a/provider/parameter_test.go b/provider/parameter_test.go index e3b2f36..ba2e0b8 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -690,8 +690,8 @@ func TestValueValidatesType(t *testing.T) { t.Parallel() for _, tc := range []struct { Name, - Type, - Value, + Type provider.OptionType + Value string Regex, RegexError string Min, From 0b28b647dc36b19f7720af5f7590e1fc7f86b87c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Thu, 3 Apr 2025 13:58:25 -0500 Subject: [PATCH 14/34] fixup! remove invalid option --- provider/parameter_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/provider/parameter_test.go b/provider/parameter_test.go index ba2e0b8..2fb718c 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -689,8 +689,8 @@ data "coder_parameter" "region" { func TestValueValidatesType(t *testing.T) { t.Parallel() for _, tc := range []struct { - Name, - Type provider.OptionType + Name string + Type provider.OptionType Value string Regex, RegexError string From bf2b6738d07895ddadd60c5684969bbc31698d27 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 09:23:43 -0500 Subject: [PATCH 15/34] testing framework for form_type --- provider/formtype.go | 10 +- provider/formtype_test.go | 215 ++++++++++++++++++++++++++++++++++++++ provider/parameter.go | 11 +- 3 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 provider/formtype_test.go diff --git a/provider/formtype.go b/provider/formtype.go index 686e305..7dcc2d2 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -7,7 +7,10 @@ import ( ) // OptionType is a type of option that can be used in the 'type' argument of -// a parameter. +// 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 ( @@ -26,6 +29,11 @@ func OptionTypes() []OptionType { } } +// 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. +// For example, "multi-select" has the type "list(string)" but the option +// values are "string". type ParameterFormType string const ( diff --git a/provider/formtype_test.go b/provider/formtype_test.go new file mode 100644 index 0000000..f307efa --- /dev/null +++ b/provider/formtype_test.go @@ -0,0 +1,215 @@ +package provider_test + +import ( + "fmt" + "regexp" + "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" +) + +type formTypeTestCase struct { + name string + config string + assert paramAssert + expectError *regexp.Regexp +} + +type paramAssert struct { + Default string + FormType string + Type string + Styling string +} + +type formTypeCheck struct { + formType provider.ParameterFormType + optionType provider.OptionType + optionsExist bool +} + +func TestValidateFormType(t *testing.T) { + t.Parallel() + + //formTypesChecked := make(map[provider.ParameterFormType]map[provider.OptionType]map[bool]struct{}) + formTypesChecked := make(map[formTypeCheck]struct{}) + + const paramName = "test_me" + + cases := []formTypeTestCase{ + { + // When nothing is specified + name: "defaults", + config: ezconfig(paramName, ezconfigOpts{}), + assert: paramAssert{ + Default: "", + FormType: "input", + Type: "string", + Styling: "", + }, + }, + { + name: "string radio", + config: ezconfig(paramName, ezconfigOpts{Options: []string{"foo"}}), + assert: paramAssert{ + Default: "", + FormType: "radio", + Type: "string", + Styling: "", + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + if c.assert.Styling == "" { + c.assert.Styling = "{}" + } + + formTypesChecked[formTypeTest(t, paramName, c)] = struct{}{} + }) + } + + // TODO: assume uncovered cases should fail + t.Run("AssumeErrorCases", func(t *testing.T) { + t.Skip() + // requiredChecks loops through all possible form_type and option_type + // combinations. + requiredChecks := make([]formTypeCheck, 0) + //requiredChecks := make(map[provider.ParameterFormType][]provider.OptionType) + for _, ft := range provider.ParameterFormTypes() { + //requiredChecks[ft] = make([]provider.OptionType, 0) + for _, ot := range provider.OptionTypes() { + requiredChecks = append(requiredChecks, formTypeCheck{ + formType: ft, + optionType: ot, + optionsExist: false, + }) + requiredChecks = append(requiredChecks, formTypeCheck{ + formType: ft, + optionType: ot, + optionsExist: true, + }) + //requiredChecks[ft] = append(requiredChecks[ft], ot) + } + } + + for _, check := range requiredChecks { + _, alreadyChecked := formTypesChecked[check] + if alreadyChecked { + continue + } + + // Assume it will fail + + } + + //checkedFormTypes := make([]provider.ParameterFormType, 0) + //for ft, ot := range formTypesChecked { + // checkedFormTypes = append(checkedFormTypes, ft) + // var _ = ot + //} + // + //// Fist check all form types have at least 1 test. + //require.ElementsMatch(t, provider.ParameterFormTypes(), checkedFormTypes, "some form types are missing tests") + // + //// Then check each form type has tried with each option type + //for expectedFt, expectedOptionTypes := range requiredChecks { + // actual := formTypesChecked[expectedFt] + // + // assert.Equalf(t, expectedOptionTypes, maps.Keys(actual), "some option types are missing for form type %s", expectedFt) + // + // // Also check for a true/false + // for _, expectedOptionType := range expectedOptionTypes { + // assert.Equalf(t, []bool{true, false}, maps.Keys(actual[expectedOptionType]), "options should be both present and absent for form type %q, option type %q", expectedFt, expectedOptionType) + // } + //} + }) +} + +type ezconfigOpts struct { + Options []string + FormType string + OptionType string + Default string +} + +func ezconfig(paramName string, cfg ezconfigOpts) string { + var body strings.Builder + if cfg.Default != "" { + body.WriteString(fmt.Sprintf("default = %q\n", cfg.Default)) + } + 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)) + } + + for i, opt := range cfg.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 coderParamHCL(paramName, body.String()) +} + +func coderParamHCL(paramName string, body string) string { + return fmt.Sprintf(` + provider "coder" { + } + data "coder_parameter" "%s" { + name = "%s" + %s + } + `, paramName, paramName, body) +} + +func formTypeTest(t *testing.T, paramName string, c formTypeTestCase) formTypeCheck { + var check formTypeCheck + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + ProviderFactories: coderFactory(), + Steps: []resource.TestStep{ + { + Config: c.config, + Check: 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] + + assert.Equal(t, c.assert.Default, param.Primary.Attributes["default"], "default value") + assert.Equal(t, c.assert.FormType, param.Primary.Attributes["form_type"], "form_type") + assert.Equal(t, c.assert.Type, param.Primary.Attributes["type"], "type") + assert.JSONEq(t, c.assert.Styling, param.Primary.Attributes["styling"], "styling") + + ft := provider.ParameterFormType(param.Primary.Attributes["form_type"]) + ot := provider.OptionType(param.Primary.Attributes["type"]) + + // Option blocks are not stored in a very friendly format + // here. + optionsExist := param.Primary.Attributes["option.0.name"] != "" + check = formTypeCheck{ + formType: ft, + optionType: ot, + optionsExist: optionsExist, + } + + return nil + }, + }, + }, + }) + return check +} diff --git a/provider/parameter.go b/provider/parameter.go index 193b3de..b6ef952 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -252,11 +252,12 @@ func parameterDataSource() *schema.Resource { ValidateFunc: validation.StringInSlice(toStrings(ParameterFormTypes()), false), Description: fmt.Sprintf("The type of this parameter. Must be one of: [%s].", strings.Join(toStrings(ParameterFormTypes()), ", ")), }, - "form_type_metadata": { - Type: schema.TypeString, - Default: `{}`, - Description: "JSON encoded string containing the metadata for controlling the appearance of this parameter in the UI.", - Optional: true, + "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, From c0be47239c570a69a415b15e0e6bd13f0b47cfbf Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 10:10:40 -0500 Subject: [PATCH 16/34] polishig up extra code and syntax --- provider/formtype_test.go | 317 +++++++++++++++++++++++++------------- 1 file changed, 207 insertions(+), 110 deletions(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index f307efa..e638caf 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -16,144 +16,240 @@ import ( type formTypeTestCase struct { name string - config string + config formTypeCheck assert paramAssert expectError *regexp.Regexp } type paramAssert struct { - Default string - FormType string - Type string + FormType provider.ParameterFormType + Type provider.OptionType Styling string } type formTypeCheck struct { - formType provider.ParameterFormType - optionType provider.OptionType - optionsExist bool + formType provider.ParameterFormType + optionType provider.OptionType + options bool +} + +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 := make(map[provider.ParameterFormType]map[provider.OptionType]map[bool]struct{}) - formTypesChecked := make(map[formTypeCheck]struct{}) + formTypesChecked := make(map[string]struct{}) - const paramName = "test_me" + obvious := func(expected provider.ParameterFormType, opts formTypeCheck) formTypeTestCase { + ftname := opts.formType + if ftname == "" { + ftname = "default" + } + return formTypeTestCase{ + name: fmt.Sprintf("%s_%s_%t", + ftname, + opts.optionType, + opts.options, + ), + config: opts, + assert: paramAssert{ + FormType: expected, + Type: opts.optionType, + Styling: "", + }, + expectError: nil, + } + } cases := []formTypeTestCase{ { // When nothing is specified name: "defaults", - config: ezconfig(paramName, ezconfigOpts{}), - assert: paramAssert{ - Default: "", - FormType: "input", - Type: "string", - Styling: "", - }, - }, - { - name: "string radio", - config: ezconfig(paramName, ezconfigOpts{Options: []string{"foo"}}), + config: formTypeCheck{}, assert: paramAssert{ - Default: "", - FormType: "radio", - Type: "string", + FormType: provider.ParameterFormTypeInput, + Type: provider.OptionTypeString, Styling: "", }, }, + // String + obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeString, + }), + obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeRadio, + }), + obvious(provider.ParameterFormTypeDropdown, formTypeCheck{ + options: true, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeDropdown, + }), + obvious(provider.ParameterFormTypeInput, formTypeCheck{ + options: false, + optionType: provider.OptionTypeString, + }), + obvious(provider.ParameterFormTypeTextArea, formTypeCheck{ + options: false, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeTextArea, + }), + // Numbers + obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeNumber, + }), + obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeRadio, + }), + obvious(provider.ParameterFormTypeDropdown, formTypeCheck{ + options: true, + optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeDropdown, + }), + obvious(provider.ParameterFormTypeInput, formTypeCheck{ + options: false, + optionType: provider.OptionTypeNumber, + }), + obvious(provider.ParameterFormTypeSlider, formTypeCheck{ + options: false, + optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeSlider, + }), + // booleans + obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + options: true, + optionType: provider.OptionTypeBoolean, + }), + obvious(provider.ParameterFormTypeCheckbox, formTypeCheck{ + options: false, + optionType: provider.OptionTypeBoolean, + }), + obvious(provider.ParameterFormTypeCheckbox, formTypeCheck{ + options: false, + optionType: provider.OptionTypeBoolean, + formType: provider.ParameterFormTypeCheckbox, + }), + obvious(provider.ParameterFormTypeSwitch, formTypeCheck{ + options: false, + optionType: provider.OptionTypeBoolean, + formType: provider.ParameterFormTypeSwitch, + }), } - for _, c := range cases { - t.Run(c.name, func(t *testing.T) { - t.Parallel() - if c.assert.Styling == "" { - c.assert.Styling = "{}" - } + // TabledCases runs through all the manual test cases + t.Run("TabledCases", func(t *testing.T) { + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + t.Parallel() + if c.assert.Styling == "" { + c.assert.Styling = "{}" + } - formTypesChecked[formTypeTest(t, paramName, c)] = struct{}{} - }) - } + formTypeTest(t, c) + formTypesChecked[c.config.String()] = struct{}{} + }) + } + }) - // TODO: assume uncovered cases should fail + // AssumeErrorCases assumes any uncovered test will return an error. + // This ensures all valid test case paths are covered. t.Run("AssumeErrorCases", func(t *testing.T) { - t.Skip() // requiredChecks loops through all possible form_type and option_type // combinations. requiredChecks := make([]formTypeCheck, 0) //requiredChecks := make(map[provider.ParameterFormType][]provider.OptionType) - for _, ft := range provider.ParameterFormTypes() { + for _, ft := range append(provider.ParameterFormTypes(), "") { //requiredChecks[ft] = make([]provider.OptionType, 0) for _, ot := range provider.OptionTypes() { requiredChecks = append(requiredChecks, formTypeCheck{ - formType: ft, - optionType: ot, - optionsExist: false, + formType: ft, + optionType: ot, + options: false, }) requiredChecks = append(requiredChecks, formTypeCheck{ - formType: ft, - optionType: ot, - optionsExist: true, + formType: ft, + optionType: ot, + options: true, }) - //requiredChecks[ft] = append(requiredChecks[ft], ot) } } for _, check := range requiredChecks { - _, alreadyChecked := formTypesChecked[check] + _, alreadyChecked := formTypesChecked[check.String()] if alreadyChecked { continue } - // Assume it will fail + 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() - //checkedFormTypes := make([]provider.ParameterFormType, 0) - //for ft, ot := range formTypesChecked { - // checkedFormTypes = append(checkedFormTypes, ft) - // var _ = ot - //} - // - //// Fist check all form types have at least 1 test. - //require.ElementsMatch(t, provider.ParameterFormTypes(), checkedFormTypes, "some form types are missing tests") - // - //// Then check each form type has tried with each option type - //for expectedFt, expectedOptionTypes := range requiredChecks { - // actual := formTypesChecked[expectedFt] - // - // assert.Equalf(t, expectedOptionTypes, maps.Keys(actual), "some option types are missing for form type %s", expectedFt) - // - // // Also check for a true/false - // for _, expectedOptionType := range expectedOptionTypes { - // assert.Equalf(t, []bool{true, false}, maps.Keys(actual[expectedOptionType]), "options should be both present and absent for form type %q, option type %q", expectedFt, expectedOptionType) - // } - //} - }) -} + tcText := fmt.Sprintf(` + obvious(%s, ezconfigOpts{ + Options: %t, + OptionType: %q, + FormType: %q, + }), + `, "", check.options, check.optionType, check.formType) + t.Logf("To construct this test case:\n%s", tcText) + formTypeTest(t, fc) + }) -type ezconfigOpts struct { - Options []string - FormType string - OptionType string - Default string + } + }) } -func ezconfig(paramName string, cfg ezconfigOpts) string { +func ezconfig(paramName string, cfg formTypeCheck) string { var body strings.Builder - if cfg.Default != "" { - body.WriteString(fmt.Sprintf("default = %q\n", cfg.Default)) + //if cfg.Default != "" { + // body.WriteString(fmt.Sprintf("default = %q\n", cfg.Default)) + //} + if cfg.formType != "" { + body.WriteString(fmt.Sprintf("form_type = %q\n", cfg.formType)) } - 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.OptionType != "" { - body.WriteString(fmt.Sprintf("type = %q\n", cfg.OptionType)) + + var options []string + if cfg.options { + switch cfg.optionType { + case provider.OptionTypeString: + options = []string{"foo"} + case provider.OptionTypeBoolean: + options = []string{"true", "false"} + case provider.OptionTypeNumber: + options = []string{"1"} + case provider.OptionTypeListString: + options = []string{`["red", "blue"]`} + default: + panic(fmt.Sprintf("unknown option type %q when generating options", cfg.optionType)) + } } - for i, opt := range cfg.Options { + 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)) @@ -174,42 +270,43 @@ func coderParamHCL(paramName string, body string) string { `, paramName, paramName, body) } -func formTypeTest(t *testing.T, paramName string, c formTypeTestCase) formTypeCheck { - var check formTypeCheck +func formTypeTest(t *testing.T, c formTypeTestCase) { + const paramName = "test_param" + + 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] + + //assert.Equal(t, c.assert.Default, param.Primary.Attributes["default"], "default value") + assert.Equal(t, string(c.assert.FormType), param.Primary.Attributes["form_type"], "form_type") + assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") + assert.JSONEq(t, c.assert.Styling, param.Primary.Attributes["styling"], "styling") + + //ft := provider.ParameterFormType(param.Primary.Attributes["form_type"]) + //ot := provider.OptionType(param.Primary.Attributes["type"]) + + // Option blocks are not stored in a very friendly format + // here. + //options := param.Primary.Attributes["option.0.name"] != "" + + return nil + } + if c.expectError != nil { + checkFn = nil + } + resource.Test(t, resource.TestCase{ IsUnitTest: true, ProviderFactories: coderFactory(), Steps: []resource.TestStep{ { - Config: c.config, - Check: 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] - - assert.Equal(t, c.assert.Default, param.Primary.Attributes["default"], "default value") - assert.Equal(t, c.assert.FormType, param.Primary.Attributes["form_type"], "form_type") - assert.Equal(t, c.assert.Type, param.Primary.Attributes["type"], "type") - assert.JSONEq(t, c.assert.Styling, param.Primary.Attributes["styling"], "styling") - - ft := provider.ParameterFormType(param.Primary.Attributes["form_type"]) - ot := provider.OptionType(param.Primary.Attributes["type"]) - - // Option blocks are not stored in a very friendly format - // here. - optionsExist := param.Primary.Attributes["option.0.name"] != "" - check = formTypeCheck{ - formType: ft, - optionType: ot, - optionsExist: optionsExist, - } - - return nil - }, + Config: ezconfig(paramName, c.config), + Check: checkFn, + ExpectError: c.expectError, }, }, }) - return check } From cd52caee8ac25a122c5eb5b09df112ef917665d3 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 10:33:53 -0500 Subject: [PATCH 17/34] finish all table test cases --- provider/formtype.go | 6 +- provider/formtype_test.go | 160 +++++++++++++++++++++++++++----------- 2 files changed, 118 insertions(+), 48 deletions(-) diff --git a/provider/formtype.go b/provider/formtype.go index 7dcc2d2..20f6401 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -8,7 +8,9 @@ import ( // 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 +// +// 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 @@ -127,7 +129,7 @@ func ValidateFormType(paramType OptionType, optionCount int, specifiedFormType P // Special case if paramType == OptionTypeListString && specifiedFormType == ParameterFormTypeMultiSelect { - return OptionTypeListString, ParameterFormTypeMultiSelect, nil + return OptionTypeString, ParameterFormTypeMultiSelect, nil } return paramType, specifiedFormType, nil diff --git a/provider/formtype_test.go b/provider/formtype_test.go index e638caf..027a39e 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -28,9 +28,11 @@ type paramAssert struct { } type formTypeCheck struct { - formType provider.ParameterFormType - optionType provider.OptionType - options bool + formType provider.ParameterFormType + optionType provider.OptionType + defValue string + options bool + customOptions []string } func (c formTypeCheck) String() string { @@ -40,10 +42,12 @@ func (c formTypeCheck) String() string { func TestValidateFormType(t *testing.T) { t.Parallel() - //formTypesChecked := make(map[provider.ParameterFormType]map[provider.OptionType]map[bool]struct{}) + // 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{}) - obvious := func(expected provider.ParameterFormType, opts formTypeCheck) formTypeTestCase { + expectType := func(expected provider.ParameterFormType, opts formTypeCheck) formTypeTestCase { ftname := opts.formType if ftname == "" { ftname = "default" @@ -64,6 +68,12 @@ func TestValidateFormType(t *testing.T) { } } + // obvious just assumes the FormType in the check is the expected + // FormType. Using `expectType` these fields can differ + obvious := func(opts formTypeCheck) formTypeTestCase { + return expectType(opts.formType, opts) + } + cases := []formTypeTestCase{ { // When nothing is specified @@ -75,77 +85,125 @@ func TestValidateFormType(t *testing.T) { Styling: "", }, }, - // String - obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + // All default behaviors. Essentially legacy behavior. + // String + expectType(provider.ParameterFormTypeRadio, formTypeCheck{ options: true, optionType: provider.OptionTypeString, }), - obvious(provider.ParameterFormTypeRadio, formTypeCheck{ - options: true, + expectType(provider.ParameterFormTypeInput, formTypeCheck{ + options: false, optionType: provider.OptionTypeString, - formType: provider.ParameterFormTypeRadio, }), - obvious(provider.ParameterFormTypeDropdown, formTypeCheck{ + // 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 + obvious(formTypeCheck{ options: true, optionType: provider.OptionTypeString, formType: provider.ParameterFormTypeDropdown, }), - obvious(provider.ParameterFormTypeInput, formTypeCheck{ + obvious(formTypeCheck{ + options: true, + optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeRadio, + }), + obvious(formTypeCheck{ options: false, optionType: provider.OptionTypeString, + formType: provider.ParameterFormTypeInput, }), - obvious(provider.ParameterFormTypeTextArea, formTypeCheck{ + obvious(formTypeCheck{ options: false, optionType: provider.OptionTypeString, formType: provider.ParameterFormTypeTextArea, }), - // Numbers - obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + // Number + obvious(formTypeCheck{ options: true, optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeDropdown, }), - obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + obvious(formTypeCheck{ options: true, optionType: provider.OptionTypeNumber, formType: provider.ParameterFormTypeRadio, }), - obvious(provider.ParameterFormTypeDropdown, formTypeCheck{ - options: true, - optionType: provider.OptionTypeNumber, - formType: provider.ParameterFormTypeDropdown, - }), - obvious(provider.ParameterFormTypeInput, formTypeCheck{ + obvious(formTypeCheck{ options: false, optionType: provider.OptionTypeNumber, + formType: provider.ParameterFormTypeInput, }), - obvious(provider.ParameterFormTypeSlider, formTypeCheck{ + obvious(formTypeCheck{ options: false, optionType: provider.OptionTypeNumber, formType: provider.ParameterFormTypeSlider, }), - // booleans - obvious(provider.ParameterFormTypeRadio, formTypeCheck{ + // Boolean + obvious(formTypeCheck{ options: true, optionType: provider.OptionTypeBoolean, + formType: provider.ParameterFormTypeRadio, }), - obvious(provider.ParameterFormTypeCheckbox, formTypeCheck{ + obvious(formTypeCheck{ options: false, optionType: provider.OptionTypeBoolean, + formType: provider.ParameterFormTypeSwitch, }), - obvious(provider.ParameterFormTypeCheckbox, formTypeCheck{ + obvious(formTypeCheck{ options: false, optionType: provider.OptionTypeBoolean, formType: provider.ParameterFormTypeCheckbox, }), - obvious(provider.ParameterFormTypeSwitch, formTypeCheck{ + // List(string) + obvious(formTypeCheck{ + options: true, + optionType: provider.OptionTypeListString, + formType: provider.ParameterFormTypeRadio, + }), + obvious(formTypeCheck{ + options: true, + optionType: provider.OptionTypeListString, + formType: provider.ParameterFormTypeMultiSelect, + customOptions: []string{"red", "blue", "green"}, + defValue: `["red", "blue"]`, + }), + obvious(formTypeCheck{ options: false, - optionType: provider.OptionTypeBoolean, - formType: provider.ParameterFormTypeSwitch, + optionType: provider.OptionTypeListString, + formType: provider.ParameterFormTypeTagSelect, }), } - // TabledCases runs through all the manual test cases - t.Run("TabledCases", func(t *testing.T) { + 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() @@ -154,6 +212,10 @@ func TestValidateFormType(t *testing.T) { } formTypeTest(t, c) + 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{}{} }) } @@ -221,11 +283,11 @@ func TestValidateFormType(t *testing.T) { }) } -func ezconfig(paramName string, cfg formTypeCheck) string { +func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf string) { var body strings.Builder - //if cfg.Default != "" { - // body.WriteString(fmt.Sprintf("default = %q\n", cfg.Default)) - //} + 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)) } @@ -233,17 +295,21 @@ func ezconfig(paramName string, cfg formTypeCheck) string { body.WriteString(fmt.Sprintf("type = %q\n", cfg.optionType)) } - var options []string - if cfg.options { + 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"]` default: panic(fmt.Sprintf("unknown option type %q when generating options", cfg.optionType)) } @@ -256,23 +322,25 @@ func ezconfig(paramName string, cfg formTypeCheck) string { body.WriteString("}\n") } - return coderParamHCL(paramName, body.String()) -} - -func coderParamHCL(paramName string, body string) string { - return fmt.Sprintf(` + if cfg.defValue == "" { + cfg.defValue = defaultValue + } + return cfg.defValue, fmt.Sprintf(` provider "coder" { } data "coder_parameter" "%s" { name = "%s" %s } - `, paramName, paramName, body) + `, paramName, paramName, body.String()) } func formTypeTest(t *testing.T, c formTypeTestCase) { + t.Helper() const paramName = "test_param" + def, tf := ezconfig(paramName, c.config) + t.Logf("Terraform config:\n%s", tf) checkFn := func(state *terraform.State) error { require.Len(t, state.Modules, 1) require.Len(t, state.Modules[0].Resources, 1) @@ -280,7 +348,7 @@ func formTypeTest(t *testing.T, c formTypeTestCase) { key := strings.Join([]string{"data", "coder_parameter", paramName}, ".") param := state.Modules[0].Resources[key] - //assert.Equal(t, c.assert.Default, param.Primary.Attributes["default"], "default value") + assert.Equal(t, def, param.Primary.Attributes["default"], "default value") assert.Equal(t, string(c.assert.FormType), param.Primary.Attributes["form_type"], "form_type") assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") assert.JSONEq(t, c.assert.Styling, param.Primary.Attributes["styling"], "styling") @@ -303,7 +371,7 @@ func formTypeTest(t *testing.T, c formTypeTestCase) { ProviderFactories: coderFactory(), Steps: []resource.TestStep{ { - Config: ezconfig(paramName, c.config), + Config: tf, Check: checkFn, ExpectError: c.expectError, }, From fec030d6d8e3bef452b8e229265d86c1050f5edd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 10:38:46 -0500 Subject: [PATCH 18/34] final touches on the test --- provider/formtype_test.go | 44 +++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index 027a39e..0bc9963 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -14,6 +14,7 @@ import ( "github.com/coder/terraform-provider-coder/v2/provider" ) +// formTypeTestCase is the config for a single test case. type formTypeTestCase struct { name string config formTypeCheck @@ -21,17 +22,19 @@ type formTypeTestCase struct { expectError *regexp.Regexp } +// paramAssert is asserted on the provider's parsed terraform state. type paramAssert struct { FormType provider.ParameterFormType Type provider.OptionType Styling string } +// formTypeCheck is a struct that helps build the terraform config type formTypeCheck struct { formType provider.ParameterFormType optionType provider.OptionType - defValue string options bool + defValue string customOptions []string } @@ -283,18 +286,8 @@ func TestValidateFormType(t *testing.T) { }) } +// ezconfig converts a formTypeCheck into a terraform config string. func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf string) { - 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)) - } - options := cfg.customOptions if cfg.options && len(cfg.customOptions) == 0 { switch cfg.optionType { @@ -309,12 +302,27 @@ func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf stri defaultValue = "1" case provider.OptionTypeListString: options = []string{`["red", "blue"]`} - defaultValue = `["red"]` + 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)) + } + for i, opt := range options { body.WriteString("option {\n") body.WriteString(fmt.Sprintf("name = \"val_%d\"\n", i)) @@ -322,9 +330,6 @@ func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf stri body.WriteString("}\n") } - if cfg.defValue == "" { - cfg.defValue = defaultValue - } return cfg.defValue, fmt.Sprintf(` provider "coder" { } @@ -353,13 +358,6 @@ func formTypeTest(t *testing.T, c formTypeTestCase) { assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") assert.JSONEq(t, c.assert.Styling, param.Primary.Attributes["styling"], "styling") - //ft := provider.ParameterFormType(param.Primary.Attributes["form_type"]) - //ot := provider.OptionType(param.Primary.Attributes["type"]) - - // Option blocks are not stored in a very friendly format - // here. - //options := param.Primary.Attributes["option.0.name"] != "" - return nil } if c.expectError != nil { From f0bca91d87145f0c17165294c9d59e3553d10ce6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 10:46:35 -0500 Subject: [PATCH 19/34] also assert styling --- provider/formtype_test.go | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index 0bc9963..119c716 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -1,8 +1,10 @@ package provider_test import ( + "encoding/json" "fmt" "regexp" + "strconv" "strings" "testing" @@ -26,16 +28,19 @@ type formTypeTestCase struct { type paramAssert struct { FormType provider.ParameterFormType Type provider.OptionType - Styling string + Styling json.RawMessage } // formTypeCheck is a struct that helps build the terraform config type formTypeCheck struct { - formType provider.ParameterFormType - optionType provider.OptionType - options bool - defValue string + 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 { @@ -55,6 +60,18 @@ func TestValidateFormType(t *testing.T) { 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, @@ -65,7 +82,7 @@ func TestValidateFormType(t *testing.T) { assert: paramAssert{ FormType: expected, Type: opts.optionType, - Styling: "", + Styling: opts.styling, }, expectError: nil, } @@ -85,7 +102,7 @@ func TestValidateFormType(t *testing.T) { assert: paramAssert{ FormType: provider.ParameterFormTypeInput, Type: provider.OptionTypeString, - Styling: "", + Styling: []byte("{}"), }, }, // All default behaviors. Essentially legacy behavior. @@ -210,9 +227,6 @@ func TestValidateFormType(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { t.Parallel() - if c.assert.Styling == "" { - c.assert.Styling = "{}" - } formTypeTest(t, c) if _, ok := formTypesChecked[c.config.String()]; ok { @@ -322,6 +336,9 @@ func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf stri 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") @@ -356,7 +373,7 @@ func formTypeTest(t *testing.T, c formTypeTestCase) { assert.Equal(t, def, param.Primary.Attributes["default"], "default value") assert.Equal(t, string(c.assert.FormType), param.Primary.Attributes["form_type"], "form_type") assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") - assert.JSONEq(t, c.assert.Styling, param.Primary.Attributes["styling"], "styling") + assert.JSONEq(t, string(c.assert.Styling), param.Primary.Attributes["styling"], "styling") return nil } From 18c3d05ba1a99d848af99bd19bf9037f75a3be86 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 10:53:20 -0500 Subject: [PATCH 20/34] add a test case for invalid multi selecT --- provider/formtype.go | 4 +++- provider/formtype_test.go | 13 +++++++++++++ provider/parameter.go | 11 ++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/provider/formtype.go b/provider/formtype.go index 20f6401..dc035b9 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -127,7 +127,9 @@ func ValidateFormType(paramType OptionType, optionCount int, specifiedFormType P return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", specifiedFormType) } - // Special case + // 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 } diff --git a/provider/formtype_test.go b/provider/formtype_test.go index 119c716..530af30 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -220,6 +220,19 @@ func TestValidateFormType(t *testing.T) { 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"), + }, } t.Run("TabledTests", func(t *testing.T) { diff --git a/provider/parameter.go b/provider/parameter.go index b6ef952..4eb8a28 100644 --- a/provider/parameter.go +++ b/provider/parameter.go @@ -153,13 +153,17 @@ 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. + // 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 { @@ -187,11 +191,13 @@ func parameterDataSource() *schema.Resource { // 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] @@ -206,7 +212,6 @@ func parameterDataSource() *schema.Resource { parameter.Default, strings.Join(missing, ", "), ) } - } else { _, defaultIsValid := values[parameter.Default] if !defaultIsValid { From a04ffa60aa4e1ad9d1f48d12e8366c4664d6a6c4 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 10:56:52 -0500 Subject: [PATCH 21/34] push up an example multi select --- .../coder_parameter/data-source.tf | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/examples/data-sources/coder_parameter/data-source.tf b/examples/data-sources/coder_parameter/data-source.tf index ac0de7c..07b99c8 100644 --- a/examples/data-sources/coder_parameter/data-source.tf +++ b/examples/data-sources/coder_parameter/data-source.tf @@ -114,4 +114,36 @@ data "coder_parameter" "home_volume_size" { validation { monotonic = "increasing" } +} + +data "coder_parameter" "tools" { + name = "Tools" + description = "What tools do you want to install?" + type = "list(string)" + form_type = "multi-select" + stying = jsonencode({ + disabled = false + }) + default = jsonencode(["git", "docker"]) + + option { + value = "Docker" + name = "docker" + icon = "/icon/docker.svg" + } + option { + value = "Git" + name = "git" + icon = "/icon/git.svg" + } + option { + value = "Golang" + name = "go" + icon = "/icon/go.svg" + } + option { + value = "Typescript" + name = "ts" + icon = "/icon/typescript.svg" + } } \ No newline at end of file From 2b5ff0815acbb57fdca704b7123f9fa3722ab100 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 10:59:32 -0500 Subject: [PATCH 22/34] make gne --- docs/data-sources/parameter.md | 34 ++++++++++++++++++++++++++++++++++ provider/formtype.go | 4 +++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md index e46cf86..80011bc 100644 --- a/docs/data-sources/parameter.md +++ b/docs/data-sources/parameter.md @@ -130,6 +130,38 @@ data "coder_parameter" "home_volume_size" { monotonic = "increasing" } } + +data "coder_parameter" "tools" { + name = "Tools" + description = "What tools do you want to install?" + type = "list(string)" + form_type = "multi-select" + stying = jsonencode({ + disabled = false + }) + default = jsonencode(["git", "docker"]) + + option { + value = "Docker" + name = "docker" + icon = "/icon/docker.svg" + } + option { + value = "Git" + name = "git" + icon = "/icon/git.svg" + } + option { + value = "Golang" + name = "go" + icon = "/icon/go.svg" + } + option { + value = "Typescript" + name = "ts" + icon = "/icon/typescript.svg" + } +} ``` @@ -145,10 +177,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 index dc035b9..de9ad79 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -55,7 +55,9 @@ const ( // ParameterFormTypes should be kept in sync with the enum list above. func ParameterFormTypes() []ParameterFormType { return []ParameterFormType{ - ParameterFormTypeDefault, + // 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, From 18886f84692f4d599cd42160b598a73056096637 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 11:03:47 -0500 Subject: [PATCH 23/34] make fmt --- examples/data-sources/coder_parameter/data-source.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/data-sources/coder_parameter/data-source.tf b/examples/data-sources/coder_parameter/data-source.tf index 07b99c8..5be14ac 100644 --- a/examples/data-sources/coder_parameter/data-source.tf +++ b/examples/data-sources/coder_parameter/data-source.tf @@ -121,10 +121,10 @@ data "coder_parameter" "tools" { description = "What tools do you want to install?" type = "list(string)" form_type = "multi-select" - stying = jsonencode({ + stying = jsonencode({ disabled = false }) - default = jsonencode(["git", "docker"]) + default = jsonencode(["git", "docker"]) option { value = "Docker" From acf5977f26eaa903c57b3a706fe2bf6a31463489 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 11:15:19 -0500 Subject: [PATCH 24/34] use comment instead of json --- examples/data-sources/coder_parameter/data-source.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/data-sources/coder_parameter/data-source.tf b/examples/data-sources/coder_parameter/data-source.tf index 5be14ac..4bd8872 100644 --- a/examples/data-sources/coder_parameter/data-source.tf +++ b/examples/data-sources/coder_parameter/data-source.tf @@ -122,7 +122,7 @@ data "coder_parameter" "tools" { type = "list(string)" form_type = "multi-select" stying = jsonencode({ - disabled = false + # Arbitrary JSON object to be passed to the frontend }) default = jsonencode(["git", "docker"]) From fcdd6bde6ee473f02c186273c64b0cb16be50ab6 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 11:23:23 -0500 Subject: [PATCH 25/34] make gen --- docs/data-sources/parameter.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md index 80011bc..b9dd61a 100644 --- a/docs/data-sources/parameter.md +++ b/docs/data-sources/parameter.md @@ -136,10 +136,10 @@ data "coder_parameter" "tools" { description = "What tools do you want to install?" type = "list(string)" form_type = "multi-select" - stying = jsonencode({ - disabled = false + stying = jsonencode({ + # Arbitrary JSON object to be passed to the frontend }) - default = jsonencode(["git", "docker"]) + default = jsonencode(["git", "docker"]) option { value = "Docker" From 5da70d5ce15144ed16454d7b3b76502a77a2526a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 11:35:07 -0500 Subject: [PATCH 26/34] spelling typo --- docs/data-sources/parameter.md | 2 +- examples/data-sources/coder_parameter/data-source.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md index b9dd61a..81bc80b 100644 --- a/docs/data-sources/parameter.md +++ b/docs/data-sources/parameter.md @@ -136,7 +136,7 @@ data "coder_parameter" "tools" { description = "What tools do you want to install?" type = "list(string)" form_type = "multi-select" - stying = jsonencode({ + styling = jsonencode({ # Arbitrary JSON object to be passed to the frontend }) default = jsonencode(["git", "docker"]) diff --git a/examples/data-sources/coder_parameter/data-source.tf b/examples/data-sources/coder_parameter/data-source.tf index 4bd8872..baf4f7a 100644 --- a/examples/data-sources/coder_parameter/data-source.tf +++ b/examples/data-sources/coder_parameter/data-source.tf @@ -121,7 +121,7 @@ data "coder_parameter" "tools" { description = "What tools do you want to install?" type = "list(string)" form_type = "multi-select" - stying = jsonencode({ + styling = jsonencode({ # Arbitrary JSON object to be passed to the frontend }) default = jsonencode(["git", "docker"]) From 771be40b6dc3687e957836f7dfe03cbbc3dbc22c Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 13:08:46 -0500 Subject: [PATCH 27/34] mark the check up front --- provider/formtype_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index 530af30..3b900b1 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -241,12 +241,12 @@ func TestValidateFormType(t *testing.T) { t.Run(c.name, func(t *testing.T) { t.Parallel() + formTypesChecked[c.config.String()] = struct{}{} formTypeTest(t, c) 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{}{} }) } }) From bb00c257cb6d1dc33e11afa01a1616c70f68abfd Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 13:20:20 -0500 Subject: [PATCH 28/34] cleanup output --- provider/formtype_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index 3b900b1..5f766ff 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -235,7 +235,7 @@ func TestValidateFormType(t *testing.T) { }, } - t.Run("TabledTests", func(t *testing.T) { + 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) { @@ -251,6 +251,12 @@ func TestValidateFormType(t *testing.T) { } }) + 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. // This ensures all valid test case paths are covered. t.Run("AssumeErrorCases", func(t *testing.T) { From d25fcf602956cdfb0420e78113259c793b48621a Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 13:22:53 -0500 Subject: [PATCH 29/34] add test dupe check up front --- provider/formtype_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index 5f766ff..6fa0672 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -240,13 +240,13 @@ func TestValidateFormType(t *testing.T) { for _, c := range cases { t.Run(c.name, func(t *testing.T) { t.Parallel() - - formTypesChecked[c.config.String()] = struct{}{} - formTypeTest(t, c) 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) }) } }) From 89519404c9a15735fc4984d97188ecb403377916 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 14:11:46 -0500 Subject: [PATCH 30/34] adding the example before release The example needs the release. So it's invalid until we push a tag --- docs/data-sources/parameter.md | 32 ------------------- .../coder_parameter/data-source.tf | 32 ------------------- 2 files changed, 64 deletions(-) diff --git a/docs/data-sources/parameter.md b/docs/data-sources/parameter.md index 81bc80b..934ae77 100644 --- a/docs/data-sources/parameter.md +++ b/docs/data-sources/parameter.md @@ -130,38 +130,6 @@ data "coder_parameter" "home_volume_size" { monotonic = "increasing" } } - -data "coder_parameter" "tools" { - name = "Tools" - description = "What tools do you want to install?" - type = "list(string)" - form_type = "multi-select" - styling = jsonencode({ - # Arbitrary JSON object to be passed to the frontend - }) - default = jsonencode(["git", "docker"]) - - option { - value = "Docker" - name = "docker" - icon = "/icon/docker.svg" - } - option { - value = "Git" - name = "git" - icon = "/icon/git.svg" - } - option { - value = "Golang" - name = "go" - icon = "/icon/go.svg" - } - option { - value = "Typescript" - name = "ts" - icon = "/icon/typescript.svg" - } -} ``` diff --git a/examples/data-sources/coder_parameter/data-source.tf b/examples/data-sources/coder_parameter/data-source.tf index baf4f7a..ac0de7c 100644 --- a/examples/data-sources/coder_parameter/data-source.tf +++ b/examples/data-sources/coder_parameter/data-source.tf @@ -114,36 +114,4 @@ data "coder_parameter" "home_volume_size" { validation { monotonic = "increasing" } -} - -data "coder_parameter" "tools" { - name = "Tools" - description = "What tools do you want to install?" - type = "list(string)" - form_type = "multi-select" - styling = jsonencode({ - # Arbitrary JSON object to be passed to the frontend - }) - default = jsonencode(["git", "docker"]) - - option { - value = "Docker" - name = "docker" - icon = "/icon/docker.svg" - } - option { - value = "Git" - name = "git" - icon = "/icon/git.svg" - } - option { - value = "Golang" - name = "go" - icon = "/icon/go.svg" - } - option { - value = "Typescript" - name = "ts" - icon = "/icon/typescript.svg" - } } \ No newline at end of file From 5d485d61f64af9b56368a41390279eafcd354516 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 14:17:25 -0500 Subject: [PATCH 31/34] cleanup test logs --- provider/formtype_test.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index 6fa0672..e3640d3 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -310,9 +310,13 @@ func TestValidateFormType(t *testing.T) { OptionType: %q, FormType: %q, }), - `, "", check.options, check.optionType, check.formType) - t.Logf("To construct this test case:\n%s", tcText) - formTypeTest(t, fc) + //`, "", check.options, check.optionType, check.formType) + var _ = tcText + + probablyPassed := formTypeTest(t, fc) + if !probablyPassed { + t.Logf("To construct this test case:\n%s", tcText) + } }) } @@ -376,12 +380,13 @@ func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf stri `, paramName, paramName, body.String()) } -func formTypeTest(t *testing.T, c formTypeTestCase) { +func formTypeTest(t *testing.T, c formTypeTestCase) bool { t.Helper() const paramName = "test_param" + // probablyPassed is just a guess used for logging. It's not important. + probablyPassed := true def, tf := ezconfig(paramName, c.config) - t.Logf("Terraform config:\n%s", tf) checkFn := func(state *terraform.State) error { require.Len(t, state.Modules, 1) require.Len(t, state.Modules[0].Resources, 1) @@ -389,10 +394,10 @@ func formTypeTest(t *testing.T, c formTypeTestCase) { key := strings.Join([]string{"data", "coder_parameter", paramName}, ".") param := state.Modules[0].Resources[key] - assert.Equal(t, def, param.Primary.Attributes["default"], "default value") - assert.Equal(t, string(c.assert.FormType), param.Primary.Attributes["form_type"], "form_type") - assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") - assert.JSONEq(t, string(c.assert.Styling), param.Primary.Attributes["styling"], "styling") + probablyPassed = probablyPassed && assert.Equal(t, def, param.Primary.Attributes["default"], "default value") + probablyPassed = probablyPassed && assert.Equal(t, string(c.assert.FormType), param.Primary.Attributes["form_type"], "form_type") + probablyPassed = probablyPassed && assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") + probablyPassed = probablyPassed && assert.JSONEq(t, string(c.assert.Styling), param.Primary.Attributes["styling"], "styling") return nil } @@ -411,4 +416,9 @@ func formTypeTest(t *testing.T, c formTypeTestCase) { }, }, }) + + if !probablyPassed { + t.Logf("Terraform config:\n%s", tf) + } + return probablyPassed } From f21c732047fd53970b60cd8a1ea2e8f8632bbaaf Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Mon, 7 Apr 2025 14:22:44 -0500 Subject: [PATCH 32/34] add comment --- provider/formtype_test.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/provider/formtype_test.go b/provider/formtype_test.go index e3640d3..bdbf737 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -257,15 +257,16 @@ func TestValidateFormType(t *testing.T) { return } - // AssumeErrorCases assumes any uncovered test will return an error. - // This ensures all valid test case paths are covered. + // 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) - //requiredChecks := make(map[provider.ParameterFormType][]provider.OptionType) for _, ft := range append(provider.ParameterFormTypes(), "") { - //requiredChecks[ft] = make([]provider.OptionType, 0) for _, ot := range provider.OptionTypes() { requiredChecks = append(requiredChecks, formTypeCheck{ formType: ft, @@ -304,6 +305,8 @@ func TestValidateFormType(t *testing.T) { 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(` obvious(%s, ezconfigOpts{ Options: %t, @@ -311,7 +314,6 @@ func TestValidateFormType(t *testing.T) { FormType: %q, }), //`, "", check.options, check.optionType, check.formType) - var _ = tcText probablyPassed := formTypeTest(t, fc) if !probablyPassed { From f1101291916420503cb0f2752362fed6eb4e6688 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 8 Apr 2025 08:32:09 -0500 Subject: [PATCH 33/34] renaming from PR comments --- provider/formtype.go | 4 +-- provider/formtype_test.go | 66 ++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/provider/formtype.go b/provider/formtype.go index de9ad79..48fbeed 100644 --- a/provider/formtype.go +++ b/provider/formtype.go @@ -12,7 +12,7 @@ import ( // https://developer.hashicorp.com/terraform/language/expressions/types // // The value have to be string literals, as type constraint keywords are not -// supported in providers. :'( +// supported in providers. type OptionType string const ( @@ -33,7 +33,7 @@ func OptionTypes() []OptionType { // 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. +// 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 diff --git a/provider/formtype_test.go b/provider/formtype_test.go index bdbf737..599c2e4 100644 --- a/provider/formtype_test.go +++ b/provider/formtype_test.go @@ -88,9 +88,9 @@ func TestValidateFormType(t *testing.T) { } } - // obvious just assumes the FormType in the check is the expected + // expectSameFormType just assumes the FormType in the check is the expected // FormType. Using `expectType` these fields can differ - obvious := func(opts formTypeCheck) formTypeTestCase { + expectSameFormType := func(opts formTypeCheck) formTypeTestCase { return expectType(opts.formType, opts) } @@ -145,77 +145,77 @@ func TestValidateFormType(t *testing.T) { // ---- New Behavior // String - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: true, optionType: provider.OptionTypeString, formType: provider.ParameterFormTypeDropdown, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: true, optionType: provider.OptionTypeString, formType: provider.ParameterFormTypeRadio, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: false, optionType: provider.OptionTypeString, formType: provider.ParameterFormTypeInput, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: false, optionType: provider.OptionTypeString, formType: provider.ParameterFormTypeTextArea, }), // Number - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: true, optionType: provider.OptionTypeNumber, formType: provider.ParameterFormTypeDropdown, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: true, optionType: provider.OptionTypeNumber, formType: provider.ParameterFormTypeRadio, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: false, optionType: provider.OptionTypeNumber, formType: provider.ParameterFormTypeInput, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: false, optionType: provider.OptionTypeNumber, formType: provider.ParameterFormTypeSlider, }), // Boolean - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: true, optionType: provider.OptionTypeBoolean, formType: provider.ParameterFormTypeRadio, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: false, optionType: provider.OptionTypeBoolean, formType: provider.ParameterFormTypeSwitch, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: false, optionType: provider.OptionTypeBoolean, formType: provider.ParameterFormTypeCheckbox, }), // List(string) - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: true, optionType: provider.OptionTypeListString, formType: provider.ParameterFormTypeRadio, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: true, optionType: provider.OptionTypeListString, formType: provider.ParameterFormTypeMultiSelect, customOptions: []string{"red", "blue", "green"}, defValue: `["red", "blue"]`, }), - obvious(formTypeCheck{ + expectSameFormType(formTypeCheck{ options: false, optionType: provider.OptionTypeListString, formType: provider.ParameterFormTypeTagSelect, @@ -282,8 +282,7 @@ func TestValidateFormType(t *testing.T) { } for _, check := range requiredChecks { - _, alreadyChecked := formTypesChecked[check.String()] - if alreadyChecked { + if _, alreadyChecked := formTypesChecked[check.String()]; alreadyChecked { continue } @@ -308,15 +307,15 @@ func TestValidateFormType(t *testing.T) { // This is just helpful log output to give the boilerplate // to write the manual test. tcText := fmt.Sprintf(` - obvious(%s, ezconfigOpts{ + expectSameFormType(%s, ezconfigOpts{ Options: %t, OptionType: %q, FormType: %q, }), //`, "", check.options, check.optionType, check.formType) - probablyPassed := formTypeTest(t, fc) - if !probablyPassed { + logDebugInfo := formTypeTest(t, fc) + if !logDebugInfo { t.Logf("To construct this test case:\n%s", tcText) } }) @@ -325,8 +324,8 @@ func TestValidateFormType(t *testing.T) { }) } -// ezconfig converts a formTypeCheck into a terraform config string. -func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf string) { +// 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 { @@ -385,10 +384,13 @@ func ezconfig(paramName string, cfg formTypeCheck) (defaultValue string, tf stri func formTypeTest(t *testing.T, c formTypeTestCase) bool { t.Helper() const paramName = "test_param" - // probablyPassed is just a guess used for logging. It's not important. - probablyPassed := true + // 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 := ezconfig(paramName, c.config) + 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) @@ -396,10 +398,10 @@ func formTypeTest(t *testing.T, c formTypeTestCase) bool { key := strings.Join([]string{"data", "coder_parameter", paramName}, ".") param := state.Modules[0].Resources[key] - probablyPassed = probablyPassed && assert.Equal(t, def, param.Primary.Attributes["default"], "default value") - probablyPassed = probablyPassed && assert.Equal(t, string(c.assert.FormType), param.Primary.Attributes["form_type"], "form_type") - probablyPassed = probablyPassed && assert.Equal(t, string(c.assert.Type), param.Primary.Attributes["type"], "type") - probablyPassed = probablyPassed && assert.JSONEq(t, string(c.assert.Styling), param.Primary.Attributes["styling"], "styling") + 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 } @@ -419,8 +421,8 @@ func formTypeTest(t *testing.T, c formTypeTestCase) bool { }, }) - if !probablyPassed { + if !logDebugInfo { t.Logf("Terraform config:\n%s", tf) } - return probablyPassed + return logDebugInfo } From d330f2103b65fb1544667b5bdc2b7c648d43c358 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 8 Apr 2025 08:37:08 -0500 Subject: [PATCH 34/34] make struct field types explicit --- provider/parameter_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/provider/parameter_test.go b/provider/parameter_test.go index 2fb718c..f817280 100644 --- a/provider/parameter_test.go +++ b/provider/parameter_test.go @@ -689,13 +689,13 @@ data "coder_parameter" "region" { func TestValueValidatesType(t *testing.T) { t.Parallel() for _, tc := range []struct { - Name string - Type provider.OptionType - Value string - 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