23fc060132
Previously we required optional attributes to be specified as pointers so that we could represent the empty vs. absent distinction. For applications that don't need to make that distinction, representing "optional" as a struct tag is more convenient.
175 lines
4.0 KiB
Go
175 lines
4.0 KiB
Go
package gohcl
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
)
|
|
|
|
// 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
|
|
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 "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
|
|
}
|