hcldec: ImpliedType function
This function returns the type of value that should be returned when decoding the given spec. As well as being generally useful to the caller for book-keeping purposes, this also allows us to return correct type information when we are returning null and empty values, where before we were leaning a little too much on cty.DynamicPseudoType.
This commit is contained in:
parent
0d6247f4cf
commit
44bad6dbf5
@ -24,6 +24,10 @@ func decode(body hcl.Body, blockLabels []blockLabel, ctx *hcl.EvalContext, spec
|
||||
return val, leftovers, diags
|
||||
}
|
||||
|
||||
func impliedType(spec Spec) cty.Type {
|
||||
return spec.impliedType()
|
||||
}
|
||||
|
||||
func sourceRange(body hcl.Body, blockLabels []blockLabel, spec Spec) hcl.Range {
|
||||
schema := ImpliedSchema(spec)
|
||||
content, _, _ := body.PartialContent(schema)
|
||||
|
@ -26,6 +26,12 @@ func PartialDecode(body hcl.Body, spec Spec, ctx *hcl.EvalContext) (cty.Value, h
|
||||
return decode(body, nil, ctx, spec, true)
|
||||
}
|
||||
|
||||
// ImpliedType returns the value type that should result from decoding the
|
||||
// given spec.
|
||||
func ImpliedType(spec Spec) cty.Type {
|
||||
return impliedType(spec)
|
||||
}
|
||||
|
||||
// SourceRange interprets the given body using the given specification and
|
||||
// then returns the source range of the value that would be used to
|
||||
// fulfill the spec.
|
||||
|
@ -193,7 +193,7 @@ b {
|
||||
},
|
||||
},
|
||||
nil,
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.NullVal(cty.String),
|
||||
1, // missing name label
|
||||
},
|
||||
{
|
||||
@ -203,7 +203,7 @@ b {
|
||||
Nested: ObjectSpec{},
|
||||
},
|
||||
nil,
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.NullVal(cty.EmptyObject),
|
||||
0,
|
||||
},
|
||||
{
|
||||
@ -213,7 +213,7 @@ b {
|
||||
Nested: ObjectSpec{},
|
||||
},
|
||||
nil,
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.NullVal(cty.EmptyObject),
|
||||
1, // blocks of type "a" are not supported
|
||||
},
|
||||
{
|
||||
@ -224,7 +224,7 @@ b {
|
||||
Required: true,
|
||||
},
|
||||
nil,
|
||||
cty.NullVal(cty.DynamicPseudoType),
|
||||
cty.NullVal(cty.EmptyObject),
|
||||
1, // a block of type "b" is required
|
||||
},
|
||||
{
|
||||
@ -261,7 +261,7 @@ b {}
|
||||
Nested: ObjectSpec{},
|
||||
},
|
||||
nil,
|
||||
cty.ListValEmpty(cty.DynamicPseudoType),
|
||||
cty.ListValEmpty(cty.EmptyObject),
|
||||
0,
|
||||
},
|
||||
{
|
||||
@ -433,7 +433,7 @@ b "foo" "bar" {}
|
||||
Nested: ObjectSpec{},
|
||||
},
|
||||
nil,
|
||||
cty.MapValEmpty(cty.DynamicPseudoType),
|
||||
cty.MapValEmpty(cty.EmptyObject),
|
||||
1, // too many labels
|
||||
},
|
||||
{
|
||||
@ -446,7 +446,7 @@ b "bar" {}
|
||||
Nested: ObjectSpec{},
|
||||
},
|
||||
nil,
|
||||
cty.MapValEmpty(cty.DynamicPseudoType),
|
||||
cty.MapValEmpty(cty.EmptyObject),
|
||||
1, // not enough labels
|
||||
},
|
||||
{
|
||||
@ -510,7 +510,7 @@ b "foo" {}
|
||||
},
|
||||
},
|
||||
nil,
|
||||
cty.MapValEmpty(cty.DynamicPseudoType),
|
||||
cty.MapValEmpty(cty.String),
|
||||
1, // missing name
|
||||
},
|
||||
}
|
||||
|
@ -22,6 +22,10 @@ type Spec interface {
|
||||
// types that work on block bodies.
|
||||
decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics)
|
||||
|
||||
// Return the cty.Type that should be returned when decoding a body with
|
||||
// this spec.
|
||||
impliedType() cty.Type
|
||||
|
||||
// Call the given callback once for each of the nested specs that would
|
||||
// get decoded with the same body and block as the receiver. This should
|
||||
// not descend into the nested specs used when decoding blocks.
|
||||
@ -75,6 +79,18 @@ func (s ObjectSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, c
|
||||
return cty.ObjectVal(vals), diags
|
||||
}
|
||||
|
||||
func (s ObjectSpec) impliedType() cty.Type {
|
||||
if len(s) == 0 {
|
||||
return cty.EmptyObject
|
||||
}
|
||||
|
||||
attrTypes := make(map[string]cty.Type)
|
||||
for k, childSpec := range s {
|
||||
attrTypes[k] = childSpec.impliedType()
|
||||
}
|
||||
return cty.Object(attrTypes)
|
||||
}
|
||||
|
||||
func (s ObjectSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// This is not great, but the best we can do. In practice, it's rather
|
||||
// strange to ask for the source range of an entire top-level body, since
|
||||
@ -105,6 +121,18 @@ func (s TupleSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ct
|
||||
return cty.TupleVal(vals), diags
|
||||
}
|
||||
|
||||
func (s TupleSpec) impliedType() cty.Type {
|
||||
if len(s) == 0 {
|
||||
return cty.EmptyTuple
|
||||
}
|
||||
|
||||
attrTypes := make([]cty.Type, len(s))
|
||||
for i, childSpec := range s {
|
||||
attrTypes[i] = childSpec.impliedType()
|
||||
}
|
||||
return cty.Tuple(attrTypes)
|
||||
}
|
||||
|
||||
func (s TupleSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// This is not great, but the best we can do. In practice, it's rather
|
||||
// strange to ask for the source range of an entire top-level body, since
|
||||
@ -186,6 +214,10 @@ func (s *AttrSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ct
|
||||
return val, diags
|
||||
}
|
||||
|
||||
func (s *AttrSpec) impliedType() cty.Type {
|
||||
return s.Type
|
||||
}
|
||||
|
||||
// A LiteralSpec is a Spec that produces the given literal value, ignoring
|
||||
// the given body.
|
||||
type LiteralSpec struct {
|
||||
@ -200,6 +232,10 @@ func (s *LiteralSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel,
|
||||
return s.Value, nil
|
||||
}
|
||||
|
||||
func (s *LiteralSpec) impliedType() cty.Type {
|
||||
return s.Value.Type()
|
||||
}
|
||||
|
||||
func (s *LiteralSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// No sensible range to return for a literal, so the caller had better
|
||||
// ensure it doesn't cause any diagnostics.
|
||||
@ -227,6 +263,11 @@ func (s *ExprSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, ct
|
||||
return s.Expr.Value(ctx)
|
||||
}
|
||||
|
||||
func (s *ExprSpec) impliedType() cty.Type {
|
||||
// We can't know the type of our expression until we evaluate it
|
||||
return cty.DynamicPseudoType
|
||||
}
|
||||
|
||||
func (s *ExprSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
return s.Expr.Range()
|
||||
}
|
||||
@ -312,7 +353,7 @@ func (s *BlockSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, c
|
||||
Subject: &content.MissingItemRange,
|
||||
})
|
||||
}
|
||||
return cty.NullVal(cty.DynamicPseudoType), diags
|
||||
return cty.NullVal(s.Nested.impliedType()), diags
|
||||
}
|
||||
|
||||
if s.Nested == nil {
|
||||
@ -323,6 +364,10 @@ func (s *BlockSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, c
|
||||
return val, diags
|
||||
}
|
||||
|
||||
func (s *BlockSpec) impliedType() cty.Type {
|
||||
return s.Nested.impliedType()
|
||||
}
|
||||
|
||||
func (s *BlockSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
var childBlock *hcl.Block
|
||||
for _, candidate := range content.Blocks {
|
||||
@ -418,9 +463,7 @@ func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabe
|
||||
var ret cty.Value
|
||||
|
||||
if len(elems) == 0 {
|
||||
// FIXME: We don't currently have enough info to construct a type for
|
||||
// an empty list, so we'll just stub it out.
|
||||
ret = cty.ListValEmpty(cty.DynamicPseudoType)
|
||||
ret = cty.ListValEmpty(s.Nested.impliedType())
|
||||
} else {
|
||||
ret = cty.ListVal(elems)
|
||||
}
|
||||
@ -428,6 +471,10 @@ func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabe
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func (s *BlockListSpec) impliedType() cty.Type {
|
||||
return cty.List(s.Nested.impliedType())
|
||||
}
|
||||
|
||||
func (s *BlockListSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// We return the source range of the _first_ block of the given type,
|
||||
// since they are not guaranteed to form a contiguous range.
|
||||
@ -526,9 +573,7 @@ func (s *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
|
||||
var ret cty.Value
|
||||
|
||||
if len(elems) == 0 {
|
||||
// FIXME: We don't currently have enough info to construct a type for
|
||||
// an empty list, so we'll just stub it out.
|
||||
ret = cty.SetValEmpty(cty.DynamicPseudoType)
|
||||
ret = cty.SetValEmpty(s.Nested.impliedType())
|
||||
} else {
|
||||
ret = cty.SetVal(elems)
|
||||
}
|
||||
@ -536,6 +581,10 @@ func (s *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
|
||||
return ret, diags
|
||||
}
|
||||
|
||||
func (s *BlockSetSpec) impliedType() cty.Type {
|
||||
return cty.Set(s.Nested.impliedType())
|
||||
}
|
||||
|
||||
func (s *BlockSetSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// We return the source range of the _first_ block of the given type,
|
||||
// since they are not guaranteed to form a contiguous range.
|
||||
@ -643,13 +692,12 @@ func (s *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
|
||||
targetMap[key] = val
|
||||
}
|
||||
|
||||
if len(elems) == 0 {
|
||||
return cty.MapValEmpty(s.Nested.impliedType()), diags
|
||||
}
|
||||
|
||||
var ctyMap func(map[string]interface{}, int) cty.Value
|
||||
ctyMap = func(raw map[string]interface{}, depth int) cty.Value {
|
||||
if len(raw) == 0 {
|
||||
// FIXME: We don't currently have enough info to construct a type for
|
||||
// an empty map, so we'll just stub it out.
|
||||
return cty.MapValEmpty(cty.DynamicPseudoType)
|
||||
}
|
||||
vals := make(map[string]cty.Value, len(raw))
|
||||
if depth == 1 {
|
||||
for k, v := range raw {
|
||||
@ -666,6 +714,14 @@ func (s *BlockMapSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
|
||||
return ctyMap(elems, len(s.LabelNames)), diags
|
||||
}
|
||||
|
||||
func (s *BlockMapSpec) impliedType() cty.Type {
|
||||
ret := s.Nested.impliedType()
|
||||
for _ = range s.LabelNames {
|
||||
ret = cty.Map(ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *BlockMapSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// We return the source range of the _first_ block of the given type,
|
||||
// since they are not guaranteed to form a contiguous range.
|
||||
@ -715,6 +771,10 @@ func (s *BlockLabelSpec) decode(content *hcl.BodyContent, blockLabels []blockLab
|
||||
return cty.StringVal(blockLabels[s.Index].Value), nil
|
||||
}
|
||||
|
||||
func (s *BlockLabelSpec) impliedType() cty.Type {
|
||||
return cty.String // labels are always strings
|
||||
}
|
||||
|
||||
func (s *BlockLabelSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
if s.Index >= len(blockLabels) {
|
||||
panic("BlockListSpec used in non-block context")
|
||||
@ -763,6 +823,9 @@ func findLabelSpecs(spec Spec) []string {
|
||||
|
||||
// DefaultSpec is a spec that wraps two specs, evaluating the primary first
|
||||
// and then evaluating the default if the primary returns a null value.
|
||||
//
|
||||
// The two specifications must have the same implied result type for correct
|
||||
// operation. If not, the result is undefined.
|
||||
type DefaultSpec struct {
|
||||
Primary Spec
|
||||
Default Spec
|
||||
@ -783,6 +846,10 @@ func (s *DefaultSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel,
|
||||
return val, diags
|
||||
}
|
||||
|
||||
func (s *DefaultSpec) impliedType() cty.Type {
|
||||
return s.Primary.impliedType()
|
||||
}
|
||||
|
||||
func (s *DefaultSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
|
||||
// We can't tell from here which of the two specs will ultimately be used
|
||||
// in our result, so we'll just assume the first. This is usually the right
|
||||
|
Loading…
Reference in New Issue
Block a user