mirror of
https://github.com/opentofu/opentofu.git
synced 2025-02-25 18:45:20 -06:00
configs: Add default argument to optional()
The optional modifier previously accepted a single argument: the attribute type. This commit adds an optional second argument, which specifies a default value for the attribute. To record the default values for a variable's type, we use a separate parallel structure of `typeexpr.Defaults`, rather than extending `cty.Type` to include a `cty.Value` of defaults (which may in turn include a `cty.Type` with defaults, and so on, and so forth). The new `typeexpr.TypeConstraintWithDefaults` returns a type constraint and defaults value. Defaults will be `nil` unless there are default values specified somewhere in the variable's type.
This commit is contained in:
parent
8f69e36e1b
commit
650380f3ae
@ -27,6 +27,7 @@ type Variable struct {
|
||||
// ConstraintType is used for decoding and type conversions, and may
|
||||
// contain nested ObjectWithOptionalAttr types.
|
||||
ConstraintType cty.Type
|
||||
TypeDefaults *typeexpr.Defaults
|
||||
|
||||
ParsingMode VariableParsingMode
|
||||
Validations []*CheckRule
|
||||
@ -102,9 +103,10 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["type"]; exists {
|
||||
ty, parseMode, tyDiags := decodeVariableType(attr.Expr)
|
||||
ty, tyDefaults, parseMode, tyDiags := decodeVariableType(attr.Expr)
|
||||
diags = append(diags, tyDiags...)
|
||||
v.ConstraintType = ty
|
||||
v.TypeDefaults = tyDefaults
|
||||
v.Type = ty.WithoutOptionalAttributesDeep()
|
||||
v.ParsingMode = parseMode
|
||||
}
|
||||
@ -137,6 +139,11 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
|
||||
// the type might not be set; we'll catch that during merge.
|
||||
if v.ConstraintType != cty.NilType {
|
||||
var err error
|
||||
// If the type constraint has defaults, we must apply those
|
||||
// defaults to the variable default value before type conversion.
|
||||
if v.TypeDefaults != nil {
|
||||
val = v.TypeDefaults.Apply(val)
|
||||
}
|
||||
val, err = convert.Convert(val, v.ConstraintType)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
@ -179,7 +186,7 @@ func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagno
|
||||
return v, diags
|
||||
}
|
||||
|
||||
func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) {
|
||||
func decodeVariableType(expr hcl.Expression) (cty.Type, *typeexpr.Defaults, VariableParsingMode, hcl.Diagnostics) {
|
||||
if exprIsNativeQuotedString(expr) {
|
||||
// If a user provides the pre-0.12 form of variable type argument where
|
||||
// the string values "string", "list" and "map" are accepted, we
|
||||
@ -190,7 +197,7 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
|
||||
// in the normal codepath below.
|
||||
val, diags := expr.Value(nil)
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicPseudoType, VariableParseHCL, diags
|
||||
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
||||
}
|
||||
str := val.AsString()
|
||||
switch str {
|
||||
@ -201,7 +208,7 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
|
||||
Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"string\".",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return cty.DynamicPseudoType, VariableParseLiteral, diags
|
||||
return cty.DynamicPseudoType, nil, VariableParseLiteral, diags
|
||||
case "list":
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
@ -209,7 +216,7 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
|
||||
Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"list\" and write list(string) instead to explicitly indicate that the list elements are strings.",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return cty.DynamicPseudoType, VariableParseHCL, diags
|
||||
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
||||
case "map":
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
@ -217,9 +224,9 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
|
||||
Detail: "Terraform 0.11 and earlier required type constraints to be given in quotes, but that form is now deprecated and will be removed in a future version of Terraform. Remove the quotes around \"map\" and write map(string) instead to explicitly indicate that the map elements are strings.",
|
||||
Subject: expr.Range().Ptr(),
|
||||
})
|
||||
return cty.DynamicPseudoType, VariableParseHCL, diags
|
||||
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
||||
default:
|
||||
return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, VariableParseHCL, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid legacy variable type hint",
|
||||
Detail: `To provide a full type expression, remove the surrounding quotes and give the type expression directly.`,
|
||||
@ -234,23 +241,23 @@ func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl
|
||||
// elements are consistent. This is the same as list(any) or map(any).
|
||||
switch hcl.ExprAsKeyword(expr) {
|
||||
case "list":
|
||||
return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil
|
||||
return cty.List(cty.DynamicPseudoType), nil, VariableParseHCL, nil
|
||||
case "map":
|
||||
return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil
|
||||
return cty.Map(cty.DynamicPseudoType), nil, VariableParseHCL, nil
|
||||
}
|
||||
|
||||
ty, diags := typeexpr.TypeConstraint(expr)
|
||||
ty, typeDefaults, diags := typeexpr.TypeConstraintWithDefaults(expr)
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicPseudoType, VariableParseHCL, diags
|
||||
return cty.DynamicPseudoType, nil, VariableParseHCL, diags
|
||||
}
|
||||
|
||||
switch {
|
||||
case ty.IsPrimitiveType():
|
||||
// Primitive types use literal parsing.
|
||||
return ty, VariableParseLiteral, diags
|
||||
return ty, typeDefaults, VariableParseLiteral, diags
|
||||
default:
|
||||
// Everything else uses HCL parsing
|
||||
return ty, VariableParseHCL, diags
|
||||
return ty, typeDefaults, VariableParseHCL, diags
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ terraform {
|
||||
variable "a" {
|
||||
type = object({
|
||||
foo = optional(string)
|
||||
bar = optional(bool, true)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -5,49 +5,52 @@ import (
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
"github.com/zclconf/go-cty/cty/convert"
|
||||
)
|
||||
|
||||
const invalidTypeSummary = "Invalid type specification"
|
||||
|
||||
// getType is the internal implementation of both Type and TypeConstraint,
|
||||
// using the passed flag to distinguish. When constraint is false, the "any"
|
||||
// keyword will produce an error.
|
||||
func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
// getType is the internal implementation of Type, TypeConstraint, and
|
||||
// TypeConstraintWithDefaults, using the passed flags to distinguish. When
|
||||
// `constraint` is true, the "any" keyword can be used in place of a concrete
|
||||
// type. When `withDefaults` is true, the "optional" call expression supports
|
||||
// an additional argument describing a default value.
|
||||
func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) {
|
||||
// First we'll try for one of our keywords
|
||||
kw := hcl.ExprAsKeyword(expr)
|
||||
switch kw {
|
||||
case "bool":
|
||||
return cty.Bool, nil
|
||||
return cty.Bool, nil, nil
|
||||
case "string":
|
||||
return cty.String, nil
|
||||
return cty.String, nil, nil
|
||||
case "number":
|
||||
return cty.Number, nil
|
||||
return cty.Number, nil, nil
|
||||
case "any":
|
||||
if constraint {
|
||||
return cty.DynamicPseudoType, nil
|
||||
return cty.DynamicPseudoType, nil, nil
|
||||
}
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw),
|
||||
Subject: expr.Range().Ptr(),
|
||||
}}
|
||||
case "list", "map", "set":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw),
|
||||
Subject: expr.Range().Ptr(),
|
||||
}}
|
||||
case "object":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
|
||||
Subject: expr.Range().Ptr(),
|
||||
}}
|
||||
case "tuple":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
|
||||
@ -56,7 +59,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
case "":
|
||||
// okay! we'll fall through and try processing as a call, then.
|
||||
default:
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw),
|
||||
@ -68,7 +71,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
// try to process it as a call instead.
|
||||
call, diags := hcl.ExprCall(expr)
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).",
|
||||
@ -78,14 +81,14 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
|
||||
switch call.Name {
|
||||
case "bool", "string", "number":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name),
|
||||
Subject: &call.ArgsRange,
|
||||
}}
|
||||
case "any":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name),
|
||||
@ -105,7 +108,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
|
||||
switch call.Name {
|
||||
case "list", "set", "map":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name),
|
||||
@ -113,7 +116,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
Context: &contextRange,
|
||||
}}
|
||||
case "object":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.",
|
||||
@ -121,7 +124,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
Context: &contextRange,
|
||||
}}
|
||||
case "tuple":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "The tuple type constructor requires one argument specifying the element types as a list.",
|
||||
@ -134,18 +137,21 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
switch call.Name {
|
||||
|
||||
case "list":
|
||||
ety, diags := getType(call.Arguments[0], constraint)
|
||||
return cty.List(ety), diags
|
||||
ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
|
||||
ty := cty.List(ety)
|
||||
return ty, collectionDefaults(ty, defaults), diags
|
||||
case "set":
|
||||
ety, diags := getType(call.Arguments[0], constraint)
|
||||
return cty.Set(ety), diags
|
||||
ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
|
||||
ty := cty.Set(ety)
|
||||
return ty, collectionDefaults(ty, defaults), diags
|
||||
case "map":
|
||||
ety, diags := getType(call.Arguments[0], constraint)
|
||||
return cty.Map(ety), diags
|
||||
ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults)
|
||||
ty := cty.Map(ety)
|
||||
return ty, collectionDefaults(ty, defaults), diags
|
||||
case "object":
|
||||
attrDefs, diags := hcl.ExprMap(call.Arguments[0])
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.",
|
||||
@ -155,6 +161,8 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
}
|
||||
|
||||
atys := make(map[string]cty.Type)
|
||||
defaultValues := make(map[string]cty.Value)
|
||||
children := make(map[string]*Defaults)
|
||||
var optAttrs []string
|
||||
for _, attrDef := range attrDefs {
|
||||
attrName := hcl.ExprAsKeyword(attrDef.Key)
|
||||
@ -174,6 +182,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
// modifier optional(...) to indicate an optional attribute. If
|
||||
// so, we'll unwrap that first and make a note about it being
|
||||
// optional for when we construct the type below.
|
||||
var defaultExpr hcl.Expression
|
||||
if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() {
|
||||
if call.Name == "optional" {
|
||||
if len(call.Arguments) < 1 {
|
||||
@ -187,16 +196,40 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
continue
|
||||
}
|
||||
if constraint {
|
||||
if len(call.Arguments) > 1 {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "Optional attribute modifier expects only one argument: the attribute type.",
|
||||
Subject: call.ArgsRange.Ptr(),
|
||||
Context: atyExpr.Range().Ptr(),
|
||||
})
|
||||
if withDefaults {
|
||||
switch len(call.Arguments) {
|
||||
case 2:
|
||||
defaultExpr = call.Arguments[1]
|
||||
defaultVal, defaultDiags := defaultExpr.Value(nil)
|
||||
diags = append(diags, defaultDiags...)
|
||||
if !defaultDiags.HasErrors() {
|
||||
optAttrs = append(optAttrs, attrName)
|
||||
defaultValues[attrName] = defaultVal
|
||||
}
|
||||
case 1:
|
||||
optAttrs = append(optAttrs, attrName)
|
||||
default:
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "Optional attribute modifier expects at most two arguments: the attribute type, and a default value.",
|
||||
Subject: call.ArgsRange.Ptr(),
|
||||
Context: atyExpr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if len(call.Arguments) == 1 {
|
||||
optAttrs = append(optAttrs, attrName)
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "Optional attribute modifier expects only one argument: the attribute type.",
|
||||
Subject: call.ArgsRange.Ptr(),
|
||||
Context: atyExpr.Range().Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
optAttrs = append(optAttrs, attrName)
|
||||
} else {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
@ -210,19 +243,39 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
}
|
||||
}
|
||||
|
||||
aty, attrDiags := getType(atyExpr, constraint)
|
||||
aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults)
|
||||
diags = append(diags, attrDiags...)
|
||||
|
||||
// If a default is set for an optional attribute, verify that it is
|
||||
// convertible to the attribute type.
|
||||
if defaultVal, ok := defaultValues[attrName]; ok {
|
||||
_, err := convert.Convert(defaultVal, aty)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid default value for optional attribute",
|
||||
Detail: fmt.Sprintf("This default value is not compatible with the attribute's type constraint: %s.", err),
|
||||
Subject: defaultExpr.Range().Ptr(),
|
||||
})
|
||||
delete(defaultValues, attrName)
|
||||
}
|
||||
}
|
||||
|
||||
atys[attrName] = aty
|
||||
if aDefaults != nil {
|
||||
children[attrName] = aDefaults
|
||||
}
|
||||
}
|
||||
// NOTE: ObjectWithOptionalAttrs is experimental in cty at the
|
||||
// time of writing, so this interface might change even in future
|
||||
// minor versions of cty. We're accepting that because Terraform
|
||||
// itself is considering optional attributes as experimental right now.
|
||||
return cty.ObjectWithOptionalAttrs(atys, optAttrs), diags
|
||||
ty := cty.ObjectWithOptionalAttrs(atys, optAttrs)
|
||||
return ty, structuredDefaults(ty, defaultValues, children), diags
|
||||
case "tuple":
|
||||
elemDefs, diags := hcl.ExprList(call.Arguments[0])
|
||||
if diags.HasErrors() {
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: "Tuple type constructor requires a list of element types.",
|
||||
@ -231,14 +284,19 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
}}
|
||||
}
|
||||
etys := make([]cty.Type, len(elemDefs))
|
||||
children := make(map[string]*Defaults, len(elemDefs))
|
||||
for i, defExpr := range elemDefs {
|
||||
ety, elemDiags := getType(defExpr, constraint)
|
||||
ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults)
|
||||
diags = append(diags, elemDiags...)
|
||||
etys[i] = ety
|
||||
if elemDefaults != nil {
|
||||
children[fmt.Sprintf("%d", i)] = elemDefaults
|
||||
}
|
||||
}
|
||||
return cty.Tuple(etys), diags
|
||||
ty := cty.Tuple(etys)
|
||||
return ty, structuredDefaults(ty, nil, children), diags
|
||||
case "optional":
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name),
|
||||
@ -247,7 +305,7 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
default:
|
||||
// Can't access call.Arguments in this path because we've not validated
|
||||
// that it contains exactly one expression here.
|
||||
return cty.DynamicPseudoType, hcl.Diagnostics{{
|
||||
return cty.DynamicPseudoType, nil, hcl.Diagnostics{{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: invalidTypeSummary,
|
||||
Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name),
|
||||
@ -255,3 +313,33 @@ func getType(expr hcl.Expression, constraint bool) (cty.Type, hcl.Diagnostics) {
|
||||
}}
|
||||
}
|
||||
}
|
||||
|
||||
func collectionDefaults(ty cty.Type, defaults *Defaults) *Defaults {
|
||||
if defaults == nil {
|
||||
return nil
|
||||
}
|
||||
return &Defaults{
|
||||
Type: ty,
|
||||
Children: map[string]*Defaults{
|
||||
"": defaults,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func structuredDefaults(ty cty.Type, defaultValues map[string]cty.Value, children map[string]*Defaults) *Defaults {
|
||||
if len(defaultValues) == 0 && len(children) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
defaults := &Defaults{
|
||||
Type: ty,
|
||||
}
|
||||
if len(defaultValues) > 0 {
|
||||
defaults.DefaultValues = defaultValues
|
||||
}
|
||||
if len(children) > 0 {
|
||||
defaults.Children = children
|
||||
}
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
@ -6,12 +6,17 @@ import (
|
||||
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/hclsyntax"
|
||||
"github.com/hashicorp/hcl/v2/json"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var (
|
||||
typeComparer = cmp.Comparer(cty.Type.Equals)
|
||||
)
|
||||
|
||||
func TestGetType(t *testing.T) {
|
||||
tests := []struct {
|
||||
Source string
|
||||
@ -284,6 +289,23 @@ func TestGetType(t *testing.T) {
|
||||
}),
|
||||
`Optional attribute modifier is only for type constraints, not for exact types.`,
|
||||
},
|
||||
{
|
||||
`object({name=string,meta=optional()})`,
|
||||
true,
|
||||
cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
}),
|
||||
`Optional attribute modifier requires the attribute type as its argument.`,
|
||||
},
|
||||
{
|
||||
`object({name=string,meta=optional(string, "hello")})`,
|
||||
true,
|
||||
cty.Object(map[string]cty.Type{
|
||||
"name": cty.String,
|
||||
"meta": cty.String,
|
||||
}),
|
||||
`Optional attribute modifier expects only one argument: the attribute type.`,
|
||||
},
|
||||
{
|
||||
`optional(string)`,
|
||||
false,
|
||||
@ -305,7 +327,7 @@ func TestGetType(t *testing.T) {
|
||||
t.Fatalf("failed to parse: %s", diags)
|
||||
}
|
||||
|
||||
got, diags := getType(expr, test.Constraint)
|
||||
got, _, diags := getType(expr, test.Constraint, false)
|
||||
if test.WantError == "" {
|
||||
for _, diag := range diags {
|
||||
t.Error(diag)
|
||||
@ -377,7 +399,7 @@ func TestGetTypeJSON(t *testing.T) {
|
||||
t.Fatalf("failed to decode: %s", diags)
|
||||
}
|
||||
|
||||
got, diags := getType(content.Expr, test.Constraint)
|
||||
got, _, diags := getType(content.Expr, test.Constraint, false)
|
||||
if test.WantError == "" {
|
||||
for _, diag := range diags {
|
||||
t.Error(diag)
|
||||
@ -401,3 +423,247 @@ func TestGetTypeJSON(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetTypeDefaults(t *testing.T) {
|
||||
tests := []struct {
|
||||
Source string
|
||||
Want *Defaults
|
||||
WantError string
|
||||
}{
|
||||
// primitive types have nil defaults
|
||||
{
|
||||
`bool`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
`number`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
`string`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
`any`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
|
||||
// complex structures with no defaults have nil defaults
|
||||
{
|
||||
`map(string)`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
`set(number)`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
`tuple([number, string])`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
`object({ a = string, b = number })`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
{
|
||||
`map(list(object({ a = string, b = optional(number) })))`,
|
||||
nil,
|
||||
"",
|
||||
},
|
||||
|
||||
// object optional attribute with defaults
|
||||
{
|
||||
`object({ a = string, b = optional(number, 5) })`,
|
||||
&Defaults{
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"b": cty.NumberIntVal(5),
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
|
||||
// nested defaults
|
||||
{
|
||||
`object({ a = optional(object({ b = optional(number, 5) }), {}) })`,
|
||||
&Defaults{
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
}, []string{"a"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"a": cty.EmptyObjectVal,
|
||||
},
|
||||
Children: map[string]*Defaults{
|
||||
"a": {
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"b": cty.NumberIntVal(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
|
||||
// collections of objects with defaults
|
||||
{
|
||||
`map(object({ a = string, b = optional(number, 5) }))`,
|
||||
&Defaults{
|
||||
Type: cty.Map(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"})),
|
||||
Children: map[string]*Defaults{
|
||||
"": {
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"b": cty.NumberIntVal(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
`list(object({ a = string, b = optional(number, 5) }))`,
|
||||
&Defaults{
|
||||
Type: cty.List(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"})),
|
||||
Children: map[string]*Defaults{
|
||||
"": {
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"b": cty.NumberIntVal(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
`set(object({ a = string, b = optional(number, 5) }))`,
|
||||
&Defaults{
|
||||
Type: cty.Set(cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"})),
|
||||
Children: map[string]*Defaults{
|
||||
"": {
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"b": cty.NumberIntVal(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
|
||||
// tuples containing objects with defaults work differently from
|
||||
// collections
|
||||
{
|
||||
`tuple([string, bool, object({ a = string, b = optional(number, 5) })])`,
|
||||
&Defaults{
|
||||
Type: cty.Tuple([]cty.Type{
|
||||
cty.String,
|
||||
cty.Bool,
|
||||
cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
}),
|
||||
Children: map[string]*Defaults{
|
||||
"2": {
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"b"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"b": cty.NumberIntVal(5),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"",
|
||||
},
|
||||
|
||||
// incompatible default value causes an error
|
||||
{
|
||||
`object({ a = optional(string, "hello"), b = optional(number, true) })`,
|
||||
&Defaults{
|
||||
Type: cty.ObjectWithOptionalAttrs(map[string]cty.Type{
|
||||
"a": cty.String,
|
||||
"b": cty.Number,
|
||||
}, []string{"a", "b"}),
|
||||
DefaultValues: map[string]cty.Value{
|
||||
"a": cty.StringVal("hello"),
|
||||
},
|
||||
},
|
||||
"This default value is not compatible with the attribute's type constraint: number required.",
|
||||
},
|
||||
|
||||
// Too many arguments
|
||||
{
|
||||
`object({name=string,meta=optional(string, "hello", "world")})`,
|
||||
nil,
|
||||
`Optional attribute modifier expects at most two arguments: the attribute type, and a default value.`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.Source, func(t *testing.T) {
|
||||
expr, diags := hclsyntax.ParseExpression([]byte(test.Source), "", hcl.Pos{Line: 1, Column: 1})
|
||||
if diags.HasErrors() {
|
||||
t.Fatalf("failed to parse: %s", diags)
|
||||
}
|
||||
|
||||
_, got, diags := getType(expr, true, true)
|
||||
if test.WantError == "" {
|
||||
for _, diag := range diags {
|
||||
t.Error(diag)
|
||||
}
|
||||
} else {
|
||||
found := false
|
||||
for _, diag := range diags {
|
||||
t.Log(diag)
|
||||
if diag.Severity == hcl.DiagError && diag.Detail == test.WantError {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("missing expected error detail message: %s", test.WantError)
|
||||
}
|
||||
}
|
||||
|
||||
if !cmp.Equal(test.Want, got, valueComparer, typeComparer) {
|
||||
t.Errorf("wrong result\n%s", cmp.Diff(test.Want, got, valueComparer, typeComparer))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ import (
|
||||
// successful, returns the resulting type. If unsuccessful, error diagnostics
|
||||
// are returned.
|
||||
func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
|
||||
return getType(expr, false)
|
||||
ty, _, diags := getType(expr, false, false)
|
||||
return ty, diags
|
||||
}
|
||||
|
||||
// TypeConstraint attempts to parse the given expression as a type constraint
|
||||
@ -26,7 +27,20 @@ func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
|
||||
// allows the keyword "any" to represent cty.DynamicPseudoType, which is often
|
||||
// used as a wildcard in type checking and type conversion operations.
|
||||
func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
|
||||
return getType(expr, true)
|
||||
ty, _, diags := getType(expr, true, false)
|
||||
return ty, diags
|
||||
}
|
||||
|
||||
// TypeConstraintWithDefaults attempts to parse the given expression as a type
|
||||
// constraint which may include default values for object attributes. If
|
||||
// successful both the resulting type and corresponding defaults are returned.
|
||||
// If unsuccessful, error diagnostics are returned.
|
||||
//
|
||||
// When using this function, defaults should be applied to the input value
|
||||
// before type conversion, to ensure that objects with missing attributes have
|
||||
// default values populated.
|
||||
func TypeConstraintWithDefaults(expr hcl.Expression) (cty.Type, *Defaults, hcl.Diagnostics) {
|
||||
return getType(expr, true, true)
|
||||
}
|
||||
|
||||
// TypeString returns a string rendering of the given type as it would be
|
||||
|
Loading…
Reference in New Issue
Block a user