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 return val, leftovers, diags
} }
func impliedType(spec Spec) cty.Type {
return spec.impliedType()
}
func sourceRange(body hcl.Body, blockLabels []blockLabel, spec Spec) hcl.Range { func sourceRange(body hcl.Body, blockLabels []blockLabel, spec Spec) hcl.Range {
schema := ImpliedSchema(spec) schema := ImpliedSchema(spec)
content, _, _ := body.PartialContent(schema) 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) 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 // SourceRange interprets the given body using the given specification and
// then returns the source range of the value that would be used to // then returns the source range of the value that would be used to
// fulfill the spec. // fulfill the spec.

View File

@ -193,7 +193,7 @@ b {
}, },
}, },
nil, nil,
cty.NullVal(cty.DynamicPseudoType), cty.NullVal(cty.String),
1, // missing name label 1, // missing name label
}, },
{ {
@ -203,7 +203,7 @@ b {
Nested: ObjectSpec{}, Nested: ObjectSpec{},
}, },
nil, nil,
cty.NullVal(cty.DynamicPseudoType), cty.NullVal(cty.EmptyObject),
0, 0,
}, },
{ {
@ -213,7 +213,7 @@ b {
Nested: ObjectSpec{}, Nested: ObjectSpec{},
}, },
nil, nil,
cty.NullVal(cty.DynamicPseudoType), cty.NullVal(cty.EmptyObject),
1, // blocks of type "a" are not supported 1, // blocks of type "a" are not supported
}, },
{ {
@ -224,7 +224,7 @@ b {
Required: true, Required: true,
}, },
nil, nil,
cty.NullVal(cty.DynamicPseudoType), cty.NullVal(cty.EmptyObject),
1, // a block of type "b" is required 1, // a block of type "b" is required
}, },
{ {
@ -261,7 +261,7 @@ b {}
Nested: ObjectSpec{}, Nested: ObjectSpec{},
}, },
nil, nil,
cty.ListValEmpty(cty.DynamicPseudoType), cty.ListValEmpty(cty.EmptyObject),
0, 0,
}, },
{ {
@ -433,7 +433,7 @@ b "foo" "bar" {}
Nested: ObjectSpec{}, Nested: ObjectSpec{},
}, },
nil, nil,
cty.MapValEmpty(cty.DynamicPseudoType), cty.MapValEmpty(cty.EmptyObject),
1, // too many labels 1, // too many labels
}, },
{ {
@ -446,7 +446,7 @@ b "bar" {}
Nested: ObjectSpec{}, Nested: ObjectSpec{},
}, },
nil, nil,
cty.MapValEmpty(cty.DynamicPseudoType), cty.MapValEmpty(cty.EmptyObject),
1, // not enough labels 1, // not enough labels
}, },
{ {
@ -510,7 +510,7 @@ b "foo" {}
}, },
}, },
nil, nil,
cty.MapValEmpty(cty.DynamicPseudoType), cty.MapValEmpty(cty.String),
1, // missing name 1, // missing name
}, },
} }

View File

@ -22,6 +22,10 @@ type Spec interface {
// types that work on block bodies. // types that work on block bodies.
decode(content *hcl.BodyContent, blockLabels []blockLabel, ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) 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 // 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 // get decoded with the same body and block as the receiver. This should
// not descend into the nested specs used when decoding blocks. // 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 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 { 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 // 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 // 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 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 { 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 // 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 // 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 return val, diags
} }
func (s *AttrSpec) impliedType() cty.Type {
return s.Type
}
// A LiteralSpec is a Spec that produces the given literal value, ignoring // A LiteralSpec is a Spec that produces the given literal value, ignoring
// the given body. // the given body.
type LiteralSpec struct { type LiteralSpec struct {
@ -200,6 +232,10 @@ func (s *LiteralSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel,
return s.Value, nil 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 { func (s *LiteralSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// No sensible range to return for a literal, so the caller had better // No sensible range to return for a literal, so the caller had better
// ensure it doesn't cause any diagnostics. // 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) 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 { func (s *ExprSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
return s.Expr.Range() return s.Expr.Range()
} }
@ -312,7 +353,7 @@ func (s *BlockSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, c
Subject: &content.MissingItemRange, Subject: &content.MissingItemRange,
}) })
} }
return cty.NullVal(cty.DynamicPseudoType), diags return cty.NullVal(s.Nested.impliedType()), diags
} }
if s.Nested == nil { if s.Nested == nil {
@ -323,6 +364,10 @@ func (s *BlockSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel, c
return val, diags return val, diags
} }
func (s *BlockSpec) impliedType() cty.Type {
return s.Nested.impliedType()
}
func (s *BlockSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { func (s *BlockSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
var childBlock *hcl.Block var childBlock *hcl.Block
for _, candidate := range content.Blocks { for _, candidate := range content.Blocks {
@ -418,9 +463,7 @@ func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabe
var ret cty.Value var ret cty.Value
if len(elems) == 0 { if len(elems) == 0 {
// FIXME: We don't currently have enough info to construct a type for ret = cty.ListValEmpty(s.Nested.impliedType())
// an empty list, so we'll just stub it out.
ret = cty.ListValEmpty(cty.DynamicPseudoType)
} else { } else {
ret = cty.ListVal(elems) ret = cty.ListVal(elems)
} }
@ -428,6 +471,10 @@ func (s *BlockListSpec) decode(content *hcl.BodyContent, blockLabels []blockLabe
return ret, diags 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 { func (s *BlockListSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We return the source range of the _first_ block of the given type, // We return the source range of the _first_ block of the given type,
// since they are not guaranteed to form a contiguous range. // 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 var ret cty.Value
if len(elems) == 0 { if len(elems) == 0 {
// FIXME: We don't currently have enough info to construct a type for ret = cty.SetValEmpty(s.Nested.impliedType())
// an empty list, so we'll just stub it out.
ret = cty.SetValEmpty(cty.DynamicPseudoType)
} else { } else {
ret = cty.SetVal(elems) ret = cty.SetVal(elems)
} }
@ -536,6 +581,10 @@ func (s *BlockSetSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel
return ret, diags 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 { func (s *BlockSetSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We return the source range of the _first_ block of the given type, // We return the source range of the _first_ block of the given type,
// since they are not guaranteed to form a contiguous range. // 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 targetMap[key] = val
} }
if len(elems) == 0 {
return cty.MapValEmpty(s.Nested.impliedType()), diags
}
var ctyMap func(map[string]interface{}, int) cty.Value var ctyMap func(map[string]interface{}, int) cty.Value
ctyMap = func(raw map[string]interface{}, depth 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)) vals := make(map[string]cty.Value, len(raw))
if depth == 1 { if depth == 1 {
for k, v := range raw { 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 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 { func (s *BlockMapSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
// We return the source range of the _first_ block of the given type, // We return the source range of the _first_ block of the given type,
// since they are not guaranteed to form a contiguous range. // 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 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 { func (s *BlockLabelSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range {
if s.Index >= len(blockLabels) { if s.Index >= len(blockLabels) {
panic("BlockListSpec used in non-block context") 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 // DefaultSpec is a spec that wraps two specs, evaluating the primary first
// and then evaluating the default if the primary returns a null value. // 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 { type DefaultSpec struct {
Primary Spec Primary Spec
Default Spec Default Spec
@ -783,6 +846,10 @@ func (s *DefaultSpec) decode(content *hcl.BodyContent, blockLabels []blockLabel,
return val, diags return val, diags
} }
func (s *DefaultSpec) impliedType() cty.Type {
return s.Primary.impliedType()
}
func (s *DefaultSpec) sourceRange(content *hcl.BodyContent, blockLabels []blockLabel) hcl.Range { 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 // 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 // in our result, so we'll just assume the first. This is usually the right