Skip to content

Commit f66adac

Browse files
authored
chore: enhance parameter validation error messages (#379)
* chore: implement validation in an exported function * chore: enhance parameter validation error messages
1 parent 53a68cd commit f66adac

File tree

3 files changed

+159
-70
lines changed

3 files changed

+159
-70
lines changed

provider/formtype.go

+25-3
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,11 @@ var formTypeTruthTable = map[OptionType]map[bool][]ParameterFormType{
115115
// The use case is when using multi-select. The options are 'string' and the
116116
// value is 'list(string)'.
117117
func ValidateFormType(paramType OptionType, optionCount int, specifiedFormType ParameterFormType) (OptionType, ParameterFormType, error) {
118-
allowed, ok := formTypeTruthTable[paramType][optionCount > 0]
118+
optionsExist := optionCount > 0
119+
allowed, ok := formTypeTruthTable[paramType][optionsExist]
119120
if !ok || len(allowed) == 0 {
120-
return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", paramType)
121+
// This error should really never be hit, as the provider sdk does an enum validation.
122+
return paramType, specifiedFormType, xerrors.Errorf("\"type\" attribute=%q is not supported, choose one of %v", paramType, OptionTypes())
121123
}
122124

123125
if specifiedFormType == ParameterFormTypeDefault {
@@ -126,7 +128,27 @@ func ValidateFormType(paramType OptionType, optionCount int, specifiedFormType P
126128
}
127129

128130
if !slices.Contains(allowed, specifiedFormType) {
129-
return paramType, specifiedFormType, xerrors.Errorf("value type %q is not supported for 'form_types'", specifiedFormType)
131+
optionMsg := ""
132+
opposite := formTypeTruthTable[paramType][!optionsExist]
133+
134+
// This extra message tells a user if they are using a valid form_type
135+
// for a 'type', but it is invalid because options do/do-not exist.
136+
// It serves as a more helpful error message.
137+
//
138+
// Eg: form_type=slider is valid for type=number, but invalid if options exist.
139+
// And this error message is more accurate than just saying "form_type=slider is
140+
// not valid for type=number".
141+
if slices.Contains(opposite, specifiedFormType) {
142+
if optionsExist {
143+
optionMsg = " when options exist"
144+
} else {
145+
optionMsg = " when options do not exist"
146+
}
147+
}
148+
return paramType, specifiedFormType,
149+
xerrors.Errorf("\"form_type\" attribute=%q is not supported for \"type\"=%q%s, choose one of %v",
150+
specifiedFormType, paramType,
151+
optionMsg, toStrings(allowed))
130152
}
131153

132154
// This is the only current special case. If 'multi-select' is selected, the type

provider/parameter.go

+132-65
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ func parameterDataSource() *schema.Resource {
124124
}
125125
var value string
126126
if parameter.Default != "" {
127-
err := valueIsType(parameter.Type, parameter.Default)
127+
err := valueIsType(parameter.Type, parameter.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
128128
if err != nil {
129129
return err
130130
}
@@ -144,6 +144,8 @@ func parameterDataSource() *schema.Resource {
144144
return diag.Errorf("ephemeral parameter requires the default property")
145145
}
146146

147+
// TODO: Should we move this into the Valid() function on
148+
// Parameter?
147149
if len(parameter.Validation) == 1 {
148150
validation := &parameter.Validation[0]
149151
err = validation.Valid(parameter.Type, value)
@@ -153,73 +155,27 @@ func parameterDataSource() *schema.Resource {
153155
}
154156

155157
// Validate options
156-
157-
// optionType might differ from parameter.Type. This is ok, and parameter.Type
158-
// should be used for the value type, and optionType for options.
159-
var optionType OptionType
160-
optionType, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
158+
_, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
161159
if err != nil {
162-
return diag.FromErr(err)
160+
return diag.Diagnostics{
161+
{
162+
Severity: diag.Error,
163+
Summary: "Invalid form_type for parameter",
164+
Detail: err.Error(),
165+
AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
166+
},
167+
}
163168
}
164169
// Set the form_type back in case the value was changed.
165170
// Eg via a default. If a user does not specify, a default value
166171
// is used and saved.
167172
rd.Set("form_type", parameter.FormType)
168173

169-
if len(parameter.Option) > 0 {
170-
names := map[string]interface{}{}
171-
values := map[string]interface{}{}
172-
for _, option := range parameter.Option {
173-
_, exists := names[option.Name]
174-
if exists {
175-
return diag.Errorf("multiple options cannot have the same name %q", option.Name)
176-
}
177-
_, exists = values[option.Value]
178-
if exists {
179-
return diag.Errorf("multiple options cannot have the same value %q", option.Value)
180-
}
181-
err := valueIsType(optionType, option.Value)
182-
if err != nil {
183-
return err
184-
}
185-
values[option.Value] = nil
186-
names[option.Name] = nil
187-
}
188-
189-
if parameter.Default != "" {
190-
if parameter.Type == OptionTypeListString && optionType == OptionTypeString {
191-
// If the type is list(string) and optionType is string, we have
192-
// to ensure all elements of the default exist as options.
193-
var defaultValues []string
194-
// TODO: We do this unmarshal in a few spots. It should be standardized.
195-
err = json.Unmarshal([]byte(parameter.Default), &defaultValues)
196-
if err != nil {
197-
return diag.Errorf("default value %q is not a list of strings", parameter.Default)
198-
}
199-
200-
// missing is used to construct a more helpful error message
201-
var missing []string
202-
for _, defaultValue := range defaultValues {
203-
_, defaultIsValid := values[defaultValue]
204-
if !defaultIsValid {
205-
missing = append(missing, defaultValue)
206-
}
207-
}
208-
209-
if len(missing) > 0 {
210-
return diag.Errorf(
211-
"default value %q is not a valid option, values %q are missing from the option",
212-
parameter.Default, strings.Join(missing, ", "),
213-
)
214-
}
215-
} else {
216-
_, defaultIsValid := values[parameter.Default]
217-
if !defaultIsValid {
218-
return diag.Errorf("%q default value %q must be defined as one of options", parameter.FormType, parameter.Default)
219-
}
220-
}
221-
}
174+
diags := parameter.Valid()
175+
if diags.HasError() {
176+
return diags
222177
}
178+
223179
return nil
224180
},
225181
Schema: map[string]*schema.Schema{
@@ -433,7 +389,7 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int
433389
return vArr, nil
434390
}
435391

436-
func valueIsType(typ OptionType, value string) diag.Diagnostics {
392+
func valueIsType(typ OptionType, value string, attrPath cty.Path) diag.Diagnostics {
437393
switch typ {
438394
case OptionTypeNumber:
439395
_, err := strconv.ParseFloat(value, 64)
@@ -446,10 +402,9 @@ func valueIsType(typ OptionType, value string) diag.Diagnostics {
446402
return diag.Errorf("%q is not a bool", value)
447403
}
448404
case OptionTypeListString:
449-
var items []string
450-
err := json.Unmarshal([]byte(value), &items)
451-
if err != nil {
452-
return diag.Errorf("%q is not an array of strings", value)
405+
_, diags := valueIsListString(value, attrPath)
406+
if diags.HasError() {
407+
return diags
453408
}
454409
case OptionTypeString:
455410
// Anything is a string!
@@ -459,6 +414,102 @@ func valueIsType(typ OptionType, value string) diag.Diagnostics {
459414
return nil
460415
}
461416

417+
func (v *Parameter) Valid() diag.Diagnostics {
418+
// optionType might differ from parameter.Type. This is ok, and parameter.Type
419+
// should be used for the value type, and optionType for options.
420+
optionType, _, err := ValidateFormType(v.Type, len(v.Option), v.FormType)
421+
if err != nil {
422+
return diag.Diagnostics{
423+
{
424+
Severity: diag.Error,
425+
Summary: "Invalid form_type for parameter",
426+
Detail: err.Error(),
427+
AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
428+
},
429+
}
430+
}
431+
432+
optionNames := map[string]any{}
433+
optionValues := map[string]any{}
434+
if len(v.Option) > 0 {
435+
for _, option := range v.Option {
436+
_, exists := optionNames[option.Name]
437+
if exists {
438+
return diag.Diagnostics{{
439+
Severity: diag.Error,
440+
Summary: "Option names must be unique.",
441+
Detail: fmt.Sprintf("multiple options found with the same name %q", option.Name),
442+
},
443+
}
444+
}
445+
_, exists = optionValues[option.Value]
446+
if exists {
447+
return diag.Diagnostics{
448+
{
449+
Severity: diag.Error,
450+
Summary: "Option values must be unique.",
451+
Detail: fmt.Sprintf("multiple options found with the same value %q", option.Value),
452+
},
453+
}
454+
}
455+
diags := valueIsType(optionType, option.Value, cty.Path{})
456+
if diags.HasError() {
457+
return diags
458+
}
459+
optionValues[option.Value] = nil
460+
optionNames[option.Name] = nil
461+
}
462+
}
463+
464+
if v.Default != "" && len(v.Option) > 0 {
465+
if v.Type == OptionTypeListString && optionType == OptionTypeString {
466+
// If the type is list(string) and optionType is string, we have
467+
// to ensure all elements of the default exist as options.
468+
defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
469+
if diags.HasError() {
470+
return diags
471+
}
472+
473+
// missing is used to construct a more helpful error message
474+
var missing []string
475+
for _, defaultValue := range defaultValues {
476+
_, defaultIsValid := optionValues[defaultValue]
477+
if !defaultIsValid {
478+
missing = append(missing, defaultValue)
479+
}
480+
}
481+
482+
if len(missing) > 0 {
483+
return diag.Diagnostics{
484+
{
485+
Severity: diag.Error,
486+
Summary: "Default values must be a valid option",
487+
Detail: fmt.Sprintf(
488+
"default value %q is not a valid option, values %q are missing from the options",
489+
v.Default, strings.Join(missing, ", "),
490+
),
491+
AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
492+
},
493+
}
494+
}
495+
} else {
496+
_, defaultIsValid := optionValues[v.Default]
497+
if !defaultIsValid {
498+
return diag.Diagnostics{
499+
{
500+
Severity: diag.Error,
501+
Summary: "Default value must be a valid option",
502+
Detail: fmt.Sprintf("the value %q must be defined as one of options", v.Default),
503+
AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
504+
},
505+
}
506+
}
507+
}
508+
}
509+
510+
return nil
511+
}
512+
462513
func (v *Validation) Valid(typ OptionType, value string) error {
463514
if typ != OptionTypeNumber {
464515
if !v.MinDisabled {
@@ -519,6 +570,22 @@ func (v *Validation) Valid(typ OptionType, value string) error {
519570
return nil
520571
}
521572

573+
func valueIsListString(value string, path cty.Path) ([]string, diag.Diagnostics) {
574+
var items []string
575+
err := json.Unmarshal([]byte(value), &items)
576+
if err != nil {
577+
return nil, diag.Diagnostics{
578+
{
579+
Severity: diag.Error,
580+
Summary: "When using list(string) type, value must be a json encoded list of strings",
581+
Detail: fmt.Sprintf("value %q is not a valid list of strings", value),
582+
AttributePath: path,
583+
},
584+
}
585+
}
586+
return items, nil
587+
}
588+
522589
// ParameterEnvironmentVariable returns the environment variable to specify for
523590
// a parameter by it's name. It's hashed because spaces and special characters
524591
// can be used in parameter names that may not be valid in env vars.

provider/parameter_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ func TestParameter(t *testing.T) {
291291
}
292292
}
293293
`,
294-
ExpectError: regexp.MustCompile("cannot have the same name"),
294+
ExpectError: regexp.MustCompile("Option names must be unique"),
295295
}, {
296296
Name: "DuplicateOptionValue",
297297
Config: `
@@ -308,7 +308,7 @@ func TestParameter(t *testing.T) {
308308
}
309309
}
310310
`,
311-
ExpectError: regexp.MustCompile("cannot have the same value"),
311+
ExpectError: regexp.MustCompile("Option values must be unique"),
312312
}, {
313313
Name: "RequiredParameterNoDefault",
314314
Config: `

0 commit comments

Comments
 (0)