2017-09-11 23:00:31 +00:00
|
|
|
package gohcl
|
2017-05-21 00:30:02 +00:00
|
|
|
|
|
|
|
import (
|
2017-05-21 15:47:05 +00:00
|
|
|
"fmt"
|
2017-05-21 19:44:07 +00:00
|
|
|
"reflect"
|
2017-05-21 15:47:05 +00:00
|
|
|
|
2017-09-20 23:23:23 +00:00
|
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
|
2019-09-09 23:08:19 +00:00
|
|
|
"github.com/hashicorp/hcl/v2"
|
2017-05-28 00:35:44 +00:00
|
|
|
"github.com/zclconf/go-cty/cty/convert"
|
|
|
|
"github.com/zclconf/go-cty/cty/gocty"
|
2017-05-21 00:30:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// 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.
|
2017-09-11 23:40:37 +00:00
|
|
|
func DecodeBody(body hcl.Body, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
2017-05-21 19:44:07 +00:00
|
|
|
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())
|
|
|
|
}
|
|
|
|
|
2017-09-11 23:40:37 +00:00
|
|
|
func decodeBodyToValue(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
|
2017-05-21 19:44:07 +00:00
|
|
|
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()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-11 23:40:37 +00:00
|
|
|
func decodeBodyToStruct(body hcl.Body, ctx *hcl.EvalContext, val reflect.Value) hcl.Diagnostics {
|
2017-05-21 19:44:07 +00:00
|
|
|
schema, partial := ImpliedBodySchema(val.Interface())
|
|
|
|
|
2017-09-11 23:40:37 +00:00
|
|
|
var content *hcl.BodyContent
|
|
|
|
var leftovers hcl.Body
|
|
|
|
var diags hcl.Diagnostics
|
2017-05-21 19:44:07 +00:00
|
|
|
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.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)...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-20 23:23:23 +00:00
|
|
|
for name, fieldIdx := range tags.Attributes {
|
|
|
|
attr := content.Attributes[name]
|
2017-05-21 19:44:07 +00:00
|
|
|
field := val.Type().Field(fieldIdx)
|
|
|
|
fieldV := val.Field(fieldIdx)
|
|
|
|
|
2017-09-20 23:23:23 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-05-21 19:44:07 +00:00
|
|
|
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 {
|
2017-09-11 23:40:37 +00:00
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
2017-05-21 19:44:07 +00:00
|
|
|
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 {
|
gohcl: retain nested blocks while decoding
Currently, if nonzero struct is passed to the DecodeBody function,
decoding process will keep already initialized top-level fields values or
overwrite them, if they are specified in HCL. This behaviour is useful,
as it allows to have some default values for top-level fields.
However, if the field is a type block or slice (multiple blocks), then the
entire block is overwritten, which erases the existing values. Because of
that, setting default values in nested structs is not possible.
With this commit, decode functions will check if the value is
nil and only then set them to empty struct, which allows for appending
to existing structs.
In case of a slice, either new empty element will be added, or existing
element will be used for setting new value (so values will be merged).
Also, to keep the same behavior as json.Unmarshal, if retained list
have more elements than new list, additional elements will be removed
and existing elements will be merged. This allows to have default values
also for positional elements.
Behavior added by this patch is the same as in json.Unmarshal and
yaml.Unmarshal, which both retain nested structs during unmarshaling
process, so I believe this is an expected behavior from user
perspective.
2019-10-10 16:15:24 +00:00
|
|
|
if val.Field(fieldIdx).IsNil() {
|
|
|
|
val.Field(fieldIdx).Set(reflect.Zero(field.Type))
|
|
|
|
}
|
2017-05-21 19:44:07 +00:00
|
|
|
} else {
|
2017-09-11 23:40:37 +00:00
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
2017-05-21 19:44:07 +00:00
|
|
|
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)
|
|
|
|
}
|
gohcl: retain nested blocks while decoding
Currently, if nonzero struct is passed to the DecodeBody function,
decoding process will keep already initialized top-level fields values or
overwrite them, if they are specified in HCL. This behaviour is useful,
as it allows to have some default values for top-level fields.
However, if the field is a type block or slice (multiple blocks), then the
entire block is overwritten, which erases the existing values. Because of
that, setting default values in nested structs is not possible.
With this commit, decode functions will check if the value is
nil and only then set them to empty struct, which allows for appending
to existing structs.
In case of a slice, either new empty element will be added, or existing
element will be used for setting new value (so values will be merged).
Also, to keep the same behavior as json.Unmarshal, if retained list
have more elements than new list, additional elements will be removed
and existing elements will be merged. This allows to have default values
also for positional elements.
Behavior added by this patch is the same as in json.Unmarshal and
yaml.Unmarshal, which both retain nested structs during unmarshaling
process, so I believe this is an expected behavior from user
perspective.
2019-10-10 16:15:24 +00:00
|
|
|
sli := val.Field(fieldIdx)
|
|
|
|
if sli.IsNil() {
|
|
|
|
sli = reflect.MakeSlice(reflect.SliceOf(elemType), len(blocks), len(blocks))
|
|
|
|
}
|
2017-05-21 19:44:07 +00:00
|
|
|
|
|
|
|
for i, block := range blocks {
|
|
|
|
if isPtr {
|
gohcl: retain nested blocks while decoding
Currently, if nonzero struct is passed to the DecodeBody function,
decoding process will keep already initialized top-level fields values or
overwrite them, if they are specified in HCL. This behaviour is useful,
as it allows to have some default values for top-level fields.
However, if the field is a type block or slice (multiple blocks), then the
entire block is overwritten, which erases the existing values. Because of
that, setting default values in nested structs is not possible.
With this commit, decode functions will check if the value is
nil and only then set them to empty struct, which allows for appending
to existing structs.
In case of a slice, either new empty element will be added, or existing
element will be used for setting new value (so values will be merged).
Also, to keep the same behavior as json.Unmarshal, if retained list
have more elements than new list, additional elements will be removed
and existing elements will be merged. This allows to have default values
also for positional elements.
Behavior added by this patch is the same as in json.Unmarshal and
yaml.Unmarshal, which both retain nested structs during unmarshaling
process, so I believe this is an expected behavior from user
perspective.
2019-10-10 16:15:24 +00:00
|
|
|
if i >= sli.Len() {
|
|
|
|
sli = reflect.Append(sli, reflect.New(ty))
|
|
|
|
}
|
|
|
|
v := sli.Index(i)
|
|
|
|
if v.IsNil() {
|
|
|
|
v = reflect.New(ty)
|
|
|
|
}
|
2017-05-21 19:44:07 +00:00
|
|
|
diags = append(diags, decodeBlockToValue(block, ctx, v.Elem())...)
|
|
|
|
sli.Index(i).Set(v)
|
|
|
|
} else {
|
2020-01-15 12:30:47 +00:00
|
|
|
if i >= sli.Len() {
|
|
|
|
sli = reflect.Append(sli, reflect.Indirect(reflect.New(ty)))
|
|
|
|
}
|
2017-05-21 19:44:07 +00:00
|
|
|
diags = append(diags, decodeBlockToValue(block, ctx, sli.Index(i))...)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
gohcl: retain nested blocks while decoding
Currently, if nonzero struct is passed to the DecodeBody function,
decoding process will keep already initialized top-level fields values or
overwrite them, if they are specified in HCL. This behaviour is useful,
as it allows to have some default values for top-level fields.
However, if the field is a type block or slice (multiple blocks), then the
entire block is overwritten, which erases the existing values. Because of
that, setting default values in nested structs is not possible.
With this commit, decode functions will check if the value is
nil and only then set them to empty struct, which allows for appending
to existing structs.
In case of a slice, either new empty element will be added, or existing
element will be used for setting new value (so values will be merged).
Also, to keep the same behavior as json.Unmarshal, if retained list
have more elements than new list, additional elements will be removed
and existing elements will be merged. This allows to have default values
also for positional elements.
Behavior added by this patch is the same as in json.Unmarshal and
yaml.Unmarshal, which both retain nested structs during unmarshaling
process, so I believe this is an expected behavior from user
perspective.
2019-10-10 16:15:24 +00:00
|
|
|
if sli.Len() > len(blocks) {
|
|
|
|
sli.SetLen(len(blocks))
|
|
|
|
}
|
|
|
|
|
2017-05-21 19:44:07 +00:00
|
|
|
val.Field(fieldIdx).Set(sli)
|
|
|
|
|
|
|
|
default:
|
|
|
|
block := blocks[0]
|
|
|
|
if isPtr {
|
gohcl: retain nested blocks while decoding
Currently, if nonzero struct is passed to the DecodeBody function,
decoding process will keep already initialized top-level fields values or
overwrite them, if they are specified in HCL. This behaviour is useful,
as it allows to have some default values for top-level fields.
However, if the field is a type block or slice (multiple blocks), then the
entire block is overwritten, which erases the existing values. Because of
that, setting default values in nested structs is not possible.
With this commit, decode functions will check if the value is
nil and only then set them to empty struct, which allows for appending
to existing structs.
In case of a slice, either new empty element will be added, or existing
element will be used for setting new value (so values will be merged).
Also, to keep the same behavior as json.Unmarshal, if retained list
have more elements than new list, additional elements will be removed
and existing elements will be merged. This allows to have default values
also for positional elements.
Behavior added by this patch is the same as in json.Unmarshal and
yaml.Unmarshal, which both retain nested structs during unmarshaling
process, so I believe this is an expected behavior from user
perspective.
2019-10-10 16:15:24 +00:00
|
|
|
v := val.Field(fieldIdx)
|
|
|
|
if v.IsNil() {
|
|
|
|
v = reflect.New(ty)
|
|
|
|
}
|
2017-05-21 19:44:07 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-09-11 23:40:37 +00:00
|
|
|
func decodeBodyToMap(body hcl.Body, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
|
2017-05-21 19:44:07 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2017-09-11 23:40:37 +00:00
|
|
|
func decodeBlockToValue(block *hcl.Block, ctx *hcl.EvalContext, v reflect.Value) hcl.Diagnostics {
|
|
|
|
var diags hcl.Diagnostics
|
2017-05-21 19:44:07 +00:00
|
|
|
|
|
|
|
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
|
2017-05-21 00:30:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2017-09-11 23:40:37 +00:00
|
|
|
func DecodeExpression(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
|
2017-05-21 15:47:05 +00:00
|
|
|
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 {
|
2017-09-11 23:40:37 +00:00
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
2017-05-21 15:47:05 +00:00
|
|
|
Summary: "Unsuitable value type",
|
2017-07-26 01:24:55 +00:00
|
|
|
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
|
2017-05-21 15:47:05 +00:00
|
|
|
Subject: expr.StartRange().Ptr(),
|
|
|
|
Context: expr.Range().Ptr(),
|
|
|
|
})
|
|
|
|
return diags
|
|
|
|
}
|
|
|
|
|
|
|
|
err = gocty.FromCtyValue(srcVal, val)
|
|
|
|
if err != nil {
|
2017-09-11 23:40:37 +00:00
|
|
|
diags = append(diags, &hcl.Diagnostic{
|
|
|
|
Severity: hcl.DiagError,
|
2017-05-21 15:47:05 +00:00
|
|
|
Summary: "Unsuitable value type",
|
2017-07-26 01:24:55 +00:00
|
|
|
Detail: fmt.Sprintf("Unsuitable value: %s", err.Error()),
|
2017-05-21 15:47:05 +00:00
|
|
|
Subject: expr.StartRange().Ptr(),
|
|
|
|
Context: expr.Range().Ptr(),
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return diags
|
2017-05-21 00:30:02 +00:00
|
|
|
}
|