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

Lines changed: 25 additions & 3 deletions
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

Lines changed: 132 additions & 65 deletions
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

Lines changed: 2 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy