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 }