From b54debed5c6bef287fdbe58701d4f06f05a1fac8 Mon Sep 17 00:00:00 2001 From: RouxAntoine Date: Mon, 8 Mar 2021 00:56:12 +0100 Subject: [PATCH] feat: Support composite struct with nested keyword --- gohcl/decode.go | 33 +++++++++++++++++++++++++++ gohcl/schema.go | 52 +++++++++++++++++++++++++++++++++++++++++- hclsyntax/structure.go | 2 +- schema.go | 8 +++++++ 4 files changed, 93 insertions(+), 2 deletions(-) diff --git a/gohcl/decode.go b/gohcl/decode.go index 302a3f4..e9646e2 100644 --- a/gohcl/decode.go +++ b/gohcl/decode.go @@ -78,6 +78,39 @@ func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) } } + for _, fieldIdx := range tags.Nested { + field := val.Type().Field(fieldIdx) + fieldV := val.Field(fieldIdx) + + ty := field.Type + isPtr := false + if ty.Kind() == reflect.Ptr { + isPtr = true + ty = ty.Elem() + } + + switch { + case bodyType.AssignableTo(field.Type): + fieldV.Set(reflect.ValueOf(leftovers)) + case attrsType.AssignableTo(field.Type): + attrs, attrsDiags := leftovers.JustAttributes() + if len(attrsDiags) > 0 { + diags = append(diags, attrsDiags...) + } + fieldV.Set(reflect.ValueOf(attrs)) + default: + v := fieldV + if isPtr { + if v.IsNil() { + v = reflect.New(ty) + } + v = v.Elem() + } + diags = append(diags, decodeBodyToValue(leftovers, ctx, v)...) + fieldV.Set(v) + } + } + if tags.Remain != nil { fieldIdx := *tags.Remain field := val.Type().Field(fieldIdx) diff --git a/gohcl/schema.go b/gohcl/schema.go index df21cc4..9797cec 100644 --- a/gohcl/schema.go +++ b/gohcl/schema.go @@ -32,6 +32,7 @@ func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) { var attrSchemas []hcl.AttributeSchema var blockSchemas []hcl.BlockHeaderSchema + var nestedBlockSchemas []hcl.NestedBlockSchemas tags := getFieldTags(ty) @@ -100,10 +101,51 @@ func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) { }) } - partial = tags.Remain != nil + nestedNames := make([]string, 0, len(tags.Nested)) + for n := range tags.Nested { + nestedNames = append(nestedNames, n) + } + sort.Strings(nestedNames) + for _, n := range nestedNames { + idx := tags.Nested[n] + optional := tags.Optional[n] + field := ty.Field(idx) + fty := field.Type + + // if its a pointer get target element + if fty.Kind() == reflect.Ptr { + fty = fty.Elem() + } + if fty.Kind() != reflect.Struct { + panic(fmt.Sprintf( + "hcl 'nested' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name, + )) + } + + 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 && !optional: + required = true + default: + required = false + } + + nestedBlockSchemas = append(nestedBlockSchemas, hcl.NestedBlockSchemas{ + Name: n, + Required: required, + }) + } + + partial = tags.Remain != nil || len(tags.Nested) > 0 schema = &hcl.BodySchema{ Attributes: attrSchemas, Blocks: blockSchemas, + Nested: nestedBlockSchemas, } return schema, partial } @@ -115,6 +157,7 @@ type fieldTags struct { Remain *int Body *int Optional map[string]bool + Nested map[string]int } type labelField struct { @@ -127,6 +170,7 @@ func getFieldTags(ty reflect.Type) *fieldTags { Attributes: map[string]int{}, Blocks: map[string]int{}, Optional: map[string]bool{}, + Nested: map[string]int{}, } ct := ty.NumField() @@ -172,6 +216,12 @@ func getFieldTags(ty reflect.Type) *fieldTags { case "optional": ret.Attributes[name] = i ret.Optional[name] = true + case "nested": + // name anonymous embedded type with anon- + if name == "" { + name = fmt.Sprintf("anon-%d", i) + } + ret.Nested[name] = i default: panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name)) } diff --git a/hclsyntax/structure.go b/hclsyntax/structure.go index 2f7470c..28b3d41 100644 --- a/hclsyntax/structure.go +++ b/hclsyntax/structure.go @@ -58,7 +58,7 @@ func (b *Body) Range() hcl.Range { func (b *Body) Content(schema *hcl.BodySchema) (*hcl.BodyContent, hcl.Diagnostics) { content, remainHCL, diags := b.PartialContent(schema) - // No we'll see if anything actually remains, to produce errors about + // Now we'll see if anything actually remains, to produce errors about // extraneous items. remain := remainHCL.(*Body) diff --git a/schema.go b/schema.go index 891257a..9dac9e0 100644 --- a/schema.go +++ b/schema.go @@ -14,8 +14,16 @@ type AttributeSchema struct { Required bool } +// NestedBlockSchemas represent the requirements for a list of nested +// attributes or block +type NestedBlockSchemas struct { + Name string + Required bool +} + // BodySchema represents the desired shallow structure of a body. type BodySchema struct { Attributes []AttributeSchema Blocks []BlockHeaderSchema + Nested []NestedBlockSchemas }