Skip to content

Commit 84f0a89

Browse files
committed
chore: enhance parameter validation error messages
Validation code is exported for external use
1 parent 8920f69 commit 84f0a89

File tree

2 files changed

+93
-30
lines changed

2 files changed

+93
-30
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 no 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

+68-27
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)
@@ -155,7 +157,12 @@ func parameterDataSource() *schema.Resource {
155157
// Validate options
156158
_, parameter.FormType, err = ValidateFormType(parameter.Type, len(parameter.Option), parameter.FormType)
157159
if err != nil {
158-
return diag.FromErr(err)
160+
return singleDiag(diag.Diagnostic{
161+
Severity: diag.Error,
162+
Summary: "Invalid form_type for parameter",
163+
Detail: err.Error(),
164+
AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
165+
})
159166
}
160167
// Set the form_type back in case the value was changed.
161168
// Eg via a default. If a user does not specify, a default value
@@ -380,7 +387,7 @@ func fixValidationResourceData(rawConfig cty.Value, validation interface{}) (int
380387
return vArr, nil
381388
}
382389

383-
func valueIsType(typ OptionType, value string) diag.Diagnostics {
390+
func valueIsType(typ OptionType, value string, attrPath cty.Path) diag.Diagnostics {
384391
switch typ {
385392
case OptionTypeNumber:
386393
_, err := strconv.ParseFloat(value, 64)
@@ -393,10 +400,9 @@ func valueIsType(typ OptionType, value string) diag.Diagnostics {
393400
return diag.Errorf("%q is not a bool", value)
394401
}
395402
case OptionTypeListString:
396-
var items []string
397-
err := json.Unmarshal([]byte(value), &items)
398-
if err != nil {
399-
return diag.Errorf("%q is not an array of strings", value)
403+
_, diags := valueIsListString(value, attrPath)
404+
if diags.HasError() {
405+
return diags
400406
}
401407
case OptionTypeString:
402408
// Anything is a string!
@@ -409,13 +415,14 @@ func valueIsType(typ OptionType, value string) diag.Diagnostics {
409415
func (v *Parameter) Valid() diag.Diagnostics {
410416
// optionType might differ from parameter.Type. This is ok, and parameter.Type
411417
// should be used for the value type, and optionType for options.
412-
optionType, formType, err := ValidateFormType(v.Type, len(v.Option), v.FormType)
418+
optionType, _, err := ValidateFormType(v.Type, len(v.Option), v.FormType)
413419
if err != nil {
414-
return diag.FromErr(err)
415-
}
416-
417-
if formType != v.FormType {
418-
return diag.FromErr(fmt.Errorf("form_type %q is not valid for type %q", v.FormType, v.Type))
420+
return singleDiag(diag.Diagnostic{
421+
Severity: diag.Error,
422+
Summary: "Invalid form_type for parameter",
423+
Detail: err.Error(),
424+
AttributePath: cty.Path{cty.GetAttrStep{Name: "form_type"}},
425+
})
419426
}
420427

421428
optionNames := map[string]interface{}{}
@@ -424,13 +431,21 @@ func (v *Parameter) Valid() diag.Diagnostics {
424431
for _, option := range v.Option {
425432
_, exists := optionNames[option.Name]
426433
if exists {
427-
return diag.FromErr(fmt.Errorf("multiple options cannot have the same name %q", option.Name))
434+
return singleDiag(diag.Diagnostic{
435+
Severity: diag.Error,
436+
Summary: "Option names must be unique.",
437+
Detail: fmt.Sprintf("multiple options found with the same name %q", option.Name),
438+
})
428439
}
429440
_, exists = optionValues[option.Value]
430441
if exists {
431-
return diag.FromErr(fmt.Errorf("multiple options cannot have the same value %q", option.Value))
442+
return singleDiag(diag.Diagnostic{
443+
Severity: diag.Error,
444+
Summary: "Option values must be unique.",
445+
Detail: fmt.Sprintf("multiple options found with the same value %q", option.Value),
446+
})
432447
}
433-
diags := valueIsType(optionType, option.Value)
448+
diags := valueIsType(optionType, option.Value, cty.Path{})
434449
if diags.HasError() {
435450
return diags
436451
}
@@ -439,15 +454,13 @@ func (v *Parameter) Valid() diag.Diagnostics {
439454
}
440455
}
441456

442-
if v.Default != "" {
457+
if v.Default != "" && len(v.Option) > 0 {
443458
if v.Type == OptionTypeListString && optionType == OptionTypeString {
444459
// If the type is list(string) and optionType is string, we have
445460
// to ensure all elements of the default exist as options.
446-
var defaultValues []string
447-
// TODO: We do this unmarshal in a few spots. It should be standardized.
448-
err = json.Unmarshal([]byte(v.Default), &defaultValues)
449-
if err != nil {
450-
return diag.FromErr(fmt.Errorf("default value %q is not a list of strings", v.Default))
461+
defaultValues, diags := valueIsListString(v.Default, cty.Path{cty.GetAttrStep{Name: "default"}})
462+
if diags.HasError() {
463+
return diags
451464
}
452465

453466
// missing is used to construct a more helpful error message
@@ -460,15 +473,25 @@ func (v *Parameter) Valid() diag.Diagnostics {
460473
}
461474

462475
if len(missing) > 0 {
463-
return diag.FromErr(fmt.Errorf(
464-
"default value %q is not a valid option, values %q are missing from the option",
465-
v.Default, strings.Join(missing, ", "),
466-
))
476+
return singleDiag(diag.Diagnostic{
477+
Severity: diag.Error,
478+
Summary: "Default values must be a valid option",
479+
Detail: fmt.Sprintf(
480+
"default value %q is not a valid option, values %q are missing from the options",
481+
v.Default, strings.Join(missing, ", "),
482+
),
483+
AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
484+
})
467485
}
468486
} else {
469487
_, defaultIsValid := optionValues[v.Default]
470488
if !defaultIsValid {
471-
return diag.FromErr(fmt.Errorf("%q default value %q must be defined as one of options", v.FormType, v.Default))
489+
return singleDiag(diag.Diagnostic{
490+
Severity: diag.Error,
491+
Summary: "Default value must be a valid option",
492+
Detail: fmt.Sprintf("the value %q must be defined as one of options", v.Default),
493+
AttributePath: cty.Path{cty.GetAttrStep{Name: "default"}},
494+
})
472495
}
473496
}
474497
}
@@ -536,6 +559,20 @@ func (v *Validation) Valid(typ OptionType, value string) error {
536559
return nil
537560
}
538561

562+
func valueIsListString(value string, path cty.Path) ([]string, diag.Diagnostics) {
563+
var items []string
564+
err := json.Unmarshal([]byte(value), &items)
565+
if err != nil {
566+
return nil, singleDiag(diag.Diagnostic{
567+
Severity: diag.Error,
568+
Summary: fmt.Sprintf("When using list(string) type, value must be a json encoded list of strings"),
569+
Detail: fmt.Sprintf("value %q is not a valid list of strings", value),
570+
AttributePath: path,
571+
})
572+
}
573+
return items, nil
574+
}
575+
539576
// ParameterEnvironmentVariable returns the environment variable to specify for
540577
// a parameter by it's name. It's hashed because spaces and special characters
541578
// can be used in parameter names that may not be valid in env vars.
@@ -563,3 +600,7 @@ func (v *Validation) errorRendered(value string) error {
563600
"{value}", value)
564601
return xerrors.Errorf(r.Replace(v.Error))
565602
}
603+
604+
func singleDiag(diagnostic diag.Diagnostic) diag.Diagnostics {
605+
return diag.Diagnostics{diagnostic}
606+
}

0 commit comments

Comments
 (0)