github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/hclext/schema.go (about) 1 package hclext 2 3 import ( 4 "fmt" 5 "reflect" 6 "sort" 7 "strings" 8 ) 9 10 // SchemaMode controls how the body's schema is declared. 11 // 12 //go:generate stringer -type=SchemaMode 13 type SchemaMode int32 14 15 const ( 16 // SchemaDefaultMode is a mode for explicitly declaring the structure of attributes and blocks. 17 SchemaDefaultMode SchemaMode = iota 18 // SchemaJustAttributesMode is the mode to extract body as attributes. 19 // In this mode you don't need to declare schema for attributes or blocks. 20 SchemaJustAttributesMode 21 ) 22 23 // BodySchema represents the desired body. 24 // This structure is designed to have attributes similar to hcl.BodySchema. 25 type BodySchema struct { 26 Mode SchemaMode 27 Attributes []AttributeSchema 28 Blocks []BlockSchema 29 } 30 31 // AttributeSchema represents the desired attribute. 32 // This structure is designed to have attributes similar to hcl.AttributeSchema. 33 type AttributeSchema struct { 34 Name string 35 Required bool 36 } 37 38 // BlockSchema represents the desired block header and body schema. 39 // Unlike hcl.BlockHeaderSchema, this can set nested body schema. 40 // Instead, hclext.Block can't handle abstract values like hcl.Body, 41 // so you need to specify all nested schemas at once. 42 type BlockSchema struct { 43 Type string 44 LabelNames []string 45 46 Body *BodySchema 47 } 48 49 // ImpliedBodySchema is a derivative of gohcl.ImpliedBodySchema that produces hclext.BodySchema instead of hcl.BodySchema. 50 // Unlike gohcl.ImpliedBodySchema, it produces nested schemas. 51 // This method differs from gohcl.DecodeBody in several ways: 52 // 53 // - Does not support `body` and `remain` tags. 54 // - Does not support partial schema. 55 // 56 // @see https://github.com/hashicorp/hcl/blob/v2.11.1/gohcl/schema.go 57 func ImpliedBodySchema(val interface{}) *BodySchema { 58 return impliedBodySchema(reflect.TypeOf(val)) 59 } 60 61 func impliedBodySchema(ty reflect.Type) *BodySchema { 62 if ty.Kind() == reflect.Ptr { 63 ty = ty.Elem() 64 } 65 66 if ty.Kind() != reflect.Struct { 67 panic(fmt.Sprintf("given type must be struct, not %s", ty.Name())) 68 } 69 70 var attrSchemas []AttributeSchema 71 var blockSchemas []BlockSchema 72 73 tags := getFieldTags(ty) 74 75 attrNames := make([]string, 0, len(tags.Attributes)) 76 for n := range tags.Attributes { 77 attrNames = append(attrNames, n) 78 } 79 sort.Strings(attrNames) 80 for _, n := range attrNames { 81 idx := tags.Attributes[n] 82 optional := tags.Optional[n] 83 field := ty.Field(idx) 84 85 var required bool 86 87 switch { 88 case field.Type.Kind() != reflect.Ptr && !optional: 89 required = true 90 default: 91 required = false 92 } 93 94 attrSchemas = append(attrSchemas, AttributeSchema{ 95 Name: n, 96 Required: required, 97 }) 98 } 99 100 blockNames := make([]string, 0, len(tags.Blocks)) 101 for n := range tags.Blocks { 102 blockNames = append(blockNames, n) 103 } 104 sort.Strings(blockNames) 105 for _, n := range blockNames { 106 idx := tags.Blocks[n] 107 field := ty.Field(idx) 108 fty := field.Type 109 if fty.Kind() == reflect.Slice { 110 fty = fty.Elem() 111 } 112 if fty.Kind() == reflect.Ptr { 113 fty = fty.Elem() 114 } 115 if fty.Kind() != reflect.Struct { 116 panic(fmt.Sprintf( 117 "schema 'block' tag kind cannot be applied to %s field %s: struct required", field.Type.String(), field.Name, 118 )) 119 } 120 ftags := getFieldTags(fty) 121 var labelNames []string 122 if len(ftags.Labels) > 0 { 123 labelNames = make([]string, len(ftags.Labels)) 124 for i, l := range ftags.Labels { 125 labelNames[i] = l.Name 126 } 127 } 128 129 blockSchemas = append(blockSchemas, BlockSchema{ 130 Type: n, 131 LabelNames: labelNames, 132 Body: impliedBodySchema(fty), 133 }) 134 } 135 136 return &BodySchema{ 137 Attributes: attrSchemas, 138 Blocks: blockSchemas, 139 } 140 } 141 142 type fieldTags struct { 143 Attributes map[string]int 144 Blocks map[string]int 145 Labels []labelField 146 Optional map[string]bool 147 } 148 149 type labelField struct { 150 FieldIndex int 151 Name string 152 } 153 154 func getFieldTags(ty reflect.Type) *fieldTags { 155 ret := &fieldTags{ 156 Attributes: map[string]int{}, 157 Blocks: map[string]int{}, 158 Optional: map[string]bool{}, 159 } 160 161 ct := ty.NumField() 162 for i := 0; i < ct; i++ { 163 field := ty.Field(i) 164 tag := field.Tag.Get("hclext") 165 if tag == "" { 166 continue 167 } 168 169 comma := strings.Index(tag, ",") 170 var name, kind string 171 if comma != -1 { 172 name = tag[:comma] 173 kind = tag[comma+1:] 174 } else { 175 name = tag 176 kind = "attr" 177 } 178 179 switch kind { 180 case "attr": 181 ret.Attributes[name] = i 182 case "block": 183 ret.Blocks[name] = i 184 case "label": 185 ret.Labels = append(ret.Labels, labelField{ 186 FieldIndex: i, 187 Name: name, 188 }) 189 case "optional": 190 ret.Attributes[name] = i 191 ret.Optional[name] = true 192 case "remain", "body": 193 panic(fmt.Sprintf("'%s' tag is permitted in HCL, but not permitted in schema", kind)) 194 default: 195 panic(fmt.Sprintf("invalid schema field tag kind %q on %s %q", kind, field.Type.String(), field.Name)) 196 } 197 } 198 199 return ret 200 }