mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-25 08:21:07 -06:00
07a9185767
Signed-off-by: Christian Mesh <christianmesh1@gmail.com> Signed-off-by: James Humphries <james@james-humphries.co.uk> Co-authored-by: James Humphries <james@james-humphries.co.uk>
197 lines
4.4 KiB
Go
197 lines
4.4 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
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
|
|
}
|
|
|
|
func hasFieldTags(ty reflect.Type) bool {
|
|
ct := ty.NumField()
|
|
for i := 0; i < ct; i++ {
|
|
field := ty.Field(i)
|
|
tag := field.Tag.Get("hcl")
|
|
if tag != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|