github.com/terraform-linters/tflint-plugin-sdk@v0.22.0/hclext/decode.go (about) 1 package hclext 2 3 import ( 4 "fmt" 5 "reflect" 6 "strings" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/hashicorp/hcl/v2/gohcl" 10 ) 11 12 // DecodeBody is a derivative of gohcl.DecodeBody the receives hclext.BodyContent instead of hcl.Body. 13 // Since hcl.Body is hard to send over a wire protocol, it is needed to support BodyContent. 14 // This method differs from gohcl.DecodeBody in several ways: 15 // 16 // - Does not support decoding to map, cty.Value, hcl.Body, hcl.Expression. 17 // - Does not support `body` and `remain` tags. 18 // - Extraneous attributes are always ignored. 19 // 20 // @see https://github.com/hashicorp/hcl/blob/v2.11.1/gohcl/decode.go 21 func DecodeBody(body *BodyContent, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { 22 rv := reflect.ValueOf(val) 23 if rv.Kind() != reflect.Ptr { 24 panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String())) 25 } 26 27 return decodeBody(body, ctx, rv.Elem()) 28 } 29 30 func decodeBody(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { 31 if body == nil { 32 return nil 33 } 34 35 et := val.Type() 36 switch et.Kind() { 37 case reflect.Struct: 38 return decodeBodyToStruct(body, ctx, val) 39 default: 40 panic(fmt.Sprintf("target value must be pointer to struct, not %s", et.String())) 41 } 42 } 43 44 func decodeBodyToStruct(body *BodyContent, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { 45 var diags hcl.Diagnostics 46 47 tags := getFieldTags(val.Type()) 48 49 for name, fieldIdx := range tags.Attributes { 50 attr, exists := body.Attributes[name] 51 if !exists { 52 if tags.Optional[name] || val.Type().Field(fieldIdx).Type.Kind() == reflect.Ptr { 53 // noop 54 } else { 55 diags = append(diags, &hcl.Diagnostic{ 56 Severity: hcl.DiagError, 57 Summary: fmt.Sprintf("Missing %s attribute", name), 58 Detail: fmt.Sprintf("%s is required, but not defined here", name), 59 }) 60 } 61 continue 62 } 63 diags = diags.Extend(gohcl.DecodeExpression(attr.Expr, ctx, val.Field(fieldIdx).Addr().Interface())) 64 } 65 66 blocksByType := body.Blocks.ByType() 67 68 for typeName, fieldIdx := range tags.Blocks { 69 blocks := blocksByType[typeName] 70 field := val.Type().Field((fieldIdx)) 71 72 ty := field.Type 73 isSlice := false 74 isPtr := false 75 if ty.Kind() == reflect.Slice { 76 isSlice = true 77 ty = ty.Elem() 78 } 79 if ty.Kind() == reflect.Ptr { 80 isPtr = true 81 ty = ty.Elem() 82 } 83 84 if len(blocks) > 1 && !isSlice { 85 diags = append(diags, &hcl.Diagnostic{ 86 Severity: hcl.DiagError, 87 Summary: fmt.Sprintf("Duplicate %s block", typeName), 88 Detail: fmt.Sprintf( 89 "Only one %s block is allowed. Another was defined at %s.", 90 typeName, blocks[0].DefRange.String(), 91 ), 92 Subject: &blocks[1].DefRange, 93 }) 94 continue 95 } 96 97 if len(blocks) == 0 { 98 if isSlice || isPtr { 99 if val.Field(fieldIdx).IsNil() { 100 val.Field(fieldIdx).Set(reflect.Zero(field.Type)) 101 } 102 } else { 103 diags = append(diags, &hcl.Diagnostic{ 104 Severity: hcl.DiagError, 105 Summary: fmt.Sprintf("Missing %s block", typeName), 106 Detail: fmt.Sprintf("A %s block is required.", typeName), 107 }) 108 } 109 continue 110 } 111 112 switch { 113 114 case isSlice: 115 elemType := ty 116 if isPtr { 117 elemType = reflect.PtrTo(ty) 118 } 119 sli := val.Field(fieldIdx) 120 if sli.IsNil() { 121 sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks)) 122 } 123 124 for i, block := range blocks { 125 if isPtr { 126 if i >= sli.Len() { 127 sli = reflect.Append(sli, reflect.New(ty)) 128 } 129 v := sli.Index(i) 130 if v.IsNil() { 131 v = reflect.New(ty) 132 } 133 diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) 134 sli.Index(i).Set(v) 135 } else { 136 if i >= sli.Len() { 137 sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty))) 138 } 139 diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...) 140 } 141 } 142 143 if sli.Len() > len(blocks) { 144 sli.SetLen(len(blocks)) 145 } 146 147 val.Field(fieldIdx).Set(sli) 148 149 default: 150 block := blocks[0] 151 if isPtr { 152 v := val.Field(fieldIdx) 153 if v.IsNil() { 154 v = reflect.New(ty) 155 } 156 diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) 157 val.Field(fieldIdx).Set(v) 158 } else { 159 diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...) 160 } 161 162 } 163 } 164 165 return diags 166 } 167 168 func decodeBlockToValue(block *Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { 169 diags := decodeBody(block.Body, ctx, v) 170 171 blockTags := getFieldTags(v.Type()) 172 173 if len(block.Labels) > len(blockTags.Labels) { 174 expectedLabels := make([]string, len(blockTags.Labels)) 175 for i, label := range blockTags.Labels { 176 expectedLabels[i] = label.Name 177 } 178 return append(diags, &hcl.Diagnostic{ 179 Severity: hcl.DiagError, 180 Summary: fmt.Sprintf("Extraneous label for %s", block.Type), 181 Detail: fmt.Sprintf("Only %d labels (%s) are expected for %s blocks.", len(blockTags.Labels), strings.Join(expectedLabels, ", "), block.Type), 182 Subject: &block.DefRange, 183 }) 184 } 185 if len(block.Labels) < len(blockTags.Labels) { 186 expectedLabels := make([]string, len(blockTags.Labels)) 187 for i, label := range blockTags.Labels { 188 expectedLabels[i] = label.Name 189 } 190 return append(diags, &hcl.Diagnostic{ 191 Severity: hcl.DiagError, 192 Summary: fmt.Sprintf("Missing label for %s", block.Type), 193 Detail: fmt.Sprintf("All %s blocks must be have %d labels (%s).", block.Type, len(blockTags.Labels), strings.Join(expectedLabels, ", ")), 194 Subject: &block.DefRange, 195 }) 196 } 197 198 for li, lv := range block.Labels { 199 lfieldIdx := blockTags.Labels[li].FieldIndex 200 v.Field(lfieldIdx).Set(reflect.ValueOf(lv)) 201 } 202 203 return diags 204 }