gohcl: Decode missing hcl.Expression as a cty Null

Rather than writing a nil hcl.Expression, as a special case we'll deal
with this within the cty type system, which we assume is what the caller
wants if they are decoding into a hcl.Expression rather than a native Go
type.
This commit is contained in:
Martin Atkins 2017-09-20 16:23:23 -07:00
parent 339b9cfc34
commit f44382c4fa
4 changed files with 105 additions and 3 deletions

View File

@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl"
"github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/convert"
"github.com/zclconf/go-cty/cty/gocty" "github.com/zclconf/go-cty/cty/gocty"
@ -81,11 +83,25 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value)
} }
} }
for name, attr := range content.Attributes { for name, fieldIdx := range tags.Attributes {
fieldIdx := tags.Attributes[name] attr := content.Attributes[name]
field := val.Type().Field(fieldIdx) field := val.Type().Field(fieldIdx)
fieldV := val.Field(fieldIdx) fieldV := val.Field(fieldIdx)
if attr == nil {
if !exprType.AssignableTo(field.Type) {
continue
}
// As a special case, if the target is of type hcl.Expression then
// we'll assign an actual expression that evalues to a cty null,
// so the caller can deal with it within the cty realm rather
// than within the Go realm.
synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange())
fieldV.Set(reflect.ValueOf(synthExpr))
continue
}
switch { switch {
case attrType.AssignableTo(field.Type): case attrType.AssignableTo(field.Type):
fieldV.Set(reflect.ValueOf(attr)) fieldV.Set(reflect.ValueOf(attr))

View File

@ -19,6 +19,10 @@ func TestDecodeBody(t *testing.T) {
} }
} }
type withNameExpression struct {
Name hcl.Expression `hcl:"name"`
}
tests := []struct { tests := []struct {
Body map[string]interface{} Body map[string]interface{}
Target interface{} Target interface{}
@ -51,6 +55,60 @@ func TestDecodeBody(t *testing.T) {
}{}), }{}),
0, 0,
}, },
{
map[string]interface{}{},
withNameExpression{},
func(v interface{}) bool {
if v == nil {
return false
}
wne, valid := v.(withNameExpression)
if !valid {
return false
}
if wne.Name == nil {
return false
}
nameVal, _ := wne.Name.Value(nil)
if !nameVal.IsNull() {
return false
}
return true
},
0,
},
{
map[string]interface{}{
"name": "Ermintrude",
},
withNameExpression{},
func(v interface{}) bool {
if v == nil {
return false
}
wne, valid := v.(withNameExpression)
if !valid {
return false
}
if wne.Name == nil {
return false
}
nameVal, _ := wne.Name.Value(nil)
if !nameVal.Equals(cty.StringVal("Ermintrude")).True() {
return false
}
return true
},
0,
},
{ {
map[string]interface{}{ map[string]interface{}{
"name": "Ermintrude", "name": "Ermintrude",

View File

@ -43,9 +43,23 @@ func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) {
for _, n := range attrNames { for _, n := range attrNames {
idx := tags.Attributes[n] idx := tags.Attributes[n]
field := ty.Field(idx) field := ty.Field(idx)
var required bool
switch {
case field.Type.AssignableTo(exprType):
// If we're decoding to hcl.Expression then absense can be
// indicated via a null value, so we don't specify that
// the field is required during decoding.
required = false
case field.Type.Kind() != reflect.Ptr:
required = true
default:
required = false
}
attrSchemas = append(attrSchemas, hcl.AttributeSchema{ attrSchemas = append(attrSchemas, hcl.AttributeSchema{
Name: n, Name: n,
Required: field.Type.Kind() != reflect.Ptr, Required: required,
}) })
} }

View File

@ -179,6 +179,20 @@ func TestImpliedBodySchema(t *testing.T) {
}, },
true, true,
}, },
{
struct {
Expr hcl.Expression `hcl:"expr"`
}{},
&hcl.BodySchema{
Attributes: []hcl.AttributeSchema{
{
Name: "expr",
Required: false,
},
},
},
false,
},
} }
for _, test := range tests { for _, test := range tests {