github.com/hashicorp/hcl/v2@v2.20.0/gohcl/schema.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package gohcl
     5  
     6  import (
     7  	"fmt"
     8  	"reflect"
     9  	"sort"
    10  	"strings"
    11  
    12  	"github.com/hashicorp/hcl/v2"
    13  )
    14  
    15  // ImpliedBodySchema produces a hcl.BodySchema derived from the type of the
    16  // given value, which must be a struct value or a pointer to one. If an
    17  // inappropriate value is passed, this function will panic.
    18  //
    19  // The second return argument indicates whether the given struct includes
    20  // a "remain" field, and thus the returned schema is non-exhaustive.
    21  //
    22  // This uses the tags on the fields of the struct to discover how each
    23  // field's value should be expressed within configuration. If an invalid
    24  // mapping is attempted, this function will panic.
    25  func ImpliedBodySchema(val interface{}) (schema *hcl.BodySchema, partial bool) {
    26  	ty := reflect.TypeOf(val)
    27  
    28  	if ty.Kind() == reflect.Ptr {
    29  		ty = ty.Elem()
    30  	}
    31  
    32  	if ty.Kind() != reflect.Struct {
    33  		panic(fmt.Sprintf("given value must be struct, not %T", val))
    34  	}
    35  
    36  	var attrSchemas []hcl.AttributeSchema
    37  	var blockSchemas []hcl.BlockHeaderSchema
    38  
    39  	tags := getFieldTags(ty)
    40  
    41  	attrNames := make([]string, 0, len(tags.Attributes))
    42  	for n := range tags.Attributes {
    43  		attrNames = append(attrNames, n)
    44  	}
    45  	sort.Strings(attrNames)
    46  	for _, n := range attrNames {
    47  		idx := tags.Attributes[n]
    48  		optional := tags.Optional[n]
    49  		field := ty.Field(idx)
    50  
    51  		var required bool
    52  
    53  		switch {
    54  		case field.Type.AssignableTo(exprType):
    55  			// If we're decoding to hcl.Expression then absense can be
    56  			// indicated via a null value, so we don't specify that
    57  			// the field is required during decoding.
    58  			required = false
    59  		case field.Type.Kind() != reflect.Ptr && !optional:
    60  			required = true
    61  		default:
    62  			required = false
    63  		}
    64  
    65  		attrSchemas = append(attrSchemas, hcl.AttributeSchema{
    66  			Name:     n,
    67  			Required: required,
    68  		})
    69  	}
    70  
    71  	blockNames := make([]string, 0, len(tags.Blocks))
    72  	for n := range tags.Blocks {
    73  		blockNames = append(blockNames, n)
    74  	}
    75  	sort.Strings(blockNames)
    76  	for _, n := range blockNames {
    77  		idx := tags.Blocks[n]
    78  		field := ty.Field(idx)
    79  		fty := field.Type
    80  		if fty.Kind() == reflect.Slice {
    81  			fty = fty.Elem()
    82  		}
    83  		if fty.Kind() == reflect.Ptr {
    84  			fty = fty.Elem()
    85  		}
    86  		if fty.Kind() != reflect.Struct {
    87  			panic(fmt.Sprintf(
    88  				"hcl 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name,
    89  			))
    90  		}
    91  		ftags := getFieldTags(fty)
    92  		var labelNames []string
    93  		if len(ftags.Labels) > 0 {
    94  			labelNames = make([]string, len(ftags.Labels))
    95  			for i, l := range ftags.Labels {
    96  				labelNames[i] = l.Name
    97  			}
    98  		}
    99  
   100  		blockSchemas = append(blockSchemas, hcl.BlockHeaderSchema{
   101  			Type:       n,
   102  			LabelNames: labelNames,
   103  		})
   104  	}
   105  
   106  	partial = tags.Remain != nil
   107  	schema = &hcl.BodySchema{
   108  		Attributes: attrSchemas,
   109  		Blocks:     blockSchemas,
   110  	}
   111  	return schema, partial
   112  }
   113  
   114  type fieldTags struct {
   115  	Attributes map[string]int
   116  	Blocks     map[string]int
   117  	Labels     []labelField
   118  	Remain     *int
   119  	Body       *int
   120  	Optional   map[string]bool
   121  }
   122  
   123  type labelField struct {
   124  	FieldIndex int
   125  	Name       string
   126  }
   127  
   128  func getFieldTags(ty reflect.Type) *fieldTags {
   129  	ret := &fieldTags{
   130  		Attributes: map[string]int{},
   131  		Blocks:     map[string]int{},
   132  		Optional:   map[string]bool{},
   133  	}
   134  
   135  	ct := ty.NumField()
   136  	for i := 0; i < ct; i++ {
   137  		field := ty.Field(i)
   138  		tag := field.Tag.Get("hcl")
   139  		if tag == "" {
   140  			continue
   141  		}
   142  
   143  		comma := strings.Index(tag, ",")
   144  		var name, kind string
   145  		if comma != -1 {
   146  			name = tag[:comma]
   147  			kind = tag[comma+1:]
   148  		} else {
   149  			name = tag
   150  			kind = "attr"
   151  		}
   152  
   153  		switch kind {
   154  		case "attr":
   155  			ret.Attributes[name] = i
   156  		case "block":
   157  			ret.Blocks[name] = i
   158  		case "label":
   159  			ret.Labels = append(ret.Labels, labelField{
   160  				FieldIndex: i,
   161  				Name:       name,
   162  			})
   163  		case "remain":
   164  			if ret.Remain != nil {
   165  				panic("only one 'remain' tag is permitted")
   166  			}
   167  			idx := i // copy, because this loop will continue assigning to i
   168  			ret.Remain = &idx
   169  		case "body":
   170  			if ret.Body != nil {
   171  				panic("only one 'body' tag is permitted")
   172  			}
   173  			idx := i // copy, because this loop will continue assigning to i
   174  			ret.Body = &idx
   175  		case "optional":
   176  			ret.Attributes[name] = i
   177  			ret.Optional[name] = true
   178  		default:
   179  			panic(fmt.Sprintf("invalid hcl field tag kind %q on %s %q", kind, field.Type.String(), field.Name))
   180  		}
   181  	}
   182  
   183  	return ret
   184  }