diff --git a/gohcl/decode.go b/gohcl/decode.go index 7c9e18b..3a149a8 100644 --- a/gohcl/decode.go +++ b/gohcl/decode.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" + "github.com/zclconf/go-cty/cty" + "github.com/hashicorp/hcl2/hcl" "github.com/zclconf/go-cty/cty/convert" "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 { - fieldIdx := tags.Attributes[name] + for name, fieldIdx := range tags.Attributes { + attr := content.Attributes[name] field := val.Type().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 { case attrType.AssignableTo(field.Type): fieldV.Set(reflect.ValueOf(attr)) diff --git a/gohcl/decode_test.go b/gohcl/decode_test.go index a41c231..c90df7d 100644 --- a/gohcl/decode_test.go +++ b/gohcl/decode_test.go @@ -19,6 +19,10 @@ func TestDecodeBody(t *testing.T) { } } + type withNameExpression struct { + Name hcl.Expression `hcl:"name"` + } + tests := []struct { Body map[string]interface{} Target interface{} @@ -51,6 +55,60 @@ func TestDecodeBody(t *testing.T) { }{}), 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{}{ "name": "Ermintrude", diff --git a/gohcl/schema.go b/gohcl/schema.go index c110773..a8955dc 100644 --- a/gohcl/schema.go +++ b/gohcl/schema.go @@ -43,9 +43,23 @@ func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) { for _, n := range attrNames { idx := tags.Attributes[n] 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{ Name: n, - Required: field.Type.Kind() != reflect.Ptr, + Required: required, }) } diff --git a/gohcl/schema_test.go b/gohcl/schema_test.go index 0c7b0ae..018efdb 100644 --- a/gohcl/schema_test.go +++ b/gohcl/schema_test.go @@ -179,6 +179,20 @@ func TestImpliedBodySchema(t *testing.T) { }, true, }, + { + struct { + Expr hcl.Expression `hcl:"expr"` + }{}, + &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "expr", + Required: false, + }, + }, + }, + false, + }, } for _, test := range tests {