lang: Implement Into() to set types.Value from reflect.Value
Into() mutates a given reflect.Value and sets the data represented by the types.Value into the storage represented by the reflect.Value. Into() is the opposite to ValueOf() which converts reflect.Value into types.Value, and in theory they should be (almost) bijective with some edge case exceptions where the conversion is lossy. Simply, it replaces reflect.Value.Set() in the broad case, giving finer control of how the reflect.Value is modified and how the data is set. types.Value.Value() is now also a redundant function that achieves the same outcome as Into(), but with less type specificity. Signed-off-by: Joe Groocock <me@frebib.net>
This commit is contained in:
@@ -1474,4 +1474,5 @@ func TestComplexCmp0(t *testing.T) {
|
||||
|
||||
func TestTypeOf0(t *testing.T) {
|
||||
// TODO: implement testing of the TypeOf function
|
||||
// TODO: implement testing TypeOf for struct field name mappings
|
||||
}
|
||||
|
||||
64
lang/types/util.go
Normal file
64
lang/types/util.go
Normal file
@@ -0,0 +1,64 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// nextPowerOfTwo gets the lowest number higher than v that is a power of two.
|
||||
func nextPowerOfTwo(v uint32) uint32 {
|
||||
v--
|
||||
v |= v >> 1
|
||||
v |= v >> 2
|
||||
v |= v >> 4
|
||||
v |= v >> 8
|
||||
v |= v >> 16
|
||||
v++
|
||||
return v
|
||||
}
|
||||
|
||||
// TypeStructTagToFieldName returns a mapping from recommended alias to actual
|
||||
// field name. It returns an error if it finds a collision. It uses the `lang`
|
||||
// tags. It must be passed a reflect.Type representation of a struct or it will
|
||||
// error.
|
||||
// TODO: This is a copy of engineUtil.StructTagToFieldName taking a reflect.Type
|
||||
func TypeStructTagToFieldName(st reflect.Type) (map[string]string, error) {
|
||||
if k := st.Kind(); k != reflect.Struct { // this should be a struct now
|
||||
return nil, fmt.Errorf("input doesn't point to a struct, got: %+v", k)
|
||||
}
|
||||
|
||||
result := make(map[string]string) // `lang` field tag -> field name
|
||||
|
||||
for i := 0; i < st.NumField(); i++ {
|
||||
field := st.Field(i)
|
||||
name := field.Name
|
||||
// if !ok, then nothing is found
|
||||
if alias, ok := field.Tag.Lookup(StructTag); ok { // golang 1.7+
|
||||
if val, exists := result[alias]; exists {
|
||||
return nil, fmt.Errorf("field `%s` uses the same key `%s` as field `%s`", name, alias, val)
|
||||
}
|
||||
// empty string ("") is a valid value
|
||||
if alias != "" {
|
||||
result[alias] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
35
lang/types/util_test.go
Normal file
35
lang/types/util_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2021+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package types
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNextPowerOfTwo(t *testing.T) {
|
||||
testCases := map[uint32]uint32{
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 4,
|
||||
5: 8,
|
||||
}
|
||||
|
||||
for v, exp := range testCases {
|
||||
if pow := nextPowerOfTwo(v); pow != exp {
|
||||
t.Errorf("function NextPowerOfTwo of `%d` did not match expected: `%d`", pow, exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,6 +203,201 @@ func ValueOf(v reflect.Value) (Value, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Into mutates the given reflect.Value with the data represented by the Value.
|
||||
// In almost every case, it is likely that the reflect.Value will be modified,
|
||||
// instantiating nil pointers and even potentially partially filling data before
|
||||
// returning an error. It should be assumed that if this returns an error, the
|
||||
// reflect.Value passed in has been trashed and should be discarded before
|
||||
// reuse.
|
||||
func Into(v Value, rv reflect.Value) error {
|
||||
typ := rv.Type()
|
||||
kind := typ.Kind()
|
||||
for kind == reflect.Ptr {
|
||||
typ = typ.Elem() // un-nest one pointer
|
||||
kind = typ.Kind()
|
||||
|
||||
// if pointer was nil, instantiate the destination type and point
|
||||
// at it to prevent nil pointer dereference when setting values
|
||||
if rv.IsNil() {
|
||||
rv.Set(reflect.New(typ))
|
||||
}
|
||||
rv = rv.Elem() // un-nest rv from pointer
|
||||
}
|
||||
|
||||
// capture rv and v in a closure that is static for the scope of this Into() call
|
||||
// mustInto ensures rv is in a list of compatible types before attempting to reflect it
|
||||
mustInto := func(kinds ...reflect.Kind) error {
|
||||
// sigh. Go can be so elegant, and then it makes you do this
|
||||
for _, n := range kinds {
|
||||
if kind == n {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// No matching kind found, must be an incompatible conversion
|
||||
return fmt.Errorf("cannot Into() %+v of type %s into %s", v, v.Type(), typ)
|
||||
}
|
||||
|
||||
switch v := v.(type) {
|
||||
case *BoolValue:
|
||||
if err := mustInto(reflect.Bool); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rv.SetBool(v.V)
|
||||
return nil
|
||||
|
||||
case *StrValue:
|
||||
if err := mustInto(reflect.String); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rv.SetString(v.V)
|
||||
return nil
|
||||
|
||||
case *IntValue:
|
||||
// overflow check
|
||||
switch kind { // match on destination field kind
|
||||
case reflect.Int, reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8:
|
||||
ff := reflect.Zero(typ) // test on a non-ptr equivalent
|
||||
if ff.OverflowInt(v.V) { // this is valid!
|
||||
return fmt.Errorf("%+v is an `%s`, and rv `%d` will overflow it", rv.Interface(), rv.Kind(), v.V)
|
||||
}
|
||||
rv.SetInt(v.V)
|
||||
return nil
|
||||
|
||||
case reflect.Uint, reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8:
|
||||
ff := reflect.Zero(typ)
|
||||
if ff.OverflowUint(uint64(v.V)) { // TODO: is this correct?
|
||||
return fmt.Errorf("%+v is an `%s`, and rv `%d` will overflow it", rv.Interface(), rv.Kind(), v.V)
|
||||
}
|
||||
rv.SetUint(uint64(v.V))
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("cannot Into() %+v of type %s into %s", v, v.Type(), typ)
|
||||
}
|
||||
|
||||
case *FloatValue:
|
||||
if err := mustInto(reflect.Float32, reflect.Float64); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ff := reflect.Zero(typ)
|
||||
if ff.OverflowFloat(v.V) {
|
||||
return fmt.Errorf("%+v is an `%s`, and value `%f` will overflow it", rv.Interface(), rv.Kind(), v.V)
|
||||
}
|
||||
rv.SetFloat(v.V)
|
||||
return nil
|
||||
|
||||
case *ListValue:
|
||||
if err := mustInto(reflect.Slice, reflect.Array); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count := len(v.V)
|
||||
if count > rv.Cap() {
|
||||
pow := nextPowerOfTwo(uint32(count))
|
||||
nval := reflect.MakeSlice(rv.Type(), count, int(pow))
|
||||
rv.Set(nval)
|
||||
}
|
||||
for i, x := range v.V {
|
||||
f := rv.Index(i)
|
||||
el := reflect.New(f.Type()).Elem()
|
||||
if err := Into(x, el); err != nil { // recurse
|
||||
return err
|
||||
}
|
||||
f.Set(el)
|
||||
}
|
||||
return nil
|
||||
|
||||
case *MapValue:
|
||||
if err := mustInto(reflect.Map); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rv.IsNil() {
|
||||
rv.Set(reflect.MakeMapWithSize(typ, len(v.V)))
|
||||
}
|
||||
|
||||
// convert both key and value, then set them in the map
|
||||
for mk, mv := range v.V {
|
||||
key := reflect.New(typ.Key()).Elem()
|
||||
if err := Into(mk, key); err != nil { // recurse
|
||||
return err
|
||||
}
|
||||
val := reflect.New(typ.Elem()).Elem()
|
||||
if err := Into(mv, val); err != nil { // recurse
|
||||
return err
|
||||
}
|
||||
rv.SetMapIndex(key, val)
|
||||
}
|
||||
return nil
|
||||
|
||||
case *StructValue:
|
||||
if err := mustInto(reflect.Struct); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Into sets the value of the given reflect.Value to the value of this obj
|
||||
mapping, err := TypeStructTagToFieldName(typ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for k := range v.T.Map {
|
||||
mk := k
|
||||
// map mcl field name -> go field name based on `lang:""` tag
|
||||
if key, exists := mapping[k]; exists {
|
||||
mk = key
|
||||
}
|
||||
field := rv.FieldByName(mk)
|
||||
if err := Into(v.V[k], field); err != nil { // recurse
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
case *FuncValue:
|
||||
if err := mustInto(reflect.Func); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// wrap our function with the translation that is necessary
|
||||
fn := func(args []reflect.Value) (results []reflect.Value) { // build
|
||||
innerArgs := []Value{}
|
||||
for _, x := range args {
|
||||
v, err := ValueOf(x) // reflect.Value -> Value
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("can't determine value of %+v", x))
|
||||
}
|
||||
innerArgs = append(innerArgs, v)
|
||||
}
|
||||
result, err := v.V(innerArgs) // call it
|
||||
if err != nil {
|
||||
// when calling our function with the Call method, then
|
||||
// we get the error output and have a chance to decide
|
||||
// what to do with it, but when calling it from within
|
||||
// a normal golang function call, the error represents
|
||||
// that something went horribly wrong, aka a panic...
|
||||
panic(fmt.Errorf("function panic: %+v", err))
|
||||
}
|
||||
out := reflect.New(rv.Type().Out(0))
|
||||
// convert the lang result back to a Go value
|
||||
if err := Into(result, out); err != nil {
|
||||
panic(fmt.Errorf("function return conversion panic: %+v", err))
|
||||
}
|
||||
return []reflect.Value{out} // only one result
|
||||
}
|
||||
rv.Set(reflect.MakeFunc(rv.Type(), fn))
|
||||
return nil
|
||||
|
||||
case *VariantValue:
|
||||
return Into(v.V, rv)
|
||||
|
||||
default:
|
||||
return fmt.Errorf("cannot Into() %+v of type %s into %s", v, v.Type(), typ)
|
||||
}
|
||||
}
|
||||
|
||||
// ValueSlice is a linear list of values. It is used for sorting purposes.
|
||||
type ValueSlice []Value
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
@@ -639,3 +640,287 @@ func TestValueOf0(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValueInto0(t *testing.T) {
|
||||
// converts a Go variable to a types.Value, or panics if any error
|
||||
mustValue := func(v interface{}) Value {
|
||||
val, err := ValueOfGolang(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return val
|
||||
}
|
||||
// reflect variant of & on a variable. Creates a pointer in
|
||||
// memory, sets the destination, then returns the pointer
|
||||
ptrto := func(v interface{}) interface{} {
|
||||
p := reflect.New(reflect.TypeOf(v))
|
||||
p.Elem().Set(reflect.ValueOf(v))
|
||||
return p.Interface()
|
||||
}
|
||||
ptrstr := func(s string) *string {
|
||||
return ptrto(s).(*string)
|
||||
}
|
||||
|
||||
// various container variables for below tests
|
||||
var b bool
|
||||
var s string
|
||||
|
||||
var i int64
|
||||
var u uint64
|
||||
var i8 int8
|
||||
var u8 uint8
|
||||
|
||||
var f float64
|
||||
|
||||
var l []string
|
||||
var ll [][]string
|
||||
var lptrlptr []*[]*string
|
||||
var arr [10]string
|
||||
|
||||
var m map[string]int
|
||||
|
||||
type str1 struct {
|
||||
X string
|
||||
Y int
|
||||
}
|
||||
var ms map[string]str1
|
||||
|
||||
var mptr map[string]*string
|
||||
var msptr map[string]*str1
|
||||
|
||||
type str2 struct {
|
||||
X *string
|
||||
Y *int
|
||||
}
|
||||
var mptrsptr map[string]*str2
|
||||
|
||||
var testCases = []struct {
|
||||
// backing container to call Into() on
|
||||
container interface{}
|
||||
// lang value to be Into()ed
|
||||
value Value
|
||||
// test comparison data to ensure the Into() worked
|
||||
compare interface{}
|
||||
// shouldErr set to true if an err is expected
|
||||
shouldErr bool
|
||||
// shouldPanic set to true if a panic is expected
|
||||
shouldPanic bool
|
||||
}{
|
||||
{
|
||||
container: &b,
|
||||
value: mustValue(true),
|
||||
compare: true,
|
||||
},
|
||||
{
|
||||
container: &s,
|
||||
value: mustValue("testing"),
|
||||
compare: "testing",
|
||||
},
|
||||
{
|
||||
container: &i,
|
||||
value: mustValue(int64(-12345)),
|
||||
compare: int64(-12345),
|
||||
},
|
||||
{
|
||||
container: &u,
|
||||
value: mustValue(uint64(math.MaxUint64)),
|
||||
compare: uint64(math.MaxUint64),
|
||||
},
|
||||
{ // ensure -1 from an int64 fits into an int8
|
||||
container: &i8,
|
||||
value: mustValue(int64(-1)),
|
||||
compare: int8(-1),
|
||||
},
|
||||
{ // ensure valid uint8 from an int64 fits into an uint8
|
||||
container: &u8,
|
||||
value: mustValue(int64(200)),
|
||||
compare: uint8(200),
|
||||
},
|
||||
{ // this test case proves overflows work
|
||||
container: &u8,
|
||||
value: mustValue(int64(256)),
|
||||
shouldErr: true,
|
||||
},
|
||||
{ // it would be good to put float32 -> float64 here but precision says no
|
||||
container: &f,
|
||||
value: mustValue(float64(1.23)),
|
||||
compare: float64(1.23),
|
||||
},
|
||||
{
|
||||
container: &l,
|
||||
value: mustValue([]string{"1", "2", "3"}),
|
||||
compare: []string{"1", "2", "3"},
|
||||
},
|
||||
{ // arrays are pretty much the same as slices
|
||||
container: &arr,
|
||||
value: mustValue([]string{"1", "2", "3"}),
|
||||
compare: [10]string{"1", "2", "3"},
|
||||
},
|
||||
{
|
||||
container: &ll,
|
||||
value: mustValue([][]string{{"1"}, {"2"}}),
|
||||
compare: [][]string{{"1"}, {"2"}},
|
||||
},
|
||||
{
|
||||
container: &m,
|
||||
value: mustValue(map[string]int{"1": 1, "2": 2}),
|
||||
compare: map[string]int{"1": 1, "2": 2},
|
||||
},
|
||||
{
|
||||
container: &ms,
|
||||
value: mustValue(map[string]str1{"a": {"a", 97}, "b": {"b", 98}}),
|
||||
compare: map[string]str1{"a": {"a", 97}, "b": {"b", 98}},
|
||||
},
|
||||
|
||||
// Various error sanity tests. All of these tests should return type errors.
|
||||
{ // int into string
|
||||
container: &s,
|
||||
value: mustValue(12345),
|
||||
shouldErr: true,
|
||||
},
|
||||
{ // string into int
|
||||
container: &i,
|
||||
value: mustValue("hello"),
|
||||
shouldErr: true,
|
||||
},
|
||||
{ // map[int]int into map[string]int
|
||||
container: &m,
|
||||
value: mustValue(map[int]int{1: 2, 3: 4}),
|
||||
shouldErr: true,
|
||||
},
|
||||
|
||||
// Pointer and pointer-to-pointer tests
|
||||
{
|
||||
container: ptrto(&s),
|
||||
value: mustValue("pointer to a string"),
|
||||
compare: ptrto("pointer to a string"),
|
||||
},
|
||||
{
|
||||
container: ptrto(ptrto(&s)),
|
||||
value: mustValue("pointer to a pointer to a string"),
|
||||
compare: ptrto(ptrto("pointer to a pointer to a string")),
|
||||
},
|
||||
{ // tests Into() instantiating the nil pointers in the map values before following/setting the values
|
||||
container: &mptr,
|
||||
value: mustValue(map[string]string{
|
||||
"first": "firstptr",
|
||||
"second": "secondptr",
|
||||
}),
|
||||
compare: map[string]*string{
|
||||
"first": ptrstr("firstptr"),
|
||||
"second": ptrstr("secondptr"),
|
||||
},
|
||||
},
|
||||
{
|
||||
container: &lptrlptr,
|
||||
value: mustValue([][]string{
|
||||
{"hello", "world"},
|
||||
{"hola", "món"},
|
||||
}),
|
||||
// List of pointers to lists of pointers to strings. Confused yet?
|
||||
compare: []*[]*string{
|
||||
{ptrstr("hello"), ptrstr("world")},
|
||||
{ptrstr("hola"), ptrstr("món")},
|
||||
},
|
||||
},
|
||||
{
|
||||
container: &msptr,
|
||||
value: mustValue(map[string]str1{
|
||||
"str1": {
|
||||
X: "str",
|
||||
Y: 98765,
|
||||
},
|
||||
}),
|
||||
compare: map[string]*str1{
|
||||
"str1": {
|
||||
X: "str",
|
||||
Y: 98765,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ // Use str1 to try to Into() str2. They're field-compatible, except str2 uses pointers to values instead.
|
||||
container: &mptrsptr,
|
||||
value: mustValue(map[string]str1{
|
||||
"str2": {
|
||||
X: "string pointer",
|
||||
// cannot omit any fields, as doing so makes the comparison
|
||||
// fail because nil.(*int) != 0. Using .String() or coercing
|
||||
// both values to the same type for comparison might work
|
||||
Y: 555,
|
||||
},
|
||||
}),
|
||||
compare: map[string]*str2{
|
||||
"str2": {
|
||||
X: ptrstr("string pointer"),
|
||||
Y: ptrto(555).(*int),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for index, tc := range testCases {
|
||||
name := fmt.Sprintf("test Into() %s #%d", reflect.TypeOf(tc.container).Elem(), index)
|
||||
// https://github.com/purpleidea/mgmt/pull/629/files#r568305689
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
rvo := reflect.ValueOf(tc.container)
|
||||
|
||||
if tc.shouldPanic {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("function Into() didn't panic but it was expected")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Into() the value into the reflect.Value
|
||||
err := Into(tc.value, rvo)
|
||||
|
||||
// check for non/expected errors
|
||||
if !tc.shouldErr && err != nil {
|
||||
t.Errorf("function Into() returned an error: %s", err)
|
||||
return
|
||||
} else if tc.shouldErr && err == nil {
|
||||
t.Errorf("function Into() didn't return an error but one was expected")
|
||||
return
|
||||
}
|
||||
|
||||
if tc.shouldErr || tc.shouldPanic {
|
||||
// err/panic was expected. no comparison to do here
|
||||
return
|
||||
}
|
||||
|
||||
// follow the container pointer: (*tc.container).(interface{})
|
||||
container := rvo.Elem().Interface()
|
||||
// ensure they're identical
|
||||
if !reflect.DeepEqual(container, tc.compare) {
|
||||
t.Errorf("result %s %+v doesn't match expected %s %+v",
|
||||
rvo.Elem().Type(), container, reflect.TypeOf(tc.compare), tc.compare,
|
||||
)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestValueIntoStructNameMapping(t *testing.T) {
|
||||
st := NewStruct(NewType("struct{word str; magic int}"))
|
||||
if err := st.Set("word", &StrValue{V: "zing"}); err != nil {
|
||||
t.Errorf("struct could not set key, error: %v", err)
|
||||
}
|
||||
if err := st.Set("magic", &IntValue{V: 0x5F3759DF}); err != nil {
|
||||
t.Errorf("struct could not set key, error: %v", err)
|
||||
}
|
||||
|
||||
var compare struct {
|
||||
Word string `lang:"word"`
|
||||
Magic int `lang:"magic"`
|
||||
}
|
||||
err := Into(st, reflect.ValueOf(&compare))
|
||||
if err != nil {
|
||||
t.Errorf("function Into() returned an error: %s", err)
|
||||
}
|
||||
|
||||
if compare.Word != "zing" || compare.Magic != 0x5F3759DF {
|
||||
t.Errorf("struct field value is missing or incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user