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