diff --git a/util/gconv/gconv.go b/util/gconv/gconv.go index f6a006ef5c8..91a87455557 100644 --- a/util/gconv/gconv.go +++ b/util/gconv/gconv.go @@ -6,7 +6,7 @@ // Package gconv implements powerful and convenient converting functionality for any types of variables. // -// This package should keep much less dependencies with other packages. +// This package should keep much fewer dependencies with other packages. package gconv import ( @@ -23,7 +23,8 @@ import ( "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/os/gtime" - "github.com/gogf/gf/v2/util/gtag" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" + "github.com/gogf/gf/v2/util/gconv/internal/structcache" ) var ( @@ -35,13 +36,23 @@ var ( "off": {}, "false": {}, } - - // StructTagPriority defines the default priority tags for Map*/Struct* functions. - // Note that, the `gconv/param` tags are used by old version of package. - // It is strongly recommended using short tag `c/p` instead in the future. - StructTagPriority = gtag.StructTagPriority ) +func init() { + // register common converters for internal usage. + structcache.RegisterCommonConverter(structcache.CommonConverter{ + Int64: Int64, + Uint64: Uint64, + String: String, + Float32: Float32, + Float64: Float64, + Time: Time, + GTime: GTime, + Bytes: Bytes, + Bool: Bool, + }) +} + // Byte converts `any` to byte. func Byte(any interface{}) byte { if v, ok := any.(byte); ok { @@ -63,7 +74,7 @@ func Bytes(any interface{}) []byte { return value default: - if f, ok := value.(iBytes); ok { + if f, ok := value.(localinterface.IBytes); ok { return f.Bytes() } originValueAndKind := reflection.OriginValueAndKind(any) @@ -174,12 +185,12 @@ func String(any interface{}) string { if value == nil { return "" } - if f, ok := value.(iString); ok { + if f, ok := value.(localinterface.IString); ok { // If the variable implements the String() interface, // then use that interface to perform the conversion return f.String() } - if f, ok := value.(iError); ok { + if f, ok := value.(localinterface.IError); ok { // If the variable implements the Error() interface, // then use that interface to perform the conversion return f.Error() @@ -235,7 +246,7 @@ func Bool(any interface{}) bool { } return true default: - if f, ok := value.(iBool); ok { + if f, ok := value.(localinterface.IBool); ok { return f.Bool() } rv := reflect.ValueOf(any) diff --git a/util/gconv/gconv_converter.go b/util/gconv/gconv_converter.go index 0f56156779f..eb0e226d1c0 100644 --- a/util/gconv/gconv_converter.go +++ b/util/gconv/gconv_converter.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/util/gconv/internal/structcache" ) type ( @@ -82,6 +83,7 @@ func RegisterConverter(fn interface{}) (err error) { return } registeredOutTypeMap[outType] = reflect.ValueOf(fn) + structcache.RegisterCustomConvertType(outType) return } diff --git a/util/gconv/gconv_float.go b/util/gconv/gconv_float.go index 41cfb1cc76a..f8ea6a3a619 100644 --- a/util/gconv/gconv_float.go +++ b/util/gconv/gconv_float.go @@ -10,6 +10,7 @@ import ( "strconv" "github.com/gogf/gf/v2/encoding/gbinary" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // Float32 converts `any` to float32. @@ -25,7 +26,7 @@ func Float32(any interface{}) float32 { case []byte: return gbinary.DecodeToFloat32(value) default: - if f, ok := value.(iFloat32); ok { + if f, ok := value.(localinterface.IFloat32); ok { return f.Float32() } v, _ := strconv.ParseFloat(String(any), 64) @@ -46,7 +47,7 @@ func Float64(any interface{}) float64 { case []byte: return gbinary.DecodeToFloat64(value) default: - if f, ok := value.(iFloat64); ok { + if f, ok := value.(localinterface.IFloat64); ok { return f.Float64() } v, _ := strconv.ParseFloat(String(any), 64) diff --git a/util/gconv/gconv_int.go b/util/gconv/gconv_int.go index 48e26fc2c7b..1596477e29a 100644 --- a/util/gconv/gconv_int.go +++ b/util/gconv/gconv_int.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/gogf/gf/v2/encoding/gbinary" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // Int converts `any` to int. @@ -95,7 +96,7 @@ func Int64(any interface{}) int64 { case []byte: return gbinary.DecodeToInt64(value) default: - if f, ok := value.(iInt64); ok { + if f, ok := value.(localinterface.IInt64); ok { return f.Int64() } var ( diff --git a/util/gconv/gconv_interface.go b/util/gconv/gconv_interface.go deleted file mode 100644 index 8ba7361bc2e..00000000000 --- a/util/gconv/gconv_interface.go +++ /dev/null @@ -1,117 +0,0 @@ -// 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 "github.com/gogf/gf/v2/os/gtime" - -// iVal is used for type assert api for String(). -type iVal interface { - Val() interface{} -} - -// iString is used for type assert api for String(). -type iString interface { - String() string -} - -// iBool is used for type assert api for Bool(). -type iBool interface { - Bool() bool -} - -// iInt64 is used for type assert api for Int64(). -type iInt64 interface { - Int64() int64 -} - -// iUint64 is used for type assert api for Uint64(). -type iUint64 interface { - Uint64() uint64 -} - -// iFloat32 is used for type assert api for Float32(). -type iFloat32 interface { - Float32() float32 -} - -// iFloat64 is used for type assert api for Float64(). -type iFloat64 interface { - Float64() float64 -} - -// iError is used for type assert api for Error(). -type iError interface { - Error() string -} - -// iBytes is used for type assert api for Bytes(). -type iBytes interface { - Bytes() []byte -} - -// iInterface is used for type assert api for Interface(). -type iInterface interface { - Interface() interface{} -} - -// iInterfaces is used for type assert api for Interfaces(). -type iInterfaces interface { - Interfaces() []interface{} -} - -// iFloats is used for type assert api for Floats(). -type iFloats interface { - Floats() []float64 -} - -// iInts is used for type assert api for Ints(). -type iInts interface { - Ints() []int -} - -// iStrings is used for type assert api for Strings(). -type iStrings interface { - Strings() []string -} - -// iUints is used for type assert api for Uints(). -type iUints interface { - Uints() []uint -} - -// iMapStrAny is the interface support for converting struct parameter to map. -type iMapStrAny interface { - MapStrAny() map[string]interface{} -} - -// iUnmarshalValue is the interface for custom defined types customizing value assignment. -// Note that only pointer can implement interface iUnmarshalValue. -type iUnmarshalValue interface { - UnmarshalValue(interface{}) error -} - -// iUnmarshalText is the interface for custom defined types customizing value assignment. -// Note that only pointer can implement interface iUnmarshalText. -type iUnmarshalText interface { - UnmarshalText(text []byte) error -} - -// iUnmarshalText is the interface for custom defined types customizing value assignment. -// Note that only pointer can implement interface iUnmarshalJSON. -type iUnmarshalJSON interface { - UnmarshalJSON(b []byte) error -} - -// iSet is the interface for custom value assignment. -type iSet interface { - Set(value interface{}) (old interface{}) -} - -// iGTime is the interface for gtime.Time converting. -type iGTime interface { - GTime(format ...string) *gtime.Time -} diff --git a/util/gconv/gconv_map.go b/util/gconv/gconv_map.go index 243a8aa15be..459c438c857 100644 --- a/util/gconv/gconv_map.go +++ b/util/gconv/gconv_map.go @@ -13,6 +13,7 @@ import ( "github.com/gogf/gf/v2/internal/empty" "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" "github.com/gogf/gf/v2/util/gtag" ) @@ -40,8 +41,8 @@ type MapOption struct { // Map converts any variable `value` to map[string]interface{}. If the parameter `value` is not a // map/struct/*struct type, then the conversion will fail and returns nil. // -// If `value` is a struct/*struct object, the second parameter `tags` specifies the most priority -// tags that will be detected, otherwise it detects the tags in order of: +// If `value` is a struct/*struct object, the second parameter `priorityTagAndFieldName` specifies the most priority +// priorityTagAndFieldName that will be detected, otherwise it detects the priorityTagAndFieldName in order of: // gconv, json, field name. func Map(value interface{}, option ...MapOption) map[string]interface{} { return doMapConvert(value, recursiveTypeAuto, false, option...) @@ -67,10 +68,9 @@ func doMapConvert(value interface{}, recursive recursiveType, mustMapReturn bool return nil } // It redirects to its underlying value if it has implemented interface iVal. - if v, ok := value.(iVal); ok { + if v, ok := value.(localinterface.IVal); ok { value = v.Val() } - var ( usedOption = getUsedMapOption(option...) newTags = gtag.StructTagPriority @@ -334,7 +334,7 @@ func doMapConvertForMapOrStructValue(in doMapConvertForMapOrStructValueInput) in case reflect.Struct: var dataMap = make(map[string]interface{}) // Map converting interface check. - if v, ok := in.Value.(iMapStrAny); ok { + if v, ok := in.Value.(localinterface.IMapStrAny); ok { // Value copy, in case of concurrent safety. for mapK, mapV := range v.MapStrAny() { if in.RecursiveOption { diff --git a/util/gconv/gconv_scan.go b/util/gconv/gconv_scan.go index 6d22407d610..8b58788acf3 100644 --- a/util/gconv/gconv_scan.go +++ b/util/gconv/gconv_scan.go @@ -12,6 +12,7 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/internal/json" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // Scan automatically checks the type of `pointer` and converts `params` to `pointer`. @@ -197,7 +198,7 @@ func doConvertWithJsonCheck(srcValue interface{}, dstPointer interface{}) (ok bo default: // The `params` might be struct that implements interface function Interface, eg: gvar.Var. - if v, ok := srcValue.(iInterface); ok { + if v, ok := srcValue.(localinterface.IInterface); ok { return doConvertWithJsonCheck(v.Interface(), dstPointer) } } diff --git a/util/gconv/gconv_slice_any.go b/util/gconv/gconv_slice_any.go index 8308fdf67e5..00ecee0c224 100644 --- a/util/gconv/gconv_slice_any.go +++ b/util/gconv/gconv_slice_any.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // SliceAny is alias of Interfaces. @@ -104,7 +105,7 @@ func Interfaces(any interface{}) []interface{} { if array != nil { return array } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return v.Interfaces() } // JSON format string value converting. diff --git a/util/gconv/gconv_slice_float.go b/util/gconv/gconv_slice_float.go index 3d7b49949f4..bca5af6b04b 100644 --- a/util/gconv/gconv_slice_float.go +++ b/util/gconv/gconv_slice_float.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // SliceFloat is alias of Floats. @@ -126,10 +127,10 @@ func Float32s(any interface{}) []float32 { if array != nil { return array } - if v, ok := any.(iFloats); ok { + if v, ok := any.(localinterface.IFloats); ok { return Float32s(v.Floats()) } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Float32s(v.Interfaces()) } // JSON format string value converting. @@ -250,10 +251,10 @@ func Float64s(any interface{}) []float64 { if array != nil { return array } - if v, ok := any.(iFloats); ok { + if v, ok := any.(localinterface.IFloats); ok { return v.Floats() } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Floats(v.Interfaces()) } // JSON format string value converting. diff --git a/util/gconv/gconv_slice_int.go b/util/gconv/gconv_slice_int.go index f28e7fd1846..929acfa147e 100644 --- a/util/gconv/gconv_slice_int.go +++ b/util/gconv/gconv_slice_int.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // SliceInt is alias of Ints. @@ -126,10 +127,10 @@ func Ints(any interface{}) []int { if array != nil { return array } - if v, ok := any.(iInts); ok { + if v, ok := any.(localinterface.IInts); ok { return v.Ints() } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Ints(v.Interfaces()) } // JSON format string value converting. @@ -255,10 +256,10 @@ func Int32s(any interface{}) []int32 { if array != nil { return array } - if v, ok := any.(iInts); ok { + if v, ok := any.(localinterface.IInts); ok { return Int32s(v.Ints()) } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Int32s(v.Interfaces()) } // JSON format string value converting. @@ -384,10 +385,10 @@ func Int64s(any interface{}) []int64 { if array != nil { return array } - if v, ok := any.(iInts); ok { + if v, ok := any.(localinterface.IInts); ok { return Int64s(v.Ints()) } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Int64s(v.Interfaces()) } // JSON format string value converting. diff --git a/util/gconv/gconv_slice_str.go b/util/gconv/gconv_slice_str.go index 8c97c0157d3..405e72ca522 100644 --- a/util/gconv/gconv_slice_str.go +++ b/util/gconv/gconv_slice_str.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // SliceStr is alias of Strings. @@ -127,10 +128,10 @@ func Strings(any interface{}) []string { if array != nil { return array } - if v, ok := any.(iStrings); ok { + if v, ok := any.(localinterface.IStrings); ok { return v.Strings() } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Strings(v.Interfaces()) } // JSON format string value converting. diff --git a/util/gconv/gconv_slice_uint.go b/util/gconv/gconv_slice_uint.go index a1ffa761789..8ab15dbf7fa 100644 --- a/util/gconv/gconv_slice_uint.go +++ b/util/gconv/gconv_slice_uint.go @@ -13,6 +13,7 @@ import ( "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/reflection" "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // SliceUint is alias of Uints. @@ -136,10 +137,10 @@ func Uints(any interface{}) []uint { } // Default handler. - if v, ok := any.(iUints); ok { + if v, ok := any.(localinterface.IUints); ok { return v.Uints() } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Uints(v.Interfaces()) } // JSON format string value converting. @@ -270,10 +271,10 @@ func Uint32s(any interface{}) []uint32 { } // Default handler. - if v, ok := any.(iUints); ok { + if v, ok := any.(localinterface.IUints); ok { return Uint32s(v.Uints()) } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Uint32s(v.Interfaces()) } // JSON format string value converting. @@ -404,10 +405,10 @@ func Uint64s(any interface{}) []uint64 { return array } // Default handler. - if v, ok := any.(iUints); ok { + if v, ok := any.(localinterface.IUints); ok { return Uint64s(v.Uints()) } - if v, ok := any.(iInterfaces); ok { + if v, ok := any.(localinterface.IInterfaces); ok { return Uint64s(v.Interfaces()) } // JSON format string value converting. diff --git a/util/gconv/gconv_struct.go b/util/gconv/gconv_struct.go index 62883b7e6b5..f7708347a80 100644 --- a/util/gconv/gconv_struct.go +++ b/util/gconv/gconv_struct.go @@ -15,7 +15,8 @@ import ( "github.com/gogf/gf/v2/internal/empty" "github.com/gogf/gf/v2/internal/json" "github.com/gogf/gf/v2/internal/utils" - "github.com/gogf/gf/v2/util/gtag" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" + "github.com/gogf/gf/v2/util/gconv/internal/structcache" ) // Struct maps the params key-value pairs to the corresponding struct object's attributes. @@ -36,8 +37,8 @@ func Struct(params interface{}, pointer interface{}, paramKeyToAttrMap ...map[st } // StructTag acts as Struct but also with support for priority tag feature, which retrieves the -// specified tags for `params` key-value items to struct attribute names mapping. -// The parameter `priorityTag` supports multiple tags that can be joined with char ','. +// specified priorityTagAndFieldName for `params` key-value items to struct attribute names mapping. +// The parameter `priorityTag` supports multiple priorityTagAndFieldName that can be joined with char ','. func StructTag(params interface{}, pointer interface{}, priorityTag string) (err error) { return doStruct(params, pointer, nil, priorityTag) } @@ -139,7 +140,7 @@ func doStruct( } }() } - // if v, ok := pointerElemReflectValue.Interface().(iUnmarshalValue); ok { + // if v, ok := pointerElemReflectValue.Interface().(localinterface.IUnmarshalValue); ok { // return v.UnmarshalValue(params) // } // Note that it's `pointerElemReflectValue` here not `pointerReflectValue`. @@ -149,152 +150,258 @@ func doStruct( // Retrieve its element, may be struct at last. pointerElemReflectValue = pointerElemReflectValue.Elem() } - - // paramsMap is the map[string]interface{} type variable for params. - // DO NOT use MapDeep here. - paramsMap := doMapConvert(paramsInterface, recursiveTypeAuto, true) - if paramsMap == nil { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - `convert params from "%#v" to "map[string]interface{}" failed`, - params, - ) + paramsMap, ok := paramsInterface.(map[string]interface{}) + if !ok { + // paramsMap is the map[string]interface{} type variable for params. + // DO NOT use MapDeep here. + paramsMap = doMapConvert(paramsInterface, recursiveTypeAuto, true) + if paramsMap == nil { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `convert params from "%#v" to "map[string]interface{}" failed`, + params, + ) + } } - // Nothing to be done as the parameters are empty. if len(paramsMap) == 0 { return nil } - - // Holds the info for subsequent converting. - type toBeConvertedFieldInfo struct { - Value any // Found value by tag name or field name from input. - FieldIndex int // The associated reflection field index. - FieldOrTagName string // Field name or tag name for field tag by priority tags. + // Get struct info from cache or parse struct and cache the struct info. + cachedStructInfo := structcache.GetCachedStructInfo( + pointerElemReflectValue.Type(), priorityTag, + ) + // Nothing to be converted. + if cachedStructInfo == nil { + return nil + } + // For the structure types of 0 tagOrFiledNameToFieldInfoMap, + // they also need to be cached to prevent invalid logic + if cachedStructInfo.HasNoFields() { + return nil } - var ( - priorityTagArray []string - elemFieldName string - elemFieldType reflect.StructField - elemFieldValue reflect.Value - elemType = pointerElemReflectValue.Type() - toBeConvertedFieldNameToInfoMap = map[string]toBeConvertedFieldInfo{} // key=elemFieldName + // Indicates that those values have been used and cannot be reused. + usedParamsKeyOrTagNameMap = structcache.GetUsedParamsKeyOrTagNameMapFromPool() ) + defer structcache.PutUsedParamsKeyOrTagNameMapToPool(usedParamsKeyOrTagNameMap) - if priorityTag != "" { - priorityTagArray = append(utils.SplitAndTrim(priorityTag, ","), gtag.StructTagPriority...) - } else { - priorityTagArray = gtag.StructTagPriority - } - - for i := 0; i < pointerElemReflectValue.NumField(); i++ { - elemFieldType = elemType.Field(i) - elemFieldName = elemFieldType.Name - // Only do converting to public attributes. - if !utils.IsLetterUpper(elemFieldName[0]) { - continue - } - - var fieldTagName = getTagNameFromField(elemFieldType, priorityTagArray) - // Maybe it's struct/*struct embedded. - if elemFieldType.Anonymous { - // type Name struct { - // LastName string `json:"lastName"` - // FirstName string `json:"firstName"` - // } - // - // type User struct { - // Name `json:"name"` - // // ... - // } - // - // It is only recorded if the name has a fieldTag - if fieldTagName != "" { - toBeConvertedFieldNameToInfoMap[elemFieldName] = toBeConvertedFieldInfo{ - FieldIndex: elemFieldType.Index[0], - FieldOrTagName: fieldTagName, + // Firstly, search according to custom mapping rules. + // If a possible direct assignment is found, reduce the number of subsequent map searches. + for paramKey, fieldName := range paramKeyToAttrMap { + fieldInfo := cachedStructInfo.GetFieldInfo(fieldName) + if fieldInfo != nil { + if paramsValue, ok := paramsMap[paramKey]; ok { + fieldValue := fieldInfo.GetFieldReflectValue(pointerElemReflectValue) + if err = bindVarToStructField( + fieldValue, + paramsValue, + fieldInfo, + paramKeyToAttrMap, + ); err != nil { + return err } - } - - elemFieldValue = pointerElemReflectValue.Field(i) - // Ignore the interface attribute if it's nil. - if elemFieldValue.Kind() == reflect.Interface { - elemFieldValue = elemFieldValue.Elem() - if !elemFieldValue.IsValid() { - continue + if len(fieldInfo.OtherSameNameFieldIndex) > 0 { + if err = setOtherSameNameField( + fieldInfo, paramsValue, pointerReflectValue, paramKeyToAttrMap, + ); err != nil { + return err + } } - } - if err = doStruct(paramsMap, elemFieldValue, paramKeyToAttrMap, priorityTag); err != nil { - return err - } - } else { - // Use the native elemFieldName name as the fieldTag - if fieldTagName == "" { - fieldTagName = elemFieldName - } - toBeConvertedFieldNameToInfoMap[elemFieldName] = toBeConvertedFieldInfo{ - FieldIndex: elemFieldType.Index[0], - FieldOrTagName: fieldTagName, + usedParamsKeyOrTagNameMap[paramKey] = struct{}{} } } } - - // Nothing to be converted. - if len(toBeConvertedFieldNameToInfoMap) == 0 { + // Already done converting for given `paramsMap`. + if len(usedParamsKeyOrTagNameMap) == len(paramsMap) { return nil } + // If the length of `paramsMap` is less than the number of fields, then loop based on `paramsMap` + if len(paramsMap) < len(cachedStructInfo.FieldConvertInfos) { + return bindStructWithLoopParamsMap( + paramsMap, pointerElemReflectValue, paramKeyToAttrMap, usedParamsKeyOrTagNameMap, cachedStructInfo, + ) + } + return bindStructWithLoopFieldInfos( + paramsMap, pointerElemReflectValue, paramKeyToAttrMap, usedParamsKeyOrTagNameMap, cachedStructInfo, + ) +} - // Search the parameter value for the field. - var paramsValue any - for fieldName, fieldInfo := range toBeConvertedFieldNameToInfoMap { - if paramsValue, ok = paramsMap[fieldInfo.FieldOrTagName]; ok { - fieldInfo.Value = paramsValue - toBeConvertedFieldNameToInfoMap[fieldName] = fieldInfo +func setOtherSameNameField( + fieldInfo *structcache.CachedFieldInfo, + srcValue any, + structValue reflect.Value, + paramKeyToAttrMap map[string]string, +) (err error) { + // loop the same field name of all sub attributes. + for i := range fieldInfo.OtherSameNameFieldIndex { + fieldValue := fieldInfo.GetOtherFieldReflectValue(structValue, i) + if err = bindVarToStructField(fieldValue, srcValue, fieldInfo, paramKeyToAttrMap); err != nil { + return err } } + return nil +} - // Firstly, search according to custom mapping rules. - // If a possible direct assignment is found, reduce the number of subsequent map searches. - var fieldInfo toBeConvertedFieldInfo - for paramKey, fieldName := range paramKeyToAttrMap { - // Prevent setting of non-existent fields - fieldInfo, ok = toBeConvertedFieldNameToInfoMap[fieldName] - if ok { - // Prevent non-existent values from being set. - if paramsValue, ok = paramsMap[paramKey]; ok { - fieldInfo.Value = paramsValue - toBeConvertedFieldNameToInfoMap[fieldName] = fieldInfo +func bindStructWithLoopParamsMap( + paramsMap map[string]any, + structValue reflect.Value, + paramKeyToAttrMap map[string]string, + usedParamsKeyOrTagNameMap map[string]struct{}, + cachedStructInfo *structcache.CachedStructInfo, +) (err error) { + var ( + fieldName string + fieldInfo *structcache.CachedFieldInfo + fuzzLastKey string + fieldValue reflect.Value + paramKey string + paramValue any + ok bool + ) + for paramKey, paramValue = range paramsMap { + if _, ok = usedParamsKeyOrTagNameMap[paramKey]; ok { + continue + } + fieldInfo = cachedStructInfo.GetFieldInfo(paramKey) + if fieldInfo != nil { + fieldName = fieldInfo.FieldName() + // already converted using its field name? + // the field name has the more priority than tag name. + _, ok = usedParamsKeyOrTagNameMap[fieldName] + if ok && fieldInfo.IsField { + continue + } + fieldValue = fieldInfo.GetFieldReflectValue(structValue) + if err = bindVarToStructField( + fieldValue, paramValue, fieldInfo, paramKeyToAttrMap, + ); err != nil { + return err + } + // handle same field name in nested struct. + if len(fieldInfo.OtherSameNameFieldIndex) > 0 { + if err = setOtherSameNameField(fieldInfo, paramValue, structValue, paramKeyToAttrMap); err != nil { + return err + } + } + usedParamsKeyOrTagNameMap[fieldName] = struct{}{} + continue + } + + // fuzzy matching. + for _, fieldInfo = range cachedStructInfo.FieldConvertInfos { + fieldName = fieldInfo.FieldName() + if _, ok = usedParamsKeyOrTagNameMap[fieldName]; ok { + continue + } + fuzzLastKey = fieldInfo.LastFuzzyKey.Load().(string) + paramValue, ok = paramsMap[fuzzLastKey] + if !ok { + if strings.EqualFold( + fieldInfo.RemoveSymbolsFieldName, utils.RemoveSymbols(paramKey), + ) { + paramValue, ok = paramsMap[paramKey] + // If it is found this time, update it based on what was not found last time. + fieldInfo.LastFuzzyKey.Store(paramKey) + } + } + if ok { + fieldValue = fieldInfo.GetFieldReflectValue(structValue) + if paramValue != nil { + if err = bindVarToStructField( + fieldValue, paramValue, fieldInfo, paramKeyToAttrMap, + ); err != nil { + return err + } + // handle same field name in nested struct. + if len(fieldInfo.OtherSameNameFieldIndex) > 0 { + if err = setOtherSameNameField( + fieldInfo, paramValue, structValue, paramKeyToAttrMap, + ); err != nil { + return err + } + } + } + usedParamsKeyOrTagNameMap[fieldInfo.FieldName()] = struct{}{} + break } } } + return nil +} +func bindStructWithLoopFieldInfos( + paramsMap map[string]any, + structValue reflect.Value, + paramKeyToAttrMap map[string]string, + usedParamsKeyOrTagNameMap map[string]struct{}, + cachedStructInfo *structcache.CachedStructInfo, +) (err error) { var ( - paramKey string - paramValue any - fieldName string - // Indicates that those values have been used and cannot be reused. - usedParamsKeyOrTagNameMap = map[string]struct{}{} + fieldInfo *structcache.CachedFieldInfo + fuzzLastKey string + fieldValue reflect.Value + paramKey string + paramValue any + matched bool + ok bool ) - for fieldName, fieldInfo = range toBeConvertedFieldNameToInfoMap { - // If it is not empty, the tag or elemFieldName name matches - if fieldInfo.Value != nil { - if err = bindVarToStructAttrWithFieldIndex( - pointerElemReflectValue, fieldName, fieldInfo.FieldIndex, fieldInfo.Value, paramKeyToAttrMap, + for _, fieldInfo = range cachedStructInfo.FieldConvertInfos { + for _, fieldTag := range fieldInfo.PriorityTagAndFieldName { + if paramValue, ok = paramsMap[fieldTag]; !ok { + continue + } + if _, ok = usedParamsKeyOrTagNameMap[fieldTag]; ok { + matched = true + break + } + fieldValue = fieldInfo.GetFieldReflectValue(structValue) + if err = bindVarToStructField( + fieldValue, paramValue, fieldInfo, paramKeyToAttrMap, ); err != nil { return err } - usedParamsKeyOrTagNameMap[fieldInfo.FieldOrTagName] = struct{}{} + // handle same field name in nested struct. + if len(fieldInfo.OtherSameNameFieldIndex) > 0 { + if err = setOtherSameNameField( + fieldInfo, paramValue, structValue, paramKeyToAttrMap, + ); err != nil { + return err + } + } + usedParamsKeyOrTagNameMap[fieldTag] = struct{}{} + matched = true + break + } + if matched { + matched = false continue } - // If value is nil, a fuzzy match is used for search the key and value for converting. - paramKey, paramValue = fuzzyMatchingFieldName(fieldName, paramsMap, usedParamsKeyOrTagNameMap) - if paramValue != nil { - if err = bindVarToStructAttrWithFieldIndex( - pointerElemReflectValue, fieldName, fieldInfo.FieldIndex, paramValue, paramKeyToAttrMap, - ); err != nil { - return err + fuzzLastKey = fieldInfo.LastFuzzyKey.Load().(string) + if paramValue, ok = paramsMap[fuzzLastKey]; !ok { + paramKey, paramValue = fuzzyMatchingFieldName( + fieldInfo.RemoveSymbolsFieldName, paramsMap, usedParamsKeyOrTagNameMap, + ) + ok = paramKey != "" + fieldInfo.LastFuzzyKey.Store(paramKey) + } + if ok { + fieldValue = fieldInfo.GetFieldReflectValue(structValue) + if paramValue != nil { + if err = bindVarToStructField( + fieldValue, paramValue, fieldInfo, paramKeyToAttrMap, + ); err != nil { + return err + } + // handle same field name in nested struct. + if len(fieldInfo.OtherSameNameFieldIndex) > 0 { + if err = setOtherSameNameField( + fieldInfo, paramValue, structValue, paramKeyToAttrMap, + ); err != nil { + return err + } + } } usedParamsKeyOrTagNameMap[paramKey] = struct{}{} } @@ -302,24 +409,6 @@ func doStruct( return nil } -func getTagNameFromField(field reflect.StructField, priorityTags []string) string { - for _, tag := range priorityTags { - value, ok := field.Tag.Lookup(tag) - if ok { - // If there's something else in the tag string, - // it uses the first part which is split using char ','. - // Example: - // orm:"id, priority" - // orm:"name, with:uid=id" - array := strings.Split(value, ",") - // json:",omitempty" - trimmedTagName := strings.TrimSpace(array[0]) - return trimmedTagName - } - } - return "" -} - // fuzzy matching rule: // to match field name and param key in case-insensitive and without symbols. func fuzzyMatchingFieldName( @@ -327,7 +416,6 @@ func fuzzyMatchingFieldName( paramsMap map[string]any, usedParamsKeyMap map[string]struct{}, ) (string, any) { - fieldName = utils.RemoveSymbols(fieldName) for paramKey, paramValue := range paramsMap { if _, ok := usedParamsKeyMap[paramKey]; ok { continue @@ -340,78 +428,61 @@ func fuzzyMatchingFieldName( return "", nil } -// bindVarToStructAttrWithFieldIndex sets value to struct object attribute by name. -func bindVarToStructAttrWithFieldIndex( - structReflectValue reflect.Value, attrName string, - fieldIndex int, value interface{}, paramKeyToAttrMap map[string]string, +// bindVarToStructField sets value to struct object attribute by name. +func bindVarToStructField( + fieldValue reflect.Value, + srcValue interface{}, + fieldInfo *structcache.CachedFieldInfo, + paramKeyToAttrMap map[string]string, ) (err error) { - structFieldValue := structReflectValue.Field(fieldIndex) - if !structFieldValue.IsValid() { + if !fieldValue.IsValid() { return nil } // CanSet checks whether attribute is public accessible. - if !structFieldValue.CanSet() { + if !fieldValue.CanSet() { return nil } defer func() { if exception := recover(); exception != nil { - if err = bindVarToReflectValue(structFieldValue, value, paramKeyToAttrMap); err != nil { - err = gerror.Wrapf(err, `error binding value to attribute "%s"`, attrName) + if err = bindVarToReflectValue(fieldValue, srcValue, paramKeyToAttrMap); err != nil { + err = gerror.Wrapf(err, `error binding srcValue to attribute "%s"`, fieldInfo.FieldName()) } } }() // Directly converting. - if empty.IsNil(value) { - structFieldValue.Set(reflect.Zero(structFieldValue.Type())) - } else { - // Try to call custom converter. - // Issue: https://github.com/gogf/gf/issues/3099 - var ( - customConverterInput reflect.Value - ok bool - ) - if customConverterInput, ok = value.(reflect.Value); !ok { - customConverterInput = reflect.ValueOf(value) + if empty.IsNil(srcValue) { + fieldValue.Set(reflect.Zero(fieldValue.Type())) + return nil + } + // Try to call custom converter. + // Issue: https://github.com/gogf/gf/issues/3099 + var ( + customConverterInput reflect.Value + ok bool + ) + if fieldInfo.IsCustomConvert { + if customConverterInput, ok = srcValue.(reflect.Value); !ok { + customConverterInput = reflect.ValueOf(srcValue) } - - if ok, err = callCustomConverter(customConverterInput, structFieldValue); ok || err != nil { + if ok, err = callCustomConverter(customConverterInput, fieldValue); ok || err != nil { return } - - // Special handling for certain types: - // - Overwrite the default type converting logic of stdlib for time.Time/*time.Time. - var structFieldTypeName = structFieldValue.Type().String() - switch structFieldTypeName { - case "time.Time", "*time.Time": - doConvertWithReflectValueSet(structFieldValue, doConvertInput{ - FromValue: value, - ToTypeName: structFieldTypeName, - ReferValue: structFieldValue, - }) - return - // Hold the time zone consistent in recursive - // Issue: https://github.com/gogf/gf/issues/2980 - case "*gtime.Time", "gtime.Time": - doConvertWithReflectValueSet(structFieldValue, doConvertInput{ - FromValue: value, - ToTypeName: structFieldTypeName, - ReferValue: structFieldValue, - }) + } + if fieldInfo.IsCommonInterface { + if ok, err = bindVarToReflectValueWithInterfaceCheck(fieldValue, srcValue); ok || err != nil { return } - - // Common interface check. - if ok, err = bindVarToReflectValueWithInterfaceCheck(structFieldValue, value); ok { - return err - } - - // Default converting. - doConvertWithReflectValueSet(structFieldValue, doConvertInput{ - FromValue: value, - ToTypeName: structFieldTypeName, - ReferValue: structFieldValue, - }) } + // Common types use fast assignment logic + if fieldInfo.ConvertFunc != nil { + fieldInfo.ConvertFunc(srcValue, fieldValue) + return nil + } + doConvertWithReflectValueSet(fieldValue, doConvertInput{ + FromValue: srcValue, + ToTypeName: fieldInfo.StructField.Type.String(), + ReferValue: fieldValue, + }) return nil } @@ -432,17 +503,17 @@ func bindVarToReflectValueWithInterfaceCheck(reflectValue reflect.Value, value i pointer = reflectValue.Interface() } // UnmarshalValue. - if v, ok := pointer.(iUnmarshalValue); ok { + if v, ok := pointer.(localinterface.IUnmarshalValue); ok { return ok, v.UnmarshalValue(value) } // UnmarshalText. - if v, ok := pointer.(iUnmarshalText); ok { + if v, ok := pointer.(localinterface.IUnmarshalText); ok { var valueBytes []byte if b, ok := value.([]byte); ok { valueBytes = b } else if s, ok := value.(string); ok { valueBytes = []byte(s) - } else if f, ok := value.(iString); ok { + } else if f, ok := value.(localinterface.IString); ok { valueBytes = []byte(f.String()) } if len(valueBytes) > 0 { @@ -450,13 +521,13 @@ func bindVarToReflectValueWithInterfaceCheck(reflectValue reflect.Value, value i } } // UnmarshalJSON. - if v, ok := pointer.(iUnmarshalJSON); ok { + if v, ok := pointer.(localinterface.IUnmarshalJSON); ok { var valueBytes []byte if b, ok := value.([]byte); ok { valueBytes = b } else if s, ok := value.(string); ok { valueBytes = []byte(s) - } else if f, ok := value.(iString); ok { + } else if f, ok := value.(localinterface.IString); ok { valueBytes = []byte(f.String()) } @@ -472,7 +543,7 @@ func bindVarToReflectValueWithInterfaceCheck(reflectValue reflect.Value, value i return ok, v.UnmarshalJSON(valueBytes) } } - if v, ok := pointer.(iSet); ok { + if v, ok := pointer.(localinterface.ISet); ok { v.Set(value) return ok, nil } @@ -497,7 +568,7 @@ func bindVarToReflectValue( switch kind { case reflect.Slice, reflect.Array, reflect.Ptr, reflect.Interface: if !structFieldValue.IsNil() { - if v, ok := structFieldValue.Interface().(iSet); ok { + if v, ok := structFieldValue.Interface().(localinterface.ISet); ok { v.Set(value) return nil } diff --git a/util/gconv/gconv_structs.go b/util/gconv/gconv_structs.go index b6722477a8d..867fcdd2bbd 100644 --- a/util/gconv/gconv_structs.go +++ b/util/gconv/gconv_structs.go @@ -25,8 +25,8 @@ func SliceStruct(params interface{}, pointer interface{}, mapping ...map[string] } // StructsTag acts as Structs but also with support for priority tag feature, which retrieves the -// specified tags for `params` key-value items to struct attribute names mapping. -// The parameter `priorityTag` supports multiple tags that can be joined with char ','. +// specified priorityTagAndFieldName for `params` key-value items to struct attribute names mapping. +// The parameter `priorityTag` supports multiple priorityTagAndFieldName that can be joined with char ','. func StructsTag(params interface{}, pointer interface{}, priorityTag string) (err error) { return doStructs(params, pointer, nil, priorityTag) } diff --git a/util/gconv/gconv_time.go b/util/gconv/gconv_time.go index 20823ceb498..d42a0639518 100644 --- a/util/gconv/gconv_time.go +++ b/util/gconv/gconv_time.go @@ -11,6 +11,7 @@ import ( "github.com/gogf/gf/v2/internal/utils" "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // Time converts `any` to time.Time. @@ -52,7 +53,7 @@ func GTime(any interface{}, format ...string) *gtime.Time { if any == nil { return nil } - if v, ok := any.(iGTime); ok { + if v, ok := any.(localinterface.IGTime); ok { return v.GTime(format...) } // It's already this type. diff --git a/util/gconv/gconv_uint.go b/util/gconv/gconv_uint.go index 028a14dec9a..3147832cc6d 100644 --- a/util/gconv/gconv_uint.go +++ b/util/gconv/gconv_uint.go @@ -11,6 +11,7 @@ import ( "strconv" "github.com/gogf/gf/v2/encoding/gbinary" + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" ) // Uint converts `any` to uint. @@ -95,7 +96,7 @@ func Uint64(any interface{}) uint64 { case []byte: return gbinary.DecodeToUint64(value) default: - if f, ok := value.(iUint64); ok { + if f, ok := value.(localinterface.IUint64); ok { return f.Uint64() } s := String(value) diff --git a/util/gconv/internal/localinterface/localinterface.go b/util/gconv/internal/localinterface/localinterface.go new file mode 100644 index 00000000000..bdcd041aab3 --- /dev/null +++ b/util/gconv/internal/localinterface/localinterface.go @@ -0,0 +1,118 @@ +// 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 localinterface defines some interfaces for converting usage. +package localinterface + +import "github.com/gogf/gf/v2/os/gtime" + +// IVal is used for type assert api for String(). +type IVal interface { + Val() interface{} +} + +// IString is used for type assert api for String(). +type IString interface { + String() string +} + +// IBool is used for type assert api for Bool(). +type IBool interface { + Bool() bool +} + +// IInt64 is used for type assert api for Int64(). +type IInt64 interface { + Int64() int64 +} + +// IUint64 is used for type assert api for Uint64(). +type IUint64 interface { + Uint64() uint64 +} + +// IFloat32 is used for type assert api for Float32(). +type IFloat32 interface { + Float32() float32 +} + +// IFloat64 is used for type assert api for Float64(). +type IFloat64 interface { + Float64() float64 +} + +// IError is used for type assert api for Error(). +type IError interface { + Error() string +} + +// IBytes is used for type assert api for Bytes(). +type IBytes interface { + Bytes() []byte +} + +// IInterface is used for type assert api for Interface(). +type IInterface interface { + Interface() interface{} +} + +// IInterfaces is used for type assert api for Interfaces(). +type IInterfaces interface { + Interfaces() []interface{} +} + +// IFloats is used for type assert api for Floats(). +type IFloats interface { + Floats() []float64 +} + +// IInts is used for type assert api for Ints(). +type IInts interface { + Ints() []int +} + +// IStrings is used for type assert api for Strings(). +type IStrings interface { + Strings() []string +} + +// IUints is used for type assert api for Uints(). +type IUints interface { + Uints() []uint +} + +// IMapStrAny is the interface support for converting struct parameter to map. +type IMapStrAny interface { + MapStrAny() map[string]interface{} +} + +// IUnmarshalValue is the interface for custom defined types customizing value assignment. +// Note that only pointer can implement interface iUnmarshalValue. +type IUnmarshalValue interface { + UnmarshalValue(interface{}) error +} + +// IUnmarshalText is the interface for custom defined types customizing value assignment. +// Note that only pointer can implement interface iUnmarshalText. +type IUnmarshalText interface { + UnmarshalText(text []byte) error +} + +// IUnmarshalJSON is the interface for custom defined types customizing value assignment. +// Note that only pointer can implement interface iUnmarshalJSON. +type IUnmarshalJSON interface { + UnmarshalJSON(b []byte) error +} + +// ISet is the interface for custom value assignment. +type ISet interface { + Set(value interface{}) (old interface{}) +} + +// IGTime is the interface for gtime.Time converting. +type IGTime interface { + GTime(format ...string) *gtime.Time +} diff --git a/util/gconv/internal/structcache/structcache.go b/util/gconv/internal/structcache/structcache.go new file mode 100644 index 00000000000..cdb97388dad --- /dev/null +++ b/util/gconv/internal/structcache/structcache.go @@ -0,0 +1,64 @@ +// 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 structcache provides struct and field info cache feature to enhance performance for struct converting. +package structcache + +import ( + "reflect" + + "github.com/gogf/gf/v2/util/gconv/internal/localinterface" +) + +var ( + // customConvertTypeMap is used to store whether field types are registered to custom conversions + // For example: + // func (src *TypeA) (dst *TypeB,err error) + // This map will store `TypeB` for quick judgment during assignment. + customConvertTypeMap = map[reflect.Type]struct{}{} +) + +// RegisterCustomConvertType registers custom +func RegisterCustomConvertType(fieldType reflect.Type) { + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + customConvertTypeMap[fieldType] = struct{}{} +} + +var ( + implUnmarshalText = reflect.TypeOf((*localinterface.IUnmarshalText)(nil)).Elem() + implUnmarshalJson = reflect.TypeOf((*localinterface.IUnmarshalJSON)(nil)).Elem() + implUnmarshalValue = reflect.TypeOf((*localinterface.IUnmarshalValue)(nil)).Elem() +) + +func checkTypeIsCommonInterface(field reflect.StructField) bool { + isCommonInterface := false + switch field.Type.String() { + case "time.Time", "*time.Time": + // default convert. + + case "gtime.Time", "*gtime.Time": + // default convert. + + default: + // Implemented three types of interfaces that must be pointer types, otherwise it is meaningless + if field.Type.Kind() != reflect.Ptr { + field.Type = reflect.PointerTo(field.Type) + } + switch { + case field.Type.Implements(implUnmarshalText): + isCommonInterface = true + + case field.Type.Implements(implUnmarshalJson): + isCommonInterface = true + + case field.Type.Implements(implUnmarshalValue): + isCommonInterface = true + } + } + return isCommonInterface +} diff --git a/util/gconv/internal/structcache/structcache_cached.go b/util/gconv/internal/structcache/structcache_cached.go new file mode 100644 index 00000000000..553396774e4 --- /dev/null +++ b/util/gconv/internal/structcache/structcache_cached.go @@ -0,0 +1,134 @@ +// 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 structcache + +import ( + "reflect" + "sync" + "time" + + "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gtag" +) + +// CommonConverter holds some converting functions of common types for internal usage. +type CommonConverter struct { + Int64 func(any interface{}) int64 + Uint64 func(any interface{}) uint64 + String func(any interface{}) string + Float32 func(any interface{}) float32 + Float64 func(any interface{}) float64 + Time func(any interface{}, format ...string) time.Time + GTime func(any interface{}, format ...string) *gtime.Time + Bytes func(any interface{}) []byte + Bool func(any interface{}) bool +} + +var ( + // map[reflect.Type]*CachedStructInfo + cachedStructsInfoMap = sync.Map{} + + // localCommonConverter holds some converting functions of common types for internal usage. + localCommonConverter CommonConverter +) + +// RegisterCommonConverter registers the CommonConverter for local usage. +func RegisterCommonConverter(commonConverter CommonConverter) { + localCommonConverter = commonConverter +} + +// GetCachedStructInfo retrieves or parses and returns a cached info for certain struct type. +func GetCachedStructInfo( + structType reflect.Type, + priorityTag string, +) *CachedStructInfo { + if structType.Kind() != reflect.Struct { + return nil + } + // check if it has been cached. + structInfo, ok := getCachedConvertStructInfo(structType) + if ok { + return structInfo + } + + // it parses and generates a cache info for given struct type. + structInfo = &CachedStructInfo{ + tagOrFiledNameToFieldInfoMap: make(map[string]*CachedFieldInfo), + } + var ( + priorityTagArray []string + parentIndex = make([]int, 0) + ) + if priorityTag != "" { + priorityTagArray = append(utils.SplitAndTrim(priorityTag, ","), gtag.StructTagPriority...) + } else { + priorityTagArray = gtag.StructTagPriority + } + parseStruct(structType, parentIndex, structInfo, priorityTagArray) + setCachedConvertStructInfo(structType, structInfo) + return structInfo +} + +func setCachedConvertStructInfo(structType reflect.Type, info *CachedStructInfo) { + // Temporarily enabled as an experimental feature + cachedStructsInfoMap.Store(structType, info) +} + +func getCachedConvertStructInfo(structType reflect.Type) (*CachedStructInfo, bool) { + // Temporarily enabled as an experimental feature + v, ok := cachedStructsInfoMap.Load(structType) + if ok { + return v.(*CachedStructInfo), ok + } + return nil, false +} + +func parseStruct( + structType reflect.Type, + fieldIndexes []int, + structInfo *CachedStructInfo, + priorityTagArray []string, +) { + var ( + fieldName string + structField reflect.StructField + fieldType reflect.Type + ) + // TODO: + // Check if the structure has already been cached in the cache. + // If it has been cached, some information can be reused, + // but the [FieldIndex] needs to be reset. + // We will not implement it temporarily because it is somewhat complex + for i := 0; i < structType.NumField(); i++ { + structField = structType.Field(i) + fieldType = structField.Type + fieldName = structField.Name + // Only do converting to public attributes. + if !utils.IsLetterUpper(fieldName[0]) { + continue + } + + // store field + structInfo.AddField(structField, append(fieldIndexes, i), priorityTagArray) + + // normal basic attributes. + if structField.Anonymous { + // handle struct attributes, it might be struct/*struct embedded.. + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + if fieldType.Kind() != reflect.Struct { + continue + } + if structField.Tag != "" { + // TODO: If it's an anonymous field with a tag, doesn't it need to be recursive? + } + parseStruct(fieldType, append(fieldIndexes, i), structInfo, priorityTagArray) + } + } +} diff --git a/util/gconv/internal/structcache/structcache_cached_field_info.go b/util/gconv/internal/structcache/structcache_cached_field_info.go new file mode 100644 index 00000000000..96bece627ea --- /dev/null +++ b/util/gconv/internal/structcache/structcache_cached_field_info.go @@ -0,0 +1,141 @@ +// 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 structcache + +import ( + "reflect" + "sync/atomic" +) + +// CachedFieldInfo holds the cached info for struct field. +type CachedFieldInfo struct { + // WARN: + // The [CachedFieldInfoBase] structure cannot be merged with the following [IsField] field into one structure. + // The [IsField] field should be used separately in the [bindStructWithLoopParamsMap] method + *CachedFieldInfoBase + + // This field is mainly used in the [bindStructWithLoopParamsMap] method. + // This field is needed when both `fieldName` and `tag` of a field exist in the map. + // For example: + // field string `json:"name"` + // map = { + // "field" : "f1", + // "name" : "n1", + // } + // The `name` should be used here. + // In the bindStructWithLoopParamsMap method, due to the disorder of `map`, `field` may be traversed first. + // This field is more about priority, that is, the priority of `name` is higher than that of `field`, + // even if it has been set before. + IsField bool +} + +// CachedFieldInfoBase holds the cached info for struct field. +type CachedFieldInfoBase struct { + // FieldIndexes holds the global index number from struct info. + // The field may belong to an embedded structure, so it is defined here as []int. + FieldIndexes []int + + // PriorityTagAndFieldName holds the tag value(conv, param, p, c, json) and the field name. + // PriorityTagAndFieldName contains the field name, which is the last item of slice. + PriorityTagAndFieldName []string + + // IsCommonInterface marks this field implements common interfaces as: + // - iUnmarshalValue + // - iUnmarshalText + // - iUnmarshalJSON + // Purpose: reduce the interface asserting cost in runtime. + IsCommonInterface bool + + // IsCustomConvert marks there custom converting function for this field type. + IsCustomConvert bool + + // StructField is the type info of this field. + StructField reflect.StructField + + // OtherSameNameFieldIndex holds the sub attributes of the same field name. + // For example: + // type Name struct{ + // LastName string + // FirstName string + // } + // type User struct{ + // Name + // LastName string + // FirstName string + // } + // + // As the `LastName` in `User`, its internal attributes: + // FieldIndexes = []int{0,1} + // // item length 1, as there's only one repeat item with the same field name. + // OtherSameNameFieldIndex = [][]int{[]int{1}} + // + // In value assignment, the value will be assigned to index {0,1} and {1}. + OtherSameNameFieldIndex [][]int + + // ConvertFunc is the converting function for this field. + ConvertFunc func(from any, to reflect.Value) + + // The last fuzzy matching key for this field. + // The fuzzy matching occurs only if there are no direct tag and field name matching in the params map. + // TODO If different paramsMaps contain paramKeys in different formats and all hit the same fieldName, + // the cached value may be continuously updated. + // LastFuzzyKey string. + LastFuzzyKey atomic.Value + + // removeSymbolsFieldName is used for quick fuzzy match for parameter key. + // removeSymbolsFieldName = utils.RemoveSymbols(fieldName) + RemoveSymbolsFieldName string +} + +// FieldName returns the field name of current field info. +func (cfi *CachedFieldInfo) FieldName() string { + return cfi.PriorityTagAndFieldName[len(cfi.PriorityTagAndFieldName)-1] +} + +// GetFieldReflectValue retrieves and returns the reflect.Value of given struct value, +// which is used for directly value assignment. +func (cfi *CachedFieldInfo) GetFieldReflectValue(structValue reflect.Value) reflect.Value { + if len(cfi.FieldIndexes) == 1 { + return structValue.Field(cfi.FieldIndexes[0]) + } + return cfi.fieldReflectValue(structValue, cfi.FieldIndexes) +} + +// GetOtherFieldReflectValue retrieves and returns the reflect.Value of given struct value with nested index +// by `fieldLevel`, which is used for directly value assignment. +func (cfi *CachedFieldInfo) GetOtherFieldReflectValue(structValue reflect.Value, fieldLevel int) reflect.Value { + fieldIndex := cfi.OtherSameNameFieldIndex[fieldLevel] + if len(fieldIndex) == 1 { + return structValue.Field(fieldIndex[0]) + } + return cfi.fieldReflectValue(structValue, fieldIndex) +} + +func (cfi *CachedFieldInfo) fieldReflectValue(v reflect.Value, fieldIndexes []int) reflect.Value { + for i, x := range fieldIndexes { + if i > 0 { + switch v.Kind() { + case reflect.Pointer: + if v.IsNil() { + v.Set(reflect.New(v.Type().Elem())) + } + v = v.Elem() + + case reflect.Interface: + // Compatible with previous code + // Interface => struct + v = v.Elem() + if v.Kind() == reflect.Ptr { + // maybe *struct or other types + v = v.Elem() + } + } + } + v = v.Field(x) + } + return v +} diff --git a/util/gconv/internal/structcache/structcache_cached_struct_info.go b/util/gconv/internal/structcache/structcache_cached_struct_info.go new file mode 100644 index 00000000000..4a56b206c2e --- /dev/null +++ b/util/gconv/internal/structcache/structcache_cached_struct_info.go @@ -0,0 +1,173 @@ +// 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 structcache + +import ( + "reflect" + "strings" + "time" + + "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/os/gtime" +) + +// CachedStructInfo holds the cached info for certain struct. +type CachedStructInfo struct { + // This map field is mainly used in the bindStructWithLoopParamsMap method + // key = field's name + // Will save all field names and PriorityTagAndFieldName + // for example: + // field string `json:"name"` + // + // It will be stored twice, which keys are `name` and `field`. + tagOrFiledNameToFieldInfoMap map[string]*CachedFieldInfo + + // All sub attributes field info slice. + FieldConvertInfos []*CachedFieldInfo +} + +func (csi *CachedStructInfo) HasNoFields() bool { + return len(csi.tagOrFiledNameToFieldInfoMap) == 0 +} + +func (csi *CachedStructInfo) GetFieldInfo(fieldName string) *CachedFieldInfo { + return csi.tagOrFiledNameToFieldInfoMap[fieldName] +} + +func (csi *CachedStructInfo) AddField(field reflect.StructField, fieldIndexes []int, priorityTags []string) { + alreadyExistFieldInfo, ok := csi.tagOrFiledNameToFieldInfoMap[field.Name] + if !ok { + priorityTagAndFieldName := csi.genPriorityTagAndFieldName(field, priorityTags) + newFieldInfoBase := &CachedFieldInfoBase{ + IsCommonInterface: checkTypeIsCommonInterface(field), + StructField: field, + FieldIndexes: fieldIndexes, + ConvertFunc: csi.genFieldConvertFunc(field.Type.String()), + IsCustomConvert: csi.checkTypeHasCustomConvert(field.Type), + PriorityTagAndFieldName: priorityTagAndFieldName, + RemoveSymbolsFieldName: utils.RemoveSymbols(field.Name), + } + newFieldInfoBase.LastFuzzyKey.Store(field.Name) + for _, tagOrFieldName := range priorityTagAndFieldName { + newFieldInfo := &CachedFieldInfo{ + CachedFieldInfoBase: newFieldInfoBase, + IsField: tagOrFieldName == field.Name, + } + csi.tagOrFiledNameToFieldInfoMap[tagOrFieldName] = newFieldInfo + if newFieldInfo.IsField { + csi.FieldConvertInfos = append(csi.FieldConvertInfos, newFieldInfo) + } + } + return + } + if alreadyExistFieldInfo.OtherSameNameFieldIndex == nil { + alreadyExistFieldInfo.OtherSameNameFieldIndex = make([][]int, 0, 2) + } + alreadyExistFieldInfo.OtherSameNameFieldIndex = append( + alreadyExistFieldInfo.OtherSameNameFieldIndex, + fieldIndexes, + ) + return +} + +func (csi *CachedStructInfo) genFieldConvertFunc(fieldType string) (convertFunc func(from any, to reflect.Value)) { + if fieldType[0] == '*' { + convertFunc = csi.genFieldConvertFunc(fieldType[1:]) + if convertFunc == nil { + return nil + } + return csi.genPtrConvertFunc(convertFunc) + } + switch fieldType { + case "int", "int8", "int16", "int32", "int64": + convertFunc = func(from any, to reflect.Value) { + to.SetInt(localCommonConverter.Int64(from)) + } + case "uint", "uint8", "uint16", "uint32", "uint64": + convertFunc = func(from any, to reflect.Value) { + to.SetUint(localCommonConverter.Uint64(from)) + } + case "string": + convertFunc = func(from any, to reflect.Value) { + to.SetString(localCommonConverter.String(from)) + } + case "float32": + convertFunc = func(from any, to reflect.Value) { + to.SetFloat(float64(localCommonConverter.Float32(from))) + } + case "float64": + convertFunc = func(from any, to reflect.Value) { + to.SetFloat(localCommonConverter.Float64(from)) + } + case "Time", "time.Time": + convertFunc = func(from any, to reflect.Value) { + *to.Addr().Interface().(*time.Time) = localCommonConverter.Time(from) + } + case "GTime", "gtime.Time": + convertFunc = func(from any, to reflect.Value) { + v := localCommonConverter.GTime(from) + if v == nil { + v = gtime.New() + } + *to.Addr().Interface().(*gtime.Time) = *v + } + case "bool": + convertFunc = func(from any, to reflect.Value) { + to.SetBool(localCommonConverter.Bool(from)) + } + case "[]byte": + convertFunc = func(from any, to reflect.Value) { + to.SetBytes(localCommonConverter.Bytes(from)) + } + default: + return nil + } + return convertFunc +} + +func (csi *CachedStructInfo) genPriorityTagAndFieldName( + field reflect.StructField, priorityTags []string, +) (priorityTagAndFieldName []string) { + for _, tag := range priorityTags { + value, ok := field.Tag.Lookup(tag) + if ok { + // If there's something else in the tag string, + // it uses the first part which is split using char ','. + // Example: + // orm:"id, priority" + // orm:"name, with:uid=id" + tagValueItems := strings.Split(value, ",") + // json:",omitempty" + trimmedTagName := strings.TrimSpace(tagValueItems[0]) + if trimmedTagName != "" { + priorityTagAndFieldName = append(priorityTagAndFieldName, trimmedTagName) + break + } + } + } + priorityTagAndFieldName = append(priorityTagAndFieldName, field.Name) + return +} + +func (csi *CachedStructInfo) checkTypeHasCustomConvert(fieldType reflect.Type) bool { + if fieldType.Kind() == reflect.Ptr { + fieldType = fieldType.Elem() + } + _, ok := customConvertTypeMap[fieldType] + return ok +} + +func (csi *CachedStructInfo) genPtrConvertFunc( + convertFunc func(from any, to reflect.Value), +) func(from any, to reflect.Value) { + return func(from any, to reflect.Value) { + if to.IsNil() { + to.Set(reflect.New(to.Type().Elem())) + } + convertFunc(from, to.Elem()) + } +} diff --git a/util/gconv/internal/structcache/structcache_pool.go b/util/gconv/internal/structcache/structcache_pool.go new file mode 100644 index 00000000000..30d4bca9ed8 --- /dev/null +++ b/util/gconv/internal/structcache/structcache_pool.go @@ -0,0 +1,33 @@ +// 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 structcache + +import ( + "sync" +) + +var ( + poolUsedParamsKeyOrTagNameMap = &sync.Pool{ + New: func() any { + return make(map[string]struct{}) + }, + } +) + +// GetUsedParamsKeyOrTagNameMapFromPool retrieves and returns a map for storing params key or tag name. +func GetUsedParamsKeyOrTagNameMapFromPool() map[string]struct{} { + return poolUsedParamsKeyOrTagNameMap.Get().(map[string]struct{}) +} + +// PutUsedParamsKeyOrTagNameMapToPool puts a map for storing params key or tag name into pool for re-usage. +func PutUsedParamsKeyOrTagNameMapToPool(m map[string]struct{}) { + // need to be cleared before putting back into pool. + for k := range m { + delete(m, k) + } + poolUsedParamsKeyOrTagNameMap.Put(m) +}