package gohcl import ( "fmt" "reflect" "github.com/zclconf/go-cty/cty" "github.com/hashicorp/hcl/v2" "github.com/zclconf/go-cty/cty/convert" "github.com/zclconf/go-cty/cty/gocty" ) // DecodeBody extracts the configuration within the given body into the given // value. This value must be a non-nil pointer to either a struct or // a map, where in the former case the configuration will be decoded using // struct tags and in the latter case only attributes are allowed and their // values are decoded into the map. // // The given EvalContext is used to resolve any variables or functions in // expressions encountered while decoding. This may be nil to require only // constant values, for simple applications that do not support variables or // functions. // // The returned diagnostics should be inspected with its HasErrors method to // determine if the populated value is valid and complete. If error diagnostics // are returned then the given value may have been partially-populated but // may still be accessed by a careful caller for static analysis and editor // integration use-cases. func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { rv := reflect.ValueOf(val) if rv.Kind() != reflect.Ptr { panic(fmt.Sprintf("target value must be a pointer, not %s", rv.Type().String())) } return decodeBodyToValue(body, ctx, rv.Elem()) } func decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { et := val.Type() switch et.Kind() { case reflect.Struct: return decodeBodyToStruct(body, ctx, val) case reflect.Map: return decodeBodyToMap(body, ctx, val) default: panic(fmt.Sprintf("target value must be pointer to struct or map, not %s", et.String())) } } func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics { schema, partial := ImpliedBodySchema(val.Interface()) var content *hcl.BodyContent var leftovers hcl.Body var diags hcl.Diagnostics if partial { content, leftovers, diags = body.PartialContent(schema) } else { content, diags = body.Content(schema) } if content == nil { return diags } tags := getFieldTags(val.Type()) if tags.Body != nil { fieldIdx := *tags.Body field := val.Type().Field(fieldIdx) fieldV := val.Field(fieldIdx) switch { case bodyType.AssignableTo(field.Type): fieldV.Set(reflect.ValueOf(body)) default: diags = append(diags, decodeBodyToValue(body, ctx, fieldV)...) } } for _, fieldIdx := range tags.Nested { field := val.Type().Field(fieldIdx) fieldV := val.Field(fieldIdx) ty := field.Type isPtr := false if ty.Kind() == reflect.Ptr { isPtr = true ty = ty.Elem() } switch { case bodyType.AssignableTo(field.Type): fieldV.Set(reflect.ValueOf(leftovers)) case attrsType.AssignableTo(field.Type): attrs, attrsDiags := leftovers.JustAttributes() if len(attrsDiags) > 0 { diags = append(diags, attrsDiags...) } fieldV.Set(reflect.ValueOf(attrs)) default: v := fieldV if isPtr { if v.IsNil() { v = reflect.New(ty) } v = v.Elem() } diags = append(diags, decodeBodyToValue(leftovers, ctx, v)...) fieldV.Set(v) } } if tags.Remain != nil { fieldIdx := *tags.Remain field := val.Type().Field(fieldIdx) fieldV := val.Field(fieldIdx) switch { case bodyType.AssignableTo(field.Type): fieldV.Set(reflect.ValueOf(leftovers)) case attrsType.AssignableTo(field.Type): attrs, attrsDiags := leftovers.JustAttributes() if len(attrsDiags) > 0 { diags = append(diags, attrsDiags...) } fieldV.Set(reflect.ValueOf(attrs)) default: diags = append(diags, decodeBodyToValue(leftovers, ctx, fieldV)...) } } for name, fieldIdx := range tags.Attributes { attr := content.Attributes[name] field := val.Type().Field(fieldIdx) fieldV := val.Field(fieldIdx) if attr == nil { if !exprType.AssignableTo(field.Type) { continue } // As a special case, if the target is of type hcl.Expression then // we'll assign an actual expression that evalues to a cty null, // so the caller can deal with it within the cty realm rather // than within the Go realm. synthExpr := hcl.StaticExpr(cty.NullVal(cty.DynamicPseudoType), body.MissingItemRange()) fieldV.Set(reflect.ValueOf(synthExpr)) continue } switch { case attrType.AssignableTo(field.Type): fieldV.Set(reflect.ValueOf(attr)) case exprType.AssignableTo(field.Type): fieldV.Set(reflect.ValueOf(attr.Expr)) default: diags = append(diags, DecodeExpression( attr.Expr, ctx, fieldV.Addr().Interface(), )...) } } blocksByType := content.Blocks.ByType() for typeName, fieldIdx := range tags.Blocks { blocks := blocksByType[typeName] field := val.Type().Field(fieldIdx) ty := field.Type isSlice := false isPtr := false if ty.Kind() == reflect.Slice { isSlice = true ty = ty.Elem() } if ty.Kind() == reflect.Ptr { isPtr = true ty = ty.Elem() } if len(blocks) > 1 && !isSlice { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Duplicate %s block", typeName), Detail: fmt.Sprintf( "Only one %s block is allowed. Another was defined at %s.", typeName, blocks[0].DefRange.String(), ), Subject: &blocks[1].DefRange, }) continue } if len(blocks) == 0 { if isSlice || isPtr { if val.Field(fieldIdx).IsNil() { val.Field(fieldIdx).Set(reflect.Zero(field.Type)) } } else { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("Missing %s block", typeName), Detail: fmt.Sprintf("A %s block is required.", typeName), Subject: body.MissingItemRange().Ptr(), }) } continue } switch { case isSlice: elemType := ty if isPtr { elemType = reflect.PtrTo(ty) } sli := val.Field(fieldIdx) if sli.IsNil() { sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks)) } for i, block := range blocks { if isPtr { if i >= sli.Len() { sli = reflect.Append(sli, reflect.New(ty)) } v := sli.Index(i) if v.IsNil() { v = reflect.New(ty) } diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) sli.Index(i).Set(v) } else { if i >= sli.Len() { sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty))) } diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...) } } if sli.Len() > len(blocks) { sli.SetLen(len(blocks)) } val.Field(fieldIdx).Set(sli) default: block := blocks[0] if isPtr { v := val.Field(fieldIdx) if v.IsNil() { v = reflect.New(ty) } diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...) val.Field(fieldIdx).Set(v) } else { diags = append(diags, decodeBlockToValue(block, ctx, val.Field(fieldIdx))...) } } } return diags } func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { attrs, diags := body.JustAttributes() if attrs == nil { return diags } mv := reflect.MakeMap(v.Type()) for k, attr := range attrs { switch { case attrType.AssignableTo(v.Type().Elem()): mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr)) case exprType.AssignableTo(v.Type().Elem()): mv.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(attr.Expr)) default: ev := reflect.New(v.Type().Elem()) diags = append(diags, DecodeExpression(attr.Expr, ctx, ev.Interface())...) mv.SetMapIndex(reflect.ValueOf(k), ev.Elem()) } } v.Set(mv) return diags } func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics { var diags hcl.Diagnostics ty := v.Type() switch { case blockType.AssignableTo(ty): v.Elem().Set(reflect.ValueOf(block)) case bodyType.AssignableTo(ty): v.Elem().Set(reflect.ValueOf(block.Body)) case attrsType.AssignableTo(ty): attrs, attrsDiags := block.Body.JustAttributes() if len(attrsDiags) > 0 { diags = append(diags, attrsDiags...) } v.Elem().Set(reflect.ValueOf(attrs)) default: diags = append(diags, decodeBodyToValue(block.Body, ctx, v)...) if len(block.Labels) > 0 { blockTags := getFieldTags(ty) for li, lv := range block.Labels { lfieldIdx := blockTags.Labels[li].FieldIndex v.Field(lfieldIdx).Set(reflect.ValueOf(lv)) } } } return diags } // DecodeExpression extracts the value of the given expression into the given // value. This value must be something that gocty is able to decode into, // since the final decoding is delegated to that package. // // The given EvalContext is used to resolve any variables or functions in // expressions encountered while decoding. This may be nil to require only // constant values, for simple applications that do not support variables or // functions. // // The returned diagnostics should be inspected with its HasErrors method to // determine if the populated value is valid and complete. If error diagnostics // are returned then the given value may have been partially-populated but // may still be accessed by a careful caller for static analysis and editor // integration use-cases. func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics { srcVal, diags := expr.Value(ctx) convTy, err := gocty.ImpliedType(val) if err != nil { panic(fmt.Sprintf("unsuitable DecodeExpression target: %s", err)) } srcVal, err = convert.Convert(srcVal, convTy) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unsuitable value type", Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()), Subject: expr.StartRange().Ptr(), Context: expr.Range().Ptr(), }) return diags } err = gocty.FromCtyValue(srcVal, val) if err != nil { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Unsuitable value type", Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()), Subject: expr.StartRange().Ptr(), Context: expr.Range().Ptr(), }) } return diags }