diff --git a/lang/types/type_test.go b/lang/types/type_test.go index 17dcdd67..c074a0bd 100644 --- a/lang/types/type_test.go +++ b/lang/types/type_test.go @@ -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 } diff --git a/lang/types/util.go b/lang/types/util.go new file mode 100644 index 00000000..7c986275 --- /dev/null +++ b/lang/types/util.go @@ -0,0 +1,64 @@ +// Mgmt +// Copyright (C) 2013-2021+ James Shubin and the project contributors +// Written by James Shubin 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 . + +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 +} diff --git a/lang/types/util_test.go b/lang/types/util_test.go new file mode 100644 index 00000000..828a82be --- /dev/null +++ b/lang/types/util_test.go @@ -0,0 +1,35 @@ +// Mgmt +// Copyright (C) 2013-2021+ James Shubin and the project contributors +// Written by James Shubin 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 . + +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) + } + } +} diff --git a/lang/types/value.go b/lang/types/value.go index a1e9ed4b..85b3f79f 100644 --- a/lang/types/value.go +++ b/lang/types/value.go @@ -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 diff --git a/lang/types/value_test.go b/lang/types/value_test.go index d39844c0..63041b5d 100644 --- a/lang/types/value_test.go +++ b/lang/types/value_test.go @@ -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") + } +}