|
| 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 | +} |
0 commit comments