38280c6c69
Structs can specify the "body" struct tag to capture the entire block body. This does NOT act as "remain" where leftover fields are non-erroneous. If you want leftover fields to be allowed, a "remain" field must ALSO be present. This is used to capture the full block body that was decoded for the block. This is useful if you want to ever redecode something differently (maybe with a different EvalContext) or partially decode something but redecode the entire value.
182 lines
4.2 KiB
Go
182 lines
4.2 KiB
Go
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
|
|
|
|
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,
|
|
})
|
|
}
|
|
|
|
partial = tags.Remain != nil
|
|
schema = &hcl.BodySchema{
|
|
Attributes: attrSchemas,
|
|
Blocks: blockSchemas,
|
|
}
|
|
return schema, partial
|
|
}
|
|
|
|
type fieldTags struct {
|
|
Attributes map[string]int
|
|
Blocks map[string]int
|
|
Labels []labelField
|
|
Remain *int
|
|
Body *int
|
|
Optional map[string]bool
|
|
}
|
|
|
|
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{},
|
|
}
|
|
|
|
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
|
|
default:
|
|
panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
|
|
}
|
|
}
|
|
|
|
return ret
|
|
}
|