From 932f8c48ef290ef816b75f8ac81da38d11a043ae Mon Sep 17 00:00:00 2001 From: Hunk Date: Mon, 7 Aug 2023 21:15:22 +0800 Subject: [PATCH] Add gconv custom converter feature. (#2828) --- util/gconv/gconv_converter.go | 104 ++++++++++++++++++++ util/gconv/gconv_struct.go | 10 ++ util/gconv/gconv_z_unit_converter_test.go | 114 ++++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 util/gconv/gconv_converter.go create mode 100644 util/gconv/gconv_z_unit_converter_test.go diff --git a/util/gconv/gconv_converter.go b/util/gconv/gconv_converter.go new file mode 100644 index 000000000..d28f6abaf --- /dev/null +++ b/util/gconv/gconv_converter.go @@ -0,0 +1,104 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package gconv + +import ( + "reflect" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +var customConverters map[reflect.Type]map[reflect.Type]reflect.Value + +func init() { + customConverters = make(map[reflect.Type]map[reflect.Type]reflect.Value) +} + +// RegisterConverter to register custom converter. +// It must be register before you use gconv. So suggest to do it in boot. +// Note: +// 1. The fn must be func(T1)(T2,error). It will convert T1 to T2. +// 2. The T1 and T2 must be pointer. +func RegisterConverter(fn interface{}) (err error) { + fnReflectValue := reflect.ValueOf(fn) + fnReflectType := fnReflectValue.Type() + errType := reflect.TypeOf((*error)(nil)).Elem() + + if fnReflectType.Kind() != reflect.Func || + fnReflectType.NumIn() != 1 || fnReflectType.NumOut() != 2 || + !fnReflectType.Out(1).Implements(errType) { + err = gerror.NewCode(gcode.CodeInvalidParameter, "The gconv.RegisterConverter's parameter must be a function as func(T1)(T2,error).") + return + } + + inType := fnReflectType.In(0) + outType := fnReflectType.Out(0) + + subMap, ok := customConverters[inType] + if !ok { + subMap = make(map[reflect.Type]reflect.Value) + customConverters[inType] = subMap + } + + if _, ok := subMap[outType]; ok { + err = gerror.NewCode(gcode.CodeOperationFailed, "The converter has been registered.") + return + } + + subMap[outType] = reflect.ValueOf(fn) + return +} + +// callCustomConverter call the custom converter. It will try some possible type. +func callCustomConverter(reflectValue reflect.Value, pointerReflectValue reflect.Value) (ok bool, err error) { + if reflectValue.Kind() != reflect.Pointer && reflectValue.CanAddr() { + reflectValue = reflectValue.Addr() + } + if pointerReflectValue.Kind() != reflect.Pointer && pointerReflectValue.CanAddr() { + pointerReflectValue = pointerReflectValue.Addr() + } + for { + if !reflectValue.IsValid() { + break + } + subMap, ok := customConverters[reflectValue.Type()] + if ok { + pointerTmpReflectValue := pointerReflectValue + for { + if !pointerTmpReflectValue.IsValid() { + break + } + if converter, ok := subMap[pointerTmpReflectValue.Type()]; ok { + ret := converter.Call([]reflect.Value{reflectValue}) + if pointerTmpReflectValue.CanSet() { + pointerTmpReflectValue.Set(ret[0]) + } else if pointerTmpReflectValue.Elem().CanSet() { + pointerTmpReflectValue.Elem().Set(ret[0].Elem()) + } + if ret[1].IsNil() { + err = nil + } else { + err = ret[1].Interface().(error) + } + return true, err + } + if pointerTmpReflectValue.Kind() == reflect.Pointer { + pointerTmpReflectValue = pointerTmpReflectValue.Elem() + } else { + break + } + } + } + if reflectValue.Kind() == reflect.Pointer { + reflectValue = reflectValue.Elem() + } else { + break + } + } + return false, nil +} diff --git a/util/gconv/gconv_struct.go b/util/gconv/gconv_struct.go index 514c595b1..290729916 100644 --- a/util/gconv/gconv_struct.go +++ b/util/gconv/gconv_struct.go @@ -143,6 +143,11 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string pointerElemReflectValue = pointerReflectValue.Elem() } + // custom convert try first + if ok, err := callCustomConverter(paramsReflectValue, pointerReflectValue); ok { + return err + } + // If `params` and `pointer` are the same type, the do directly assignment. // For performance enhancement purpose. if pointerElemReflectValue.IsValid() { @@ -379,6 +384,11 @@ func bindVarToStructAttr(structReflectValue reflect.Value, attrName string, valu return } + // Try to call custom converter. + if ok, err := callCustomConverter(reflect.ValueOf(value), structFieldValue); ok { + return err + } + // Common interface check. var ok bool if err, ok = bindVarToReflectValueWithInterfaceCheck(structFieldValue, value); ok { diff --git a/util/gconv/gconv_z_unit_converter_test.go b/util/gconv/gconv_z_unit_converter_test.go new file mode 100644 index 000000000..790d23b5f --- /dev/null +++ b/util/gconv/gconv_z_unit_converter_test.go @@ -0,0 +1,114 @@ +package gconv_test + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gconv" +) + +func TestConverter(t *testing.T) { + + type tA struct { + Val int + } + + type tB struct { + Val int32 + Val2 string + } + + type tAA struct { + ValTop int + ValTA tA + } + + type tBB struct { + ValTop int32 + ValTB tB + } + + type tCC struct { + ValTop string + ValTa *tB + } + + type tDD struct { + ValTop string + ValTa tB + } + + gtest.C(t, func(t *gtest.T) { + a := &tA{ + Val: 1, + } + var b *tB + err := gconv.Scan(a, &b) + t.AssertNil(err) + t.AssertNE(b, nil) + t.Assert(b.Val, 1) + t.Assert(b.Val2, "") + + err = gconv.RegisterConverter(func(a tA) (b *tB, err error) { + b = &tB{ + Val: int32(a.Val), + Val2: "abc", + } + return + }) + t.AssertNil(err) + + err = gconv.Scan(a, &b) + t.AssertNil(err) + t.AssertNE(b, nil) + t.Assert(b.Val, 1) + t.Assert(b.Val2, "abc") + + aa := &tAA{ + ValTop: 123, + ValTA: tA{Val: 234}, + } + + var bb *tBB + + err = gconv.Scan(aa, &bb) + t.AssertNil(err) + t.AssertNE(bb, nil) + t.Assert(bb.ValTop, 123) + t.AssertNE(bb.ValTB.Val, 234) + + err = gconv.RegisterConverter(func(a tAA) (b *tBB, err error) { + b = &tBB{ + ValTop: int32(a.ValTop) + 2, + } + err = gconv.Scan(a.ValTA, &b.ValTB) + return + }) + t.AssertNil(err) + + err = gconv.Scan(aa, &bb) + t.AssertNil(err) + t.AssertNE(bb, nil) + t.Assert(bb.ValTop, 125) + t.Assert(bb.ValTB.Val, 234) + t.Assert(bb.ValTB.Val2, "abc") + + var cc *tCC + err = gconv.Scan(aa, &cc) + t.AssertNil(err) + t.AssertNE(cc, nil) + t.Assert(cc.ValTop, "123") + t.AssertNE(cc.ValTa, nil) + t.Assert(cc.ValTa.Val, 234) + t.Assert(cc.ValTa.Val2, "abc") + + var dd *tDD + err = gconv.Scan(aa, &dd) + t.AssertNil(err) + t.AssertNE(dd, nil) + t.Assert(dd.ValTop, "123") + t.Assert(dd.ValTa.Val, 234) + t.Assert(dd.ValTa.Val2, "abc") + + }) +}