icingadb/pkg/structify/structify.go
2022-06-22 15:27:00 +02:00

179 lines
4.5 KiB
Go

package structify
import (
"encoding"
"fmt"
"github.com/icinga/icingadb/pkg/contracts"
"github.com/pkg/errors"
"golang.org/x/exp/constraints"
"reflect"
"strconv"
"strings"
"unsafe"
)
// structBranch represents either a leaf or a subTree.
type structBranch struct {
// field specifies the struct field index.
field int
// leaf specifies the map key to parse the struct field from.
leaf string
// subTree specifies the struct field's inner tree.
subTree []structBranch
}
type MapStructifier = func(map[string]interface{}) (interface{}, error)
// MakeMapStructifier builds a function which parses a map's string values into a new struct of type t
// and returns a pointer to it. tag specifies which tag connects struct fields to map keys.
// MakeMapStructifier panics if it detects an unsupported type (suitable for usage in init() or global vars).
func MakeMapStructifier(t reflect.Type, tag string) MapStructifier {
tree := buildStructTree(t, tag)
return func(kv map[string]interface{}) (interface{}, error) {
vPtr := reflect.New(t)
ptr := vPtr.Interface()
if initer, ok := ptr.(contracts.Initer); ok {
initer.Init()
}
vPtrElem := vPtr.Elem()
err := errors.Wrapf(structifyMapByTree(kv, tree, vPtrElem, vPtrElem, new([]int)), "can't structify map %#v by tree %#v", kv, tree)
return ptr, err
}
}
// buildStructTree assembles a tree which represents the struct t based on tag.
func buildStructTree(t reflect.Type, tag string) []structBranch {
var tree []structBranch
numFields := t.NumField()
for i := 0; i < numFields; i++ {
if field := t.Field(i); field.PkgPath == "" {
switch tagValue := field.Tag.Get(tag); tagValue {
case "", "-":
case ",inline":
if subTree := buildStructTree(field.Type, tag); subTree != nil {
tree = append(tree, structBranch{i, "", subTree})
}
default:
// If parseString doesn't support *T, it'll panic.
_ = parseString("", reflect.New(field.Type).Interface())
tree = append(tree, structBranch{i, tagValue, nil})
}
}
}
return tree
}
// structifyMapByTree parses src's string values into the struct dest according to tree's specification.
func structifyMapByTree(src map[string]interface{}, tree []structBranch, dest, root reflect.Value, stack *[]int) error {
*stack = append(*stack, 0)
defer func() {
*stack = (*stack)[:len(*stack)-1]
}()
for _, branch := range tree {
(*stack)[len(*stack)-1] = branch.field
if branch.subTree == nil {
if v, ok := src[branch.leaf]; ok {
if vs, ok := v.(string); ok {
if err := parseString(vs, dest.Field(branch.field).Addr().Interface()); err != nil {
rt := root.Type()
typ := rt
var path []string
for _, i := range *stack {
f := typ.Field(i)
path = append(path, f.Name)
typ = f.Type
}
return errors.Wrapf(err, "can't parse %s into the %s %s#%s: %s",
branch.leaf, typ.Name(), rt.Name(), strings.Join(path, "."), vs)
}
}
}
} else if err := structifyMapByTree(src, branch.subTree, dest.Field(branch.field), root, stack); err != nil {
return err
}
}
return nil
}
// parseString parses src into *dest.
func parseString(src string, dest interface{}) error {
switch ptr := dest.(type) {
case encoding.TextUnmarshaler:
return ptr.UnmarshalText([]byte(src))
case *string:
*ptr = src
return nil
case **string:
*ptr = &src
return nil
case *uint8:
return parseUint(src, ptr)
case *uint16:
return parseUint(src, ptr)
case *uint32:
return parseUint(src, ptr)
case *uint64:
return parseUint(src, ptr)
case *int8:
return parseInt(src, ptr)
case *int16:
return parseInt(src, ptr)
case *int32:
return parseInt(src, ptr)
case *int64:
return parseInt(src, ptr)
case *float32:
return parseFloat(src, ptr)
case *float64:
return parseFloat(src, ptr)
default:
panic(fmt.Sprintf("unsupported type: %T", dest))
}
}
// parseUint parses src into *dest.
func parseUint[T constraints.Unsigned](src string, dest *T) error {
i, err := strconv.ParseUint(src, 10, bitSizeOf[T]())
if err == nil {
*dest = T(i)
}
return err
}
// parseInt parses src into *dest.
func parseInt[T constraints.Signed](src string, dest *T) error {
i, err := strconv.ParseInt(src, 10, bitSizeOf[T]())
if err == nil {
*dest = T(i)
}
return err
}
// parseFloat parses src into *dest.
func parseFloat[T constraints.Float](src string, dest *T) error {
f, err := strconv.ParseFloat(src, bitSizeOf[T]())
if err == nil {
*dest = T(f)
}
return err
}
func bitSizeOf[T any]() int {
var x T
return int(unsafe.Sizeof(x) * 8)
}