buildr/internal/hcl/mapping.go
Peter e60726ef9e
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
feat: implement new and man for plugin modules
- use extracted shared libraries
2023-08-23 22:06:26 +02:00

168 lines
4 KiB
Go

package hcl
import (
"fmt"
"reflect"
"strings"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/zclconf/go-cty/cty"
)
type MappingCfg struct {
MapAsObject bool
StructAsMap bool
}
//nolint:gocyclo // expected to be complex as entrypoint
func MapToCtyVal(val any, cfg MappingCfg) (cty.Value, error) {
if val == nil {
return cty.Value{}, nil
}
var (
t reflect.Type
value reflect.Value
)
switch unwrapped := val.(type) {
case cty.Value:
return unwrapped, nil
case reflect.Value:
value = unwrapped
t = value.Type()
default:
t = reflect.TypeOf(val)
value = reflect.ValueOf(val)
}
if t.AssignableTo(reflect.TypeOf((*hclsyntax.Body)(nil))) {
return cty.EmptyObjectVal, nil
}
//nolint:exhaustive // other cases will be added if necessary
switch t.Kind() {
case reflect.Bool:
return cty.BoolVal(value.Bool()), nil
case reflect.String:
return cty.StringVal(value.String()), nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return cty.NumberIntVal(value.Int()), nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return cty.NumberUIntVal(value.Uint()), nil
case reflect.Float32, reflect.Float64:
return cty.NumberFloatVal(value.Float()), nil
case reflect.Pointer:
return MapToCtyVal(reflect.ValueOf(val).Elem().Interface(), cfg)
case reflect.Map:
return mapMap(t, value, cfg)
case reflect.Slice, reflect.Array:
length := value.Len()
if length == 0 {
return cty.ListValEmpty(mapType(t.Elem())), nil
}
out := make([]cty.Value, 0, length)
for i := 0; i < length; i++ {
if mapped, err := MapToCtyVal(value.Index(i).Interface(), cfg); err != nil {
return cty.Value{}, err
} else {
out = append(out, mapped)
}
}
return cty.ListVal(out), nil
case reflect.Struct:
return mapStruct(t, value, cfg)
default:
return cty.NilVal, fmt.Errorf("unmapped type %s", t.Kind())
}
}
func mapStruct(t reflect.Type, val reflect.Value, cfg MappingCfg) (cty.Value, error) {
numField := t.NumField()
out := make(map[string]cty.Value, numField)
for i := 0; i < numField; i++ {
field := t.Field(i)
if field.IsExported() && !isNil(val.Field(i)) {
if mapped, err := MapToCtyVal(val.Field(i).Interface(), cfg); err != nil {
return cty.Value{}, err
} else if field.Anonymous && mapped.CanIterateElements() {
for key, val := range mapped.AsValueMap() {
out[key] = val
}
} else {
keyName := strings.ToLower(field.Name)
if hclTag := field.Tag.Get("hcl"); hclTag != "" {
split := strings.Split(hclTag, ",")
if len(split) > 0 && split[0] != "" {
keyName = split[0]
}
}
out[keyName] = mapped
}
}
}
if cfg.StructAsMap {
return cty.MapVal(out), nil
}
return cty.ObjectVal(out), nil
}
func mapMap(t reflect.Type, val reflect.Value, cfg MappingCfg) (cty.Value, error) {
if val.Len() == 0 {
return cty.MapValEmpty(mapType(t.Elem())), nil
}
if t.Key().Kind() != reflect.String {
return cty.Value{}, fmt.Errorf("map key type %s is not supported", t.Key().Name())
}
out := make(map[string]cty.Value)
iter := val.MapRange()
for iter.Next() {
if mapped, err := MapToCtyVal(iter.Value().Interface(), cfg); err != nil {
return cty.Value{}, err
} else {
out[iter.Key().String()] = mapped
}
}
if cfg.MapAsObject {
return cty.ObjectVal(out), nil
}
return cty.MapVal(out), nil
}
func isNil(val reflect.Value) bool {
//nolint:exhaustive // other cases will be added if necessary
switch val.Kind() {
case reflect.Pointer, reflect.Interface, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice:
return val.IsNil()
default:
return false
}
}
func mapType(t reflect.Type) cty.Type {
//nolint:exhaustive // other cases will be added if necessary
switch t.Kind() {
case reflect.Bool:
return cty.Bool
case reflect.String:
return cty.String
case reflect.Float32, reflect.Float64,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return cty.Number
default:
return cty.Type{}
}
}