package gohcl import ( "fmt" "reflect" "sort" "strings" "github.com/hashicorp/hcl/v2" ) // ImpliedBodySchema produces a hcl.BodySchema derived from the type of the // given value, which must be a struct value or a pointer to one. If an // inappropriate value is passed, this function will panic. // // The second return argument indicates whether the given struct includes // a "remain" field, and thus the returned schema is non-exhaustive. // // This uses the tags on the fields of the struct to discover how each // field's value should be expressed within configuration. If an invalid // mapping is attempted, this function will panic. func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) { ty := reflect.TypeOf(val) if ty.Kind() == reflect.Ptr { ty = ty.Elem() } if ty.Kind() != reflect.Struct { panic(fmt.Sprintf("given value must be struct, not %T", val)) } var attrSchemas []hcl.AttributeSchema var blockSchemas []hcl.BlockHeaderSchema var nestedBlockSchemas []hcl.NestedBlockSchemas tags := getFieldTags(ty) attrNames := make([]string, 0, len(tags.Attributes)) for n := range tags.Attributes { attrNames = append(attrNames, n) } sort.Strings(attrNames) for _, n := range attrNames { idx := tags.Attributes[n] optional := tags.Optional[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 && !optional: required = true default: required = false } attrSchemas = append(attrSchemas, hcl.AttributeSchema{ Name: n, Required: required, }) } blockNames := make([]string, 0, len(tags.Blocks)) for n := range tags.Blocks { blockNames = append(blockNames, n) } sort.Strings(blockNames) for _, n := range blockNames { idx := tags.Blocks[n] field := ty.Field(idx) fty := field.Type if fty.Kind() == reflect.Slice { fty = fty.Elem() } if fty.Kind() == reflect.Ptr { fty = fty.Elem() } if fty.Kind() != reflect.Struct { panic(fmt.Sprintf( "hcl 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name, )) } ftags := getFieldTags(fty) var labelNames []string if len(ftags.Labels) > 0 { labelNames = make([]string, len(ftags.Labels)) for i, l := range ftags.Labels { labelNames[i] = l.Name } } blockSchemas = append(blockSchemas, hcl.BlockHeaderSchema{ Type: n, LabelNames: labelNames, }) } 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 } type fieldTags struct { Attributes map[string]int Blocks map[string]int Labels []labelField Remain *int Body *int Optional map[string]bool Nested map[string]int } type labelField struct { FieldIndex int Name string } func getFieldTags(ty reflect.Type) *fieldTags { ret := &fieldTags{ Attributes: map[string]int{}, Blocks: map[string]int{}, Optional: map[string]bool{}, Nested: map[string]int{}, } ct := ty.NumField() for i := 0; i < ct; i++ { field := ty.Field(i) tag := field.Tag.Get("hcl") if tag == "" { continue } comma := strings.Index(tag, ",") var name, kind string if comma != -1 { name = tag[:comma] kind = tag[comma+1:] } else { name = tag kind = "attr" } switch kind { case "attr": ret.Attributes[name] = i case "block": ret.Blocks[name] = i case "label": ret.Labels = append(ret.Labels, labelField{ FieldIndex: i, Name: name, }) case "remain": if ret.Remain != nil { panic("only one 'remain' tag is permitted") } idx := i // copy, because this loop will continue assigning to i ret.Remain = &idx case "body": if ret.Body != nil { panic("only one 'body' tag is permitted") } idx := i // copy, because this loop will continue assigning to i ret.Body = &idx 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)) } } return ret }