summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMartin Atkins <mart@degeneration.co.uk>2017-10-03 16:27:34 -0700
committerMartin Atkins <mart@degeneration.co.uk>2017-10-03 16:27:34 -0700
commit44bad6dbf5490f5da17ec991e664df3d017b706f (patch)
tree9171c3b91ee4751d2a57269f61317b2f9c50b2eb
parent0d6247f4cf3675fe1349abf5796c4dc7880ab130 (diff)
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.
-rw-r--r--hcldec/decode.go4
-rw-r--r--hcldec/public.go6
-rw-r--r--hcldec/public_test.go16
-rw-r--r--hcldec/spec.go91
4 files changed, 97 insertions, 20 deletions
diff --git a/hcldec/decode.go b/hcldec/decode.go
index bca2787..6cf93fe 100644
--- a/hcldec/decode.go
+++ b/hcldec/decode.go
@@ -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)
diff --git a/hcldec/public.go b/hcldec/public.go
index a5f693a..3e58f7b 100644
--- a/hcldec/public.go
+++ b/hcldec/public.go
@@ -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.
diff --git a/hcldec/public_test.go b/hcldec/public_test.go
index 73f21b5..23406dc 100644
--- a/hcldec/public_test.go
+++ b/hcldec/public_test.go
@@ -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
},
}
diff --git a/hcldec/spec.go b/hcldec/spec.go
index 41c4ae0..f0e6842 100644
--- a/hcldec/spec.go
+++ b/hcldec/spec.go
@@ -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