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:
Joe Groocock
2021-02-03 18:59:27 +00:00
parent c950568f1b
commit 52897cc16c
5 changed files with 580 additions and 0 deletions

View File

@@ -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
View 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
View 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)
}
}
}

View File

@@ -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

View File

@@ -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")
}
}