Skip to content

Commit

Permalink
hcldec: ImpliedType function
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
apparentlymart committed Oct 3, 2017
1 parent 0d6247f commit 44bad6d
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 20 deletions.
4 changes: 4 additions & 0 deletions hcldec/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions hcldec/public.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 8 additions & 8 deletions hcldec/public_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ b {
},
},
nil,
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.String),
1, // missing name label
},
{
Expand All @@ -203,7 +203,7 @@ b {
Nested: ObjectSpec{},
},
nil,
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.EmptyObject),
0,
},
{
Expand All @@ -213,7 +213,7 @@ b {
Nested: ObjectSpec{},
},
nil,
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.EmptyObject),
1, // blocks of type "a" are not supported
},
{
Expand All @@ -224,7 +224,7 @@ b {
Required: true,
},
nil,
cty.NullVal(cty.DynamicPseudoType),
cty.NullVal(cty.EmptyObject),
1, // a block of type "b" is required
},
{
Expand Down Expand Up @@ -261,7 +261,7 @@ b {}
Nested: ObjectSpec{},
},
nil,
cty.ListValEmpty(cty.DynamicPseudoType),
cty.ListValEmpty(cty.EmptyObject),
0,
},
{
Expand Down Expand Up @@ -433,7 +433,7 @@ b "foo" "bar" {}
Nested: ObjectSpec{},
},
nil,
cty.MapValEmpty(cty.DynamicPseudoType),
cty.MapValEmpty(cty.EmptyObject),
1, // too many labels
},
{
Expand All @@ -446,7 +446,7 @@ b "bar" {}
Nested: ObjectSpec{},
},
nil,
cty.MapValEmpty(cty.DynamicPseudoType),
cty.MapValEmpty(cty.EmptyObject),
1, // not enough labels
},
{
Expand Down Expand Up @@ -510,7 +510,7 @@ b "foo" {}
},
},
nil,
cty.MapValEmpty(cty.DynamicPseudoType),
cty.MapValEmpty(cty.String),
1, // missing name
},
}
Expand Down
91 changes: 79 additions & 12 deletions hcldec/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -418,16 +463,18 @@ 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)
}

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.
Expand Down Expand Up @@ -526,16 +573,18 @@ 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)
}

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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 44bad6d

Please sign in to comment.