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:
Martin Atkins 2017-10-03 16:27:34 -07:00
parent 0d6247f4cf
commit 44bad6dbf5
4 changed files with 97 additions and 20 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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
},
}

View File

@ -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