From 607f079b23fcba6685ade6df9bfe7fe2ffcb238d Mon Sep 17 00:00:00 2001 From: John Guo Date: Thu, 7 Mar 2024 11:36:42 +0800 Subject: [PATCH] fix: cache value assertion panic if the cache adapter is not in-memory for soft time feature of `package gdb`; improve converting performance for `gconv.Scan` (#3351) --- database/gdb/gdb.go | 2 + database/gdb/gdb_model_cache.go | 9 +- database/gdb/gdb_model_soft_time.go | 5 +- util/gconv/gconv_maptomap.go | 36 +- util/gconv/gconv_maptomaps.go | 41 +- util/gconv/gconv_scan.go | 597 ++++++-------------------- util/gconv/gconv_scan_list.go | 438 +++++++++++++++++++ util/gconv/gconv_struct.go | 134 ++---- util/gconv/gconv_structs.go | 56 +-- util/gconv/gconv_z_unit_issue_test.go | 34 ++ util/gconv/gconv_z_unit_scan_test.go | 55 ++- util/gconv/gconv_z_unit_time_test.go | 14 - util/gutil/gutil.go | 8 - util/gutil/gutil_is.go | 25 ++ util/gutil/gutil_z_unit_is_test.go | 46 ++ util/gutil/gutil_z_unit_test.go | 6 - 16 files changed, 797 insertions(+), 709 deletions(-) create mode 100644 util/gconv/gconv_scan_list.go create mode 100644 util/gutil/gutil_is.go create mode 100644 util/gutil/gutil_z_unit_is_test.go diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index d63c12ffe51..3212614d977 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -5,6 +5,8 @@ // You can obtain one at https://github.com/gogf/gf. // Package gdb provides ORM features for popular relationship databases. +// +// TODO use context.Context as required parameter for all DB operations. package gdb import ( diff --git a/database/gdb/gdb_model_cache.go b/database/gdb/gdb_model_cache.go index c3042e58875..793a9696d8b 100644 --- a/database/gdb/gdb_model_cache.go +++ b/database/gdb/gdb_model_cache.go @@ -11,7 +11,6 @@ import ( "time" "github.com/gogf/gf/v2/internal/intlog" - "github.com/gogf/gf/v2/internal/json" ) // CacheOption is options for model cache control in query. @@ -67,7 +66,6 @@ func (m *Model) getSelectResultFromCache(ctx context.Context, sql string, args . return } var ( - ok bool cacheItem *selectCacheItem cacheKey = m.makeSelectCacheKey(sql, args...) cacheObj = m.db.GetCache() @@ -82,12 +80,7 @@ func (m *Model) getSelectResultFromCache(ctx context.Context, sql string, args . } }() if v, _ := cacheObj.Get(ctx, cacheKey); !v.IsNil() { - if cacheItem, ok = v.Val().(*selectCacheItem); ok { - // In-memory cache. - return cacheItem.Result, nil - } - // Other cache, it needs conversion. - if err = json.UnmarshalUseNumber(v.Bytes(), &cacheItem); err != nil { + if err = v.Scan(&cacheItem); err != nil { return nil, err } return cacheItem.Result, nil diff --git a/database/gdb/gdb_model_soft_time.go b/database/gdb/gdb_model_soft_time.go index e9ac66a0974..ebe5953fe96 100644 --- a/database/gdb/gdb_model_soft_time.go +++ b/database/gdb/gdb_model_soft_time.go @@ -219,7 +219,10 @@ func (m *softTimeMaintainer) getSoftFieldNameAndType( intlog.Error(ctx, err) } if result != nil { - var cacheItem = result.Val().(getSoftFieldNameAndTypeCacheItem) + var cacheItem getSoftFieldNameAndTypeCacheItem + if err = result.Scan(&cacheItem); err != nil { + return "", "" + } fieldName = cacheItem.FieldName fieldType = cacheItem.FieldType } diff --git a/util/gconv/gconv_maptomap.go b/util/gconv/gconv_maptomap.go index d9b5322dce5..ffd352e582f 100644 --- a/util/gconv/gconv_maptomap.go +++ b/util/gconv/gconv_maptomap.go @@ -11,14 +11,13 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/internal/json" ) // MapToMap converts any map type variable `params` to another map type variable `pointer` // using reflect. // See doMapToMap. func MapToMap(params interface{}, pointer interface{}, mapping ...map[string]string) error { - return doMapToMap(params, pointer, mapping...) + return Scan(params, pointer, mapping...) } // doMapToMap converts any map type variable `params` to another map type variable `pointer`. @@ -32,29 +31,6 @@ func MapToMap(params interface{}, pointer interface{}, mapping ...map[string]str // The optional parameter `mapping` is used for struct attribute to map key mapping, which makes // sense only if the items of original map `params` is type struct. func doMapToMap(params interface{}, pointer interface{}, mapping ...map[string]string) (err error) { - // If given `params` is JSON, it then uses json.Unmarshal doing the converting. - switch r := params.(type) { - case []byte: - if json.Valid(r) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - return json.UnmarshalUseNumber(r, rv.Interface()) - } - } else { - return json.UnmarshalUseNumber(r, pointer) - } - } - case string: - if paramsBytes := []byte(r); json.Valid(paramsBytes) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - return json.UnmarshalUseNumber(paramsBytes, rv.Interface()) - } - } else { - return json.UnmarshalUseNumber(paramsBytes, pointer) - } - } - } var ( paramsRv reflect.Value paramsKind reflect.Kind @@ -92,7 +68,11 @@ func doMapToMap(params interface{}, pointer interface{}, mapping ...map[string]s pointerKind = pointerRv.Kind() } if pointerKind != reflect.Map { - return gerror.NewCodef(gcode.CodeInvalidParameter, "pointer should be type of *map, but got:%s", pointerKind) + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `destination pointer should be type of *map, but got: %s`, + pointerKind, + ) } defer func() { // Catch the panic, especially the reflection operation panics. @@ -119,7 +99,9 @@ func doMapToMap(params interface{}, pointer interface{}, mapping ...map[string]s mapValue := reflect.New(pointerValueType).Elem() switch pointerValueKind { case reflect.Map, reflect.Struct: - if err = doStruct(paramsRv.MapIndex(key).Interface(), mapValue, keyToAttributeNameMapping, ""); err != nil { + if err = doStruct( + paramsRv.MapIndex(key).Interface(), mapValue, keyToAttributeNameMapping, "", + ); err != nil { return err } default: diff --git a/util/gconv/gconv_maptomaps.go b/util/gconv/gconv_maptomaps.go index 63d19759d57..330ed40d045 100644 --- a/util/gconv/gconv_maptomaps.go +++ b/util/gconv/gconv_maptomaps.go @@ -11,13 +11,12 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/internal/json" ) // MapToMaps converts any slice type variable `params` to another map slice type variable `pointer`. // See doMapToMaps. func MapToMaps(params interface{}, pointer interface{}, mapping ...map[string]string) error { - return doMapToMaps(params, pointer, mapping...) + return Scan(params, pointer, mapping...) } // doMapToMaps converts any map type variable `params` to another map slice variable `pointer`. @@ -29,29 +28,6 @@ func MapToMaps(params interface{}, pointer interface{}, mapping ...map[string]st // The optional parameter `mapping` is used for struct attribute to map key mapping, which makes // sense only if the item of `params` is type struct. func doMapToMaps(params interface{}, pointer interface{}, paramKeyToAttrMap ...map[string]string) (err error) { - // If given `params` is JSON, it then uses json.Unmarshal doing the converting. - switch r := params.(type) { - case []byte: - if json.Valid(r) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - return json.UnmarshalUseNumber(r, rv.Interface()) - } - } else { - return json.UnmarshalUseNumber(r, pointer) - } - } - case string: - if paramsBytes := []byte(r); json.Valid(paramsBytes) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - return json.UnmarshalUseNumber(paramsBytes, rv.Interface()) - } - } else { - return json.UnmarshalUseNumber(paramsBytes, pointer) - } - } - } // Params and its element type check. var ( paramsRv reflect.Value @@ -68,7 +44,10 @@ func doMapToMaps(params interface{}, pointer interface{}, paramKeyToAttrMap ...m paramsKind = paramsRv.Kind() } if paramsKind != reflect.Array && paramsKind != reflect.Slice { - return gerror.NewCode(gcode.CodeInvalidParameter, "params should be type of slice, eg: []map/[]*map/[]struct/[]*struct") + return gerror.NewCode( + gcode.CodeInvalidParameter, + "params should be type of slice, example: []map/[]*map/[]struct/[]*struct", + ) } var ( paramsElem = paramsRv.Type().Elem() @@ -78,8 +57,14 @@ func doMapToMaps(params interface{}, pointer interface{}, paramKeyToAttrMap ...m paramsElem = paramsElem.Elem() paramsElemKind = paramsElem.Kind() } - if paramsElemKind != reflect.Map && paramsElemKind != reflect.Struct && paramsElemKind != reflect.Interface { - return gerror.NewCodef(gcode.CodeInvalidParameter, "params element should be type of map/*map/struct/*struct, but got: %s", paramsElemKind) + if paramsElemKind != reflect.Map && + paramsElemKind != reflect.Struct && + paramsElemKind != reflect.Interface { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + "params element should be type of map/*map/struct/*struct, but got: %s", + paramsElemKind, + ) } // Empty slice, no need continue. if paramsRv.Len() == 0 { diff --git a/util/gconv/gconv_scan.go b/util/gconv/gconv_scan.go index 4f286ad8963..6d22407d610 100644 --- a/util/gconv/gconv_scan.go +++ b/util/gconv/gconv_scan.go @@ -7,91 +7,98 @@ package gconv import ( - "database/sql" "reflect" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/internal/utils" - "github.com/gogf/gf/v2/os/gstructs" + "github.com/gogf/gf/v2/internal/json" ) -// Scan automatically checks the type of `pointer` and converts `params` to `pointer`. It supports `pointer` -// with type of `*map/*[]map/*[]*map/*struct/**struct/*[]struct/*[]*struct` for converting. +// Scan automatically checks the type of `pointer` and converts `params` to `pointer`. +// It supports `pointer` in type of `*map/*[]map/*[]*map/*struct/**struct/*[]struct/*[]*struct` for converting. // -// It calls function `doMapToMap` internally if `pointer` is type of *map for converting. -// It calls function `doMapToMaps` internally if `pointer` is type of *[]map/*[]*map for converting. -// It calls function `doStruct` internally if `pointer` is type of *struct/**struct for converting. -// It calls function `doStructs` internally if `pointer` is type of *[]struct/*[]*struct for converting. -func Scan(params interface{}, pointer interface{}, paramKeyToAttrMap ...map[string]string) (err error) { - var ( - pointerType reflect.Type - pointerKind reflect.Kind - pointerValue reflect.Value - ) - if v, ok := pointer.(reflect.Value); ok { - pointerValue = v - pointerType = v.Type() - } else { - pointerValue = reflect.ValueOf(pointer) - pointerType = reflect.TypeOf(pointer) // Do not use pointerValue.Type() as pointerValue might be zero. +// TODO change `paramKeyToAttrMap` to `ScanOption` to be more scalable; add `DeepCopy` option for `ScanOption`. +func Scan(srcValue interface{}, dstPointer interface{}, paramKeyToAttrMap ...map[string]string) (err error) { + if srcValue == nil { + // If `srcValue` is nil, no conversion. + return nil + } + if dstPointer == nil { + return gerror.NewCode( + gcode.CodeInvalidParameter, + `destination pointer should not be nil`, + ) } - if pointerType == nil { - return gerror.NewCode(gcode.CodeInvalidParameter, "parameter pointer should not be nil") + // json converting check. + ok, err := doConvertWithJsonCheck(srcValue, dstPointer) + if err != nil { + return err + } + if ok { + return nil } - pointerKind = pointerType.Kind() - if pointerKind != reflect.Ptr { - if pointerValue.CanAddr() { - pointerValue = pointerValue.Addr() - pointerType = pointerValue.Type() - pointerKind = pointerType.Kind() + + var ( + dstPointerReflectType reflect.Type + dstPointerReflectValue reflect.Value + ) + if v, ok := dstPointer.(reflect.Value); ok { + dstPointerReflectValue = v + dstPointerReflectType = v.Type() + } else { + dstPointerReflectValue = reflect.ValueOf(dstPointer) + // do not use dstPointerReflectValue.Type() as dstPointerReflectValue might be zero. + dstPointerReflectType = reflect.TypeOf(dstPointer) + } + + // pointer kind validation. + var dstPointerReflectKind = dstPointerReflectType.Kind() + if dstPointerReflectKind != reflect.Ptr { + if dstPointerReflectValue.CanAddr() { + dstPointerReflectValue = dstPointerReflectValue.Addr() + dstPointerReflectType = dstPointerReflectValue.Type() + dstPointerReflectKind = dstPointerReflectType.Kind() } else { return gerror.NewCodef( gcode.CodeInvalidParameter, - "params should be type of pointer, but got type: %v", - pointerType, + `destination pointer should be type of pointer, but got type: %v`, + dstPointerReflectType, ) } } - // Direct assignment checks! - var ( - paramsType reflect.Type - paramsValue reflect.Value - ) - if v, ok := params.(reflect.Value); ok { - paramsValue = v - paramsType = paramsValue.Type() + // direct assignment checks! + var srcValueReflectValue reflect.Value + if v, ok := srcValue.(reflect.Value); ok { + srcValueReflectValue = v } else { - paramsValue = reflect.ValueOf(params) - paramsType = reflect.TypeOf(params) // Do not use paramsValue.Type() as paramsValue might be zero. + srcValueReflectValue = reflect.ValueOf(srcValue) } - // If `params` and `pointer` are the same type, the do directly assignment. + // if `srcValue` and `dstPointer` are the same type, the do directly assignment. // For performance enhancement purpose. - var ( - pointerValueElem = pointerValue.Elem() - ) - if pointerValueElem.CanSet() && paramsType == pointerValueElem.Type() { - pointerValueElem.Set(paramsValue) + var dstPointerReflectValueElem = dstPointerReflectValue.Elem() + // if `srcValue` and `dstPointer` are the same type, the do directly assignment. + // for performance enhancement purpose. + if ok = doConvertWithTypeCheck(srcValueReflectValue, dstPointerReflectValueElem); ok { return nil } - // Converting. + // do the converting. var ( - pointerElem = pointerType.Elem() - pointerElemKind = pointerElem.Kind() - keyToAttributeNameMapping map[string]string + dstPointerReflectTypeElem = dstPointerReflectType.Elem() + dstPointerReflectTypeElemKind = dstPointerReflectTypeElem.Kind() + keyToAttributeNameMapping map[string]string ) if len(paramKeyToAttrMap) > 0 { keyToAttributeNameMapping = paramKeyToAttrMap[0] } - switch pointerElemKind { + switch dstPointerReflectTypeElemKind { case reflect.Map: - return doMapToMap(params, pointer, paramKeyToAttrMap...) + return doMapToMap(srcValue, dstPointer, paramKeyToAttrMap...) case reflect.Array, reflect.Slice: var ( - sliceElem = pointerElem.Elem() + sliceElem = dstPointerReflectTypeElem.Elem() sliceElemKind = sliceElem.Kind() ) for sliceElemKind == reflect.Ptr { @@ -99,432 +106,100 @@ func Scan(params interface{}, pointer interface{}, paramKeyToAttrMap ...map[stri sliceElemKind = sliceElem.Kind() } if sliceElemKind == reflect.Map { - return doMapToMaps(params, pointer, paramKeyToAttrMap...) + return doMapToMaps(srcValue, dstPointer, paramKeyToAttrMap...) } - return doStructs(params, pointer, keyToAttributeNameMapping, "") + return doStructs(srcValue, dstPointer, keyToAttributeNameMapping, "") default: - return doStruct(params, pointer, keyToAttributeNameMapping, "") - } -} - -// ScanList converts `structSlice` to struct slice which contains other complex struct attributes. -// Note that the parameter `structSlicePointer` should be type of *[]struct/*[]*struct. -// -// Usage example 1: Normal attribute struct relation: -// -// type EntityUser struct { -// Uid int -// Name string -// } -// -// type EntityUserDetail struct { -// Uid int -// Address string -// } -// -// type EntityUserScores struct { -// Id int -// Uid int -// Score int -// Course string -// } -// -// type Entity struct { -// User *EntityUser -// UserDetail *EntityUserDetail -// UserScores []*EntityUserScores -// } -// -// var users []*Entity -// var userRecords = EntityUser{Uid: 1, Name:"john"} -// var detailRecords = EntityUser{Uid: 1, Address: "chengdu"} -// var scoresRecords = EntityUser{Id: 1, Uid: 1, Score: 100, Course: "math"} -// ScanList(userRecords, &users, "User") -// ScanList(userRecords, &users, "User", "uid") -// ScanList(detailRecords, &users, "UserDetail", "User", "uid:Uid") -// ScanList(scoresRecords, &users, "UserScores", "User", "uid:Uid") -// ScanList(scoresRecords, &users, "UserScores", "User", "uid") -// -// Usage example 2: Embedded attribute struct relation: -// -// type EntityUser struct { -// Uid int -// Name string -// } -// -// type EntityUserDetail struct { -// Uid int -// Address string -// } -// -// type EntityUserScores struct { -// Id int -// Uid int -// Score int -// } -// -// type Entity struct { -// EntityUser -// UserDetail EntityUserDetail -// UserScores []EntityUserScores -// } -// -// var userRecords = EntityUser{Uid: 1, Name:"john"} -// var detailRecords = EntityUser{Uid: 1, Address: "chengdu"} -// var scoresRecords = EntityUser{Id: 1, Uid: 1, Score: 100, Course: "math"} -// ScanList(userRecords, &users) -// ScanList(detailRecords, &users, "UserDetail", "uid") -// ScanList(scoresRecords, &users, "UserScores", "uid") -// -// The parameters "User/UserDetail/UserScores" in the example codes specify the target attribute struct -// that current result will be bound to. -// -// The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational -// struct attribute name - not the attribute name of the bound to target. In the example codes, it's attribute -// name "Uid" of "User" of entity "Entity". It automatically calculates the HasOne/HasMany relationship with -// given `relation` parameter. -// -// See the example or unit testing cases for clear understanding for this function. -func ScanList(structSlice interface{}, structSlicePointer interface{}, bindToAttrName string, relationAttrNameAndFields ...string) (err error) { - var ( - relationAttrName string - relationFields string - ) - switch len(relationAttrNameAndFields) { - case 2: - relationAttrName = relationAttrNameAndFields[0] - relationFields = relationAttrNameAndFields[1] - case 1: - relationFields = relationAttrNameAndFields[0] + return doStruct(srcValue, dstPointer, keyToAttributeNameMapping, "") } - return doScanList(structSlice, structSlicePointer, bindToAttrName, relationAttrName, relationFields) } -// doScanList converts `structSlice` to struct slice which contains other complex struct attributes recursively. -// Note that the parameter `structSlicePointer` should be type of *[]struct/*[]*struct. -func doScanList( - structSlice interface{}, structSlicePointer interface{}, bindToAttrName, relationAttrName, relationFields string, -) (err error) { - var ( - maps = Maps(structSlice) - ) - if len(maps) == 0 { - return nil - } - // Necessary checks for parameters. - if bindToAttrName == "" { - return gerror.NewCode(gcode.CodeInvalidParameter, `bindToAttrName should not be empty`) - } - - if relationAttrName == "." { - relationAttrName = "" - } - - var ( - reflectValue = reflect.ValueOf(structSlicePointer) - reflectKind = reflectValue.Kind() - ) - if reflectKind == reflect.Interface { - reflectValue = reflectValue.Elem() - reflectKind = reflectValue.Kind() - } - if reflectKind != reflect.Ptr { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - "structSlicePointer should be type of *[]struct/*[]*struct, but got: %v", - reflectKind, - ) - } - reflectValue = reflectValue.Elem() - reflectKind = reflectValue.Kind() - if reflectKind != reflect.Slice && reflectKind != reflect.Array { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - "structSlicePointer should be type of *[]struct/*[]*struct, but got: %v", - reflectKind, - ) - } - length := len(maps) - if length == 0 { - // The pointed slice is not empty. - if reflectValue.Len() > 0 { - // It here checks if it has struct item, which is already initialized. - // It then returns error to warn the developer its empty and no conversion. - if v := reflectValue.Index(0); v.Kind() != reflect.Ptr { - return sql.ErrNoRows - } - } - // Do nothing for empty struct slice. - return nil - } - var ( - arrayValue reflect.Value // Like: []*Entity - arrayItemType reflect.Type // Like: *Entity - reflectType = reflect.TypeOf(structSlicePointer) - ) - if reflectValue.Len() > 0 { - arrayValue = reflectValue - } else { - arrayValue = reflect.MakeSlice(reflectType.Elem(), length, length) - } +func doConvertWithTypeCheck(srcValueReflectValue, dstPointerReflectValueElem reflect.Value) (ok bool) { + if !dstPointerReflectValueElem.IsValid() || !srcValueReflectValue.IsValid() { + return false + } + switch { + // Example: + // UploadFile => UploadFile + // []UploadFile => []UploadFile + // *UploadFile => *UploadFile + // *[]UploadFile => *[]UploadFile + // map => map + // []map => []map + // *[]map => *[]map + case dstPointerReflectValueElem.Type() == srcValueReflectValue.Type(): + dstPointerReflectValueElem.Set(srcValueReflectValue) + return true + + // Example: + // UploadFile => *UploadFile + // []UploadFile => *[]UploadFile + // map => *map + // []map => *[]map + case dstPointerReflectValueElem.Kind() == reflect.Ptr && + dstPointerReflectValueElem.Elem().IsValid() && + dstPointerReflectValueElem.Elem().Type() == srcValueReflectValue.Type(): + dstPointerReflectValueElem.Elem().Set(srcValueReflectValue) + return true + + // Example: + // *UploadFile => UploadFile + // *[]UploadFile => []UploadFile + // *map => map + // *[]map => []map + case srcValueReflectValue.Kind() == reflect.Ptr && + srcValueReflectValue.Elem().IsValid() && + dstPointerReflectValueElem.Type() == srcValueReflectValue.Elem().Type(): + dstPointerReflectValueElem.Set(srcValueReflectValue.Elem()) + return true - // Slice element item. - arrayItemType = arrayValue.Index(0).Type() - - // Relation variables. - var ( - relationDataMap map[string]interface{} - relationFromFieldName string // Eg: relationKV: id:uid -> id - relationBindToFieldName string // Eg: relationKV: id:uid -> uid - ) - if len(relationFields) > 0 { - // The relation key string of table field name and attribute name - // can be joined with char '=' or ':'. - array := utils.SplitAndTrim(relationFields, "=") - if len(array) == 1 { - // Compatible with old splitting char ':'. - array = utils.SplitAndTrim(relationFields, ":") - } - if len(array) == 1 { - // The relation names are the same. - array = []string{relationFields, relationFields} - } - if len(array) == 2 { - // Defined table field to relation attribute name. - // Like: - // uid:Uid - // uid:UserId - relationFromFieldName = array[0] - relationBindToFieldName = array[1] - if key, _ := utils.MapPossibleItemByKey(maps[0], relationFromFieldName); key == "" { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - `cannot find possible related table field name "%s" from given relation fields "%s"`, - relationFromFieldName, - relationFields, - ) - } else { - relationFromFieldName = key - } - } else { - return gerror.NewCode( - gcode.CodeInvalidParameter, - `parameter relationKV should be format of "ResultFieldName:BindToAttrName"`, - ) - } - if relationFromFieldName != "" { - // Note that the value might be type of slice. - relationDataMap = utils.ListToMapByKey(maps, relationFromFieldName) - } - if len(relationDataMap) == 0 { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - `cannot find the relation data map, maybe invalid relation fields given "%v"`, - relationFields, - ) - } - } - // Bind to target attribute. - var ( - ok bool - bindToAttrValue reflect.Value - bindToAttrKind reflect.Kind - bindToAttrType reflect.Type - bindToAttrField reflect.StructField - ) - if arrayItemType.Kind() == reflect.Ptr { - if bindToAttrField, ok = arrayItemType.Elem().FieldByName(bindToAttrName); !ok { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - `invalid parameter bindToAttrName: cannot find attribute with name "%s" from slice element`, - bindToAttrName, - ) - } - } else { - if bindToAttrField, ok = arrayItemType.FieldByName(bindToAttrName); !ok { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - `invalid parameter bindToAttrName: cannot find attribute with name "%s" from slice element`, - bindToAttrName, - ) - } + default: + return false } - bindToAttrType = bindToAttrField.Type - bindToAttrKind = bindToAttrType.Kind() - - // Bind to relation conditions. - var ( - relationFromAttrValue reflect.Value - relationFromAttrField reflect.Value - relationBindToFieldNameChecked bool - ) - for i := 0; i < arrayValue.Len(); i++ { - arrayElemValue := arrayValue.Index(i) - // The FieldByName should be called on non-pointer reflect.Value. - if arrayElemValue.Kind() == reflect.Ptr { - // Like: []*Entity - arrayElemValue = arrayElemValue.Elem() - if !arrayElemValue.IsValid() { - // The element is nil, then create one and set it to the slice. - // The "reflect.New(itemType.Elem())" creates a new element and returns the address of it. - // For example: - // reflect.New(itemType.Elem()) => *Entity - // reflect.New(itemType.Elem()).Elem() => Entity - arrayElemValue = reflect.New(arrayItemType.Elem()).Elem() - arrayValue.Index(i).Set(arrayElemValue.Addr()) - } - } else { - // Like: []Entity - } - bindToAttrValue = arrayElemValue.FieldByName(bindToAttrName) - if relationAttrName != "" { - // Attribute value of current slice element. - relationFromAttrValue = arrayElemValue.FieldByName(relationAttrName) - if relationFromAttrValue.Kind() == reflect.Ptr { - relationFromAttrValue = relationFromAttrValue.Elem() - } - } else { - // Current slice element. - relationFromAttrValue = arrayElemValue - } - if len(relationDataMap) > 0 && !relationFromAttrValue.IsValid() { - return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) - } - // Check and find possible bind to attribute name. - if relationFields != "" && !relationBindToFieldNameChecked { - relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) - if !relationFromAttrField.IsValid() { - var ( - fieldMap, _ = gstructs.FieldMap(gstructs.FieldMapInput{ - Pointer: relationFromAttrValue, - RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, - }) - ) - if key, _ := utils.MapPossibleItemByKey(Map(fieldMap), relationBindToFieldName); key == "" { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - `cannot find possible related attribute name "%s" from given relation fields "%s"`, - relationBindToFieldName, - relationFields, - ) - } else { - relationBindToFieldName = key - } - } - relationBindToFieldNameChecked = true - } - switch bindToAttrKind { - case reflect.Array, reflect.Slice: - if len(relationDataMap) > 0 { - relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) - if relationFromAttrField.IsValid() { - // results := make(Result, 0) - results := make([]interface{}, 0) - for _, v := range SliceAny(relationDataMap[String(relationFromAttrField.Interface())]) { - item := v - results = append(results, item) - } - if err = Structs(results, bindToAttrValue.Addr()); err != nil { - return err - } - } else { - // Maybe the attribute does not exist yet. - return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) - } - } else { - return gerror.NewCodef( - gcode.CodeInvalidParameter, - `relationKey should not be empty as field "%s" is slice`, - bindToAttrName, - ) - } +} - case reflect.Ptr: - var element reflect.Value - if bindToAttrValue.IsNil() { - element = reflect.New(bindToAttrType.Elem()).Elem() - } else { - element = bindToAttrValue.Elem() - } - if len(relationDataMap) > 0 { - relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) - if relationFromAttrField.IsValid() { - v := relationDataMap[String(relationFromAttrField.Interface())] - if v == nil { - // There's no relational data. - continue +// doConvertWithJsonCheck does json converting check. +// If given `params` is JSON, it then uses json.Unmarshal doing the converting. +func doConvertWithJsonCheck(srcValue interface{}, dstPointer interface{}) (ok bool, err error) { + switch valueResult := srcValue.(type) { + case []byte: + if json.Valid(valueResult) { + if dstPointerReflectType, ok := dstPointer.(reflect.Value); ok { + if dstPointerReflectType.Kind() == reflect.Ptr { + if dstPointerReflectType.IsNil() { + return false, nil } - if utils.IsSlice(v) { - if err = Struct(SliceAny(v)[0], element); err != nil { - return err - } - } else { - if err = Struct(v, element); err != nil { - return err - } - } - } else { - // Maybe the attribute does not exist yet. - return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) + return true, json.UnmarshalUseNumber(valueResult, dstPointerReflectType.Interface()) + } else if dstPointerReflectType.CanAddr() { + return true, json.UnmarshalUseNumber(valueResult, dstPointerReflectType.Addr().Interface()) } } else { - if i >= len(maps) { - // There's no relational data. - continue - } - v := maps[i] - if v == nil { - // There's no relational data. - continue - } - if err = Struct(v, element); err != nil { - return err - } + return true, json.UnmarshalUseNumber(valueResult, dstPointer) } - bindToAttrValue.Set(element.Addr()) + } - case reflect.Struct: - if len(relationDataMap) > 0 { - relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) - if relationFromAttrField.IsValid() { - relationDataItem := relationDataMap[String(relationFromAttrField.Interface())] - if relationDataItem == nil { - // There's no relational data. - continue - } - if utils.IsSlice(relationDataItem) { - if err = Struct(SliceAny(relationDataItem)[0], bindToAttrValue); err != nil { - return err - } - } else { - if err = Struct(relationDataItem, bindToAttrValue); err != nil { - return err - } + case string: + if valueBytes := []byte(valueResult); json.Valid(valueBytes) { + if dstPointerReflectType, ok := dstPointer.(reflect.Value); ok { + if dstPointerReflectType.Kind() == reflect.Ptr { + if dstPointerReflectType.IsNil() { + return false, nil } - } else { - // Maybe the attribute does not exist yet. - return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) + return true, json.UnmarshalUseNumber(valueBytes, dstPointerReflectType.Interface()) + } else if dstPointerReflectType.CanAddr() { + return true, json.UnmarshalUseNumber(valueBytes, dstPointerReflectType.Addr().Interface()) } } else { - if i >= len(maps) { - // There's no relational data. - continue - } - relationDataItem := maps[i] - if relationDataItem == nil { - // There's no relational data. - continue - } - if err = Struct(relationDataItem, bindToAttrValue); err != nil { - return err - } + return true, json.UnmarshalUseNumber(valueBytes, dstPointer) } + } - default: - return gerror.NewCodef(gcode.CodeInvalidParameter, `unsupported attribute type: %s`, bindToAttrKind.String()) + default: + // The `params` might be struct that implements interface function Interface, eg: gvar.Var. + if v, ok := srcValue.(iInterface); ok { + return doConvertWithJsonCheck(v.Interface(), dstPointer) } } - reflect.ValueOf(structSlicePointer).Elem().Set(arrayValue) - return nil + return false, nil } diff --git a/util/gconv/gconv_scan_list.go b/util/gconv/gconv_scan_list.go new file mode 100644 index 00000000000..2b69f209b59 --- /dev/null +++ b/util/gconv/gconv_scan_list.go @@ -0,0 +1,438 @@ +// 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 ( + "database/sql" + "reflect" + + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/utils" + "github.com/gogf/gf/v2/os/gstructs" +) + +// ScanList converts `structSlice` to struct slice which contains other complex struct attributes. +// Note that the parameter `structSlicePointer` should be type of *[]struct/*[]*struct. +// +// Usage example 1: Normal attribute struct relation: +// +// type EntityUser struct { +// Uid int +// Name string +// } +// +// type EntityUserDetail struct { +// Uid int +// Address string +// } +// +// type EntityUserScores struct { +// Id int +// Uid int +// Score int +// Course string +// } +// +// type Entity struct { +// User *EntityUser +// UserDetail *EntityUserDetail +// UserScores []*EntityUserScores +// } +// +// var users []*Entity +// var userRecords = EntityUser{Uid: 1, Name:"john"} +// var detailRecords = EntityUser{Uid: 1, Address: "chengdu"} +// var scoresRecords = EntityUser{Id: 1, Uid: 1, Score: 100, Course: "math"} +// ScanList(userRecords, &users, "User") +// ScanList(userRecords, &users, "User", "uid") +// ScanList(detailRecords, &users, "UserDetail", "User", "uid:Uid") +// ScanList(scoresRecords, &users, "UserScores", "User", "uid:Uid") +// ScanList(scoresRecords, &users, "UserScores", "User", "uid") +// +// Usage example 2: Embedded attribute struct relation: +// +// type EntityUser struct { +// Uid int +// Name string +// } +// +// type EntityUserDetail struct { +// Uid int +// Address string +// } +// +// type EntityUserScores struct { +// Id int +// Uid int +// Score int +// } +// +// type Entity struct { +// EntityUser +// UserDetail EntityUserDetail +// UserScores []EntityUserScores +// } +// +// var userRecords = EntityUser{Uid: 1, Name:"john"} +// var detailRecords = EntityUser{Uid: 1, Address: "chengdu"} +// var scoresRecords = EntityUser{Id: 1, Uid: 1, Score: 100, Course: "math"} +// ScanList(userRecords, &users) +// ScanList(detailRecords, &users, "UserDetail", "uid") +// ScanList(scoresRecords, &users, "UserScores", "uid") +// +// The parameters "User/UserDetail/UserScores" in the example codes specify the target attribute struct +// that current result will be bound to. +// +// The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational +// struct attribute name - not the attribute name of the bound to target. In the example codes, it's attribute +// name "Uid" of "User" of entity "Entity". It automatically calculates the HasOne/HasMany relationship with +// given `relation` parameter. +// +// See the example or unit testing cases for clear understanding for this function. +func ScanList(structSlice interface{}, structSlicePointer interface{}, bindToAttrName string, relationAttrNameAndFields ...string) (err error) { + var ( + relationAttrName string + relationFields string + ) + switch len(relationAttrNameAndFields) { + case 2: + relationAttrName = relationAttrNameAndFields[0] + relationFields = relationAttrNameAndFields[1] + case 1: + relationFields = relationAttrNameAndFields[0] + } + return doScanList(structSlice, structSlicePointer, bindToAttrName, relationAttrName, relationFields) +} + +// doScanList converts `structSlice` to struct slice which contains other complex struct attributes recursively. +// Note that the parameter `structSlicePointer` should be type of *[]struct/*[]*struct. +func doScanList( + structSlice interface{}, structSlicePointer interface{}, bindToAttrName, relationAttrName, relationFields string, +) (err error) { + var ( + maps = Maps(structSlice) + ) + if len(maps) == 0 { + return nil + } + // Necessary checks for parameters. + if bindToAttrName == "" { + return gerror.NewCode(gcode.CodeInvalidParameter, `bindToAttrName should not be empty`) + } + + if relationAttrName == "." { + relationAttrName = "" + } + + var ( + reflectValue = reflect.ValueOf(structSlicePointer) + reflectKind = reflectValue.Kind() + ) + if reflectKind == reflect.Interface { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + if reflectKind != reflect.Ptr { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + "structSlicePointer should be type of *[]struct/*[]*struct, but got: %v", + reflectKind, + ) + } + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + if reflectKind != reflect.Slice && reflectKind != reflect.Array { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + "structSlicePointer should be type of *[]struct/*[]*struct, but got: %v", + reflectKind, + ) + } + length := len(maps) + if length == 0 { + // The pointed slice is not empty. + if reflectValue.Len() > 0 { + // It here checks if it has struct item, which is already initialized. + // It then returns error to warn the developer its empty and no conversion. + if v := reflectValue.Index(0); v.Kind() != reflect.Ptr { + return sql.ErrNoRows + } + } + // Do nothing for empty struct slice. + return nil + } + var ( + arrayValue reflect.Value // Like: []*Entity + arrayItemType reflect.Type // Like: *Entity + reflectType = reflect.TypeOf(structSlicePointer) + ) + if reflectValue.Len() > 0 { + arrayValue = reflectValue + } else { + arrayValue = reflect.MakeSlice(reflectType.Elem(), length, length) + } + + // Slice element item. + arrayItemType = arrayValue.Index(0).Type() + + // Relation variables. + var ( + relationDataMap map[string]interface{} + relationFromFieldName string // Eg: relationKV: id:uid -> id + relationBindToFieldName string // Eg: relationKV: id:uid -> uid + ) + if len(relationFields) > 0 { + // The relation key string of table field name and attribute name + // can be joined with char '=' or ':'. + array := utils.SplitAndTrim(relationFields, "=") + if len(array) == 1 { + // Compatible with old splitting char ':'. + array = utils.SplitAndTrim(relationFields, ":") + } + if len(array) == 1 { + // The relation names are the same. + array = []string{relationFields, relationFields} + } + if len(array) == 2 { + // Defined table field to relation attribute name. + // Like: + // uid:Uid + // uid:UserId + relationFromFieldName = array[0] + relationBindToFieldName = array[1] + if key, _ := utils.MapPossibleItemByKey(maps[0], relationFromFieldName); key == "" { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `cannot find possible related table field name "%s" from given relation fields "%s"`, + relationFromFieldName, + relationFields, + ) + } else { + relationFromFieldName = key + } + } else { + return gerror.NewCode( + gcode.CodeInvalidParameter, + `parameter relationKV should be format of "ResultFieldName:BindToAttrName"`, + ) + } + if relationFromFieldName != "" { + // Note that the value might be type of slice. + relationDataMap = utils.ListToMapByKey(maps, relationFromFieldName) + } + if len(relationDataMap) == 0 { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `cannot find the relation data map, maybe invalid relation fields given "%v"`, + relationFields, + ) + } + } + // Bind to target attribute. + var ( + ok bool + bindToAttrValue reflect.Value + bindToAttrKind reflect.Kind + bindToAttrType reflect.Type + bindToAttrField reflect.StructField + ) + if arrayItemType.Kind() == reflect.Ptr { + if bindToAttrField, ok = arrayItemType.Elem().FieldByName(bindToAttrName); !ok { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `invalid parameter bindToAttrName: cannot find attribute with name "%s" from slice element`, + bindToAttrName, + ) + } + } else { + if bindToAttrField, ok = arrayItemType.FieldByName(bindToAttrName); !ok { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `invalid parameter bindToAttrName: cannot find attribute with name "%s" from slice element`, + bindToAttrName, + ) + } + } + bindToAttrType = bindToAttrField.Type + bindToAttrKind = bindToAttrType.Kind() + + // Bind to relation conditions. + var ( + relationFromAttrValue reflect.Value + relationFromAttrField reflect.Value + relationBindToFieldNameChecked bool + ) + for i := 0; i < arrayValue.Len(); i++ { + arrayElemValue := arrayValue.Index(i) + // The FieldByName should be called on non-pointer reflect.Value. + if arrayElemValue.Kind() == reflect.Ptr { + // Like: []*Entity + arrayElemValue = arrayElemValue.Elem() + if !arrayElemValue.IsValid() { + // The element is nil, then create one and set it to the slice. + // The "reflect.New(itemType.Elem())" creates a new element and returns the address of it. + // For example: + // reflect.New(itemType.Elem()) => *Entity + // reflect.New(itemType.Elem()).Elem() => Entity + arrayElemValue = reflect.New(arrayItemType.Elem()).Elem() + arrayValue.Index(i).Set(arrayElemValue.Addr()) + } + } else { + // Like: []Entity + } + bindToAttrValue = arrayElemValue.FieldByName(bindToAttrName) + if relationAttrName != "" { + // Attribute value of current slice element. + relationFromAttrValue = arrayElemValue.FieldByName(relationAttrName) + if relationFromAttrValue.Kind() == reflect.Ptr { + relationFromAttrValue = relationFromAttrValue.Elem() + } + } else { + // Current slice element. + relationFromAttrValue = arrayElemValue + } + if len(relationDataMap) > 0 && !relationFromAttrValue.IsValid() { + return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) + } + // Check and find possible bind to attribute name. + if relationFields != "" && !relationBindToFieldNameChecked { + relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) + if !relationFromAttrField.IsValid() { + var ( + fieldMap, _ = gstructs.FieldMap(gstructs.FieldMapInput{ + Pointer: relationFromAttrValue, + RecursiveOption: gstructs.RecursiveOptionEmbeddedNoTag, + }) + ) + if key, _ := utils.MapPossibleItemByKey(Map(fieldMap), relationBindToFieldName); key == "" { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `cannot find possible related attribute name "%s" from given relation fields "%s"`, + relationBindToFieldName, + relationFields, + ) + } else { + relationBindToFieldName = key + } + } + relationBindToFieldNameChecked = true + } + switch bindToAttrKind { + case reflect.Array, reflect.Slice: + if len(relationDataMap) > 0 { + relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) + if relationFromAttrField.IsValid() { + // results := make(Result, 0) + results := make([]interface{}, 0) + for _, v := range SliceAny(relationDataMap[String(relationFromAttrField.Interface())]) { + item := v + results = append(results, item) + } + if err = Structs(results, bindToAttrValue.Addr()); err != nil { + return err + } + } else { + // Maybe the attribute does not exist yet. + return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) + } + } else { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + `relationKey should not be empty as field "%s" is slice`, + bindToAttrName, + ) + } + + case reflect.Ptr: + var element reflect.Value + if bindToAttrValue.IsNil() { + element = reflect.New(bindToAttrType.Elem()).Elem() + } else { + element = bindToAttrValue.Elem() + } + if len(relationDataMap) > 0 { + relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) + if relationFromAttrField.IsValid() { + v := relationDataMap[String(relationFromAttrField.Interface())] + if v == nil { + // There's no relational data. + continue + } + if utils.IsSlice(v) { + if err = Struct(SliceAny(v)[0], element); err != nil { + return err + } + } else { + if err = Struct(v, element); err != nil { + return err + } + } + } else { + // Maybe the attribute does not exist yet. + return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) + } + } else { + if i >= len(maps) { + // There's no relational data. + continue + } + v := maps[i] + if v == nil { + // There's no relational data. + continue + } + if err = Struct(v, element); err != nil { + return err + } + } + bindToAttrValue.Set(element.Addr()) + + case reflect.Struct: + if len(relationDataMap) > 0 { + relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) + if relationFromAttrField.IsValid() { + relationDataItem := relationDataMap[String(relationFromAttrField.Interface())] + if relationDataItem == nil { + // There's no relational data. + continue + } + if utils.IsSlice(relationDataItem) { + if err = Struct(SliceAny(relationDataItem)[0], bindToAttrValue); err != nil { + return err + } + } else { + if err = Struct(relationDataItem, bindToAttrValue); err != nil { + return err + } + } + } else { + // Maybe the attribute does not exist yet. + return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) + } + } else { + if i >= len(maps) { + // There's no relational data. + continue + } + relationDataItem := maps[i] + if relationDataItem == nil { + // There's no relational data. + continue + } + if err = Struct(relationDataItem, bindToAttrValue); err != nil { + return err + } + } + + default: + return gerror.NewCodef(gcode.CodeInvalidParameter, `unsupported attribute type: %s`, bindToAttrKind.String()) + } + } + reflect.ValueOf(structSlicePointer).Elem().Set(arrayValue) + return nil +} diff --git a/util/gconv/gconv_struct.go b/util/gconv/gconv_struct.go index ed29b815e81..adb4533758b 100644 --- a/util/gconv/gconv_struct.go +++ b/util/gconv/gconv_struct.go @@ -43,50 +43,10 @@ func StructTag(params interface{}, pointer interface{}, priorityTag string) (err return doStruct(params, pointer, nil, priorityTag) } -// doStructWithJsonCheck checks if given `params` is JSON, it then uses json.Unmarshal doing the converting. -func doStructWithJsonCheck(params interface{}, pointer interface{}) (err error, ok bool) { - switch r := params.(type) { - case []byte: - if json.Valid(r) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - if rv.IsNil() { - return nil, false - } - return json.UnmarshalUseNumber(r, rv.Interface()), true - } else if rv.CanAddr() { - return json.UnmarshalUseNumber(r, rv.Addr().Interface()), true - } - } else { - return json.UnmarshalUseNumber(r, pointer), true - } - } - case string: - if paramsBytes := []byte(r); json.Valid(paramsBytes) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - if rv.IsNil() { - return nil, false - } - return json.UnmarshalUseNumber(paramsBytes, rv.Interface()), true - } else if rv.CanAddr() { - return json.UnmarshalUseNumber(paramsBytes, rv.Addr().Interface()), true - } - } else { - return json.UnmarshalUseNumber(paramsBytes, pointer), true - } - } - default: - // The `params` might be struct that implements interface function Interface, eg: gvar.Var. - if v, ok := params.(iInterface); ok { - return doStructWithJsonCheck(v.Interface(), pointer) - } - } - return nil, false -} - // doStruct is the core internal converting function for any data to struct. -func doStruct(params interface{}, pointer interface{}, paramKeyToAttrMap map[string]string, priorityTag string) (err error) { +func doStruct( + params interface{}, pointer interface{}, paramKeyToAttrMap map[string]string, priorityTag string, +) (err error) { if params == nil { // If `params` is nil, no conversion. return nil @@ -95,6 +55,15 @@ func doStruct(params interface{}, pointer interface{}, paramKeyToAttrMap map[str return gerror.NewCode(gcode.CodeInvalidParameter, "object pointer cannot be nil") } + // JSON content converting. + ok, err := doConvertWithJsonCheck(params, pointer) + if err != nil { + return err + } + if ok { + return nil + } + defer func() { // Catch the panic, especially the reflection operation panics. if exception := recover(); exception != nil { @@ -106,15 +75,6 @@ func doStruct(params interface{}, pointer interface{}, paramKeyToAttrMap map[str } }() - // JSON content converting. - err, ok := doStructWithJsonCheck(params, pointer) - if err != nil { - return err - } - if ok { - return nil - } - var ( paramsReflectValue reflect.Value paramsInterface interface{} // DO NOT use `params` directly as it might be type `reflect.Value` @@ -135,49 +95,35 @@ func doStruct(params interface{}, pointer interface{}, paramKeyToAttrMap map[str pointerReflectValue = reflect.ValueOf(pointer) pointerReflectKind = pointerReflectValue.Kind() if pointerReflectKind != reflect.Ptr { - return gerror.NewCodef(gcode.CodeInvalidParameter, "object pointer should be type of '*struct', but got '%v'", pointerReflectKind) + return gerror.NewCodef( + gcode.CodeInvalidParameter, + "destination pointer should be type of '*struct', but got '%v'", + pointerReflectKind, + ) } // Using IsNil on reflect.Ptr variable is OK. if !pointerReflectValue.IsValid() || pointerReflectValue.IsNil() { - return gerror.NewCode(gcode.CodeInvalidParameter, "object pointer cannot be nil") + return gerror.NewCode( + gcode.CodeInvalidParameter, + "destination pointer cannot be nil", + ) } 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() { - switch { - // Eg: - // UploadFile => UploadFile - // *UploadFile => *UploadFile - case pointerElemReflectValue.Type() == paramsReflectValue.Type(): - pointerElemReflectValue.Set(paramsReflectValue) - return nil - - // Eg: - // UploadFile => *UploadFile - case pointerElemReflectValue.Kind() == reflect.Ptr && pointerElemReflectValue.Elem().IsValid() && - pointerElemReflectValue.Elem().Type() == paramsReflectValue.Type(): - pointerElemReflectValue.Elem().Set(paramsReflectValue) - return nil + if ok = doConvertWithTypeCheck(paramsReflectValue, pointerElemReflectValue); ok { + return nil + } - // Eg: - // *UploadFile => UploadFile - case paramsReflectValue.Kind() == reflect.Ptr && paramsReflectValue.Elem().IsValid() && - pointerElemReflectValue.Type() == paramsReflectValue.Elem().Type(): - pointerElemReflectValue.Set(paramsReflectValue.Elem()) - return nil - } + // custom convert. + if ok, err = callCustomConverter(paramsReflectValue, pointerReflectValue); ok { + return err } // Normal unmarshalling interfaces checks. - if err, ok = bindVarToReflectValueWithInterfaceCheck(pointerReflectValue, paramsInterface); ok { + if ok, err = bindVarToReflectValueWithInterfaceCheck(pointerReflectValue, paramsInterface); ok { return err } @@ -198,7 +144,7 @@ func doStruct(params interface{}, pointer interface{}, paramKeyToAttrMap map[str // return v.UnmarshalValue(params) // } // Note that it's `pointerElemReflectValue` here not `pointerReflectValue`. - if err, ok = bindVarToReflectValueWithInterfaceCheck(pointerElemReflectValue, paramsInterface); ok { + if ok, err := bindVarToReflectValueWithInterfaceCheck(pointerElemReflectValue, paramsInterface); ok { return err } // Retrieve its element, may be struct at last. @@ -538,7 +484,7 @@ func bindVarToStructAttr( } // Common interface check. - if err, ok = bindVarToReflectValueWithInterfaceCheck(structFieldValue, value); ok { + if ok, err = bindVarToReflectValueWithInterfaceCheck(structFieldValue, value); ok { return err } @@ -553,24 +499,24 @@ func bindVarToStructAttr( } // bindVarToReflectValueWithInterfaceCheck does bind using common interfaces checks. -func bindVarToReflectValueWithInterfaceCheck(reflectValue reflect.Value, value interface{}) (error, bool) { +func bindVarToReflectValueWithInterfaceCheck(reflectValue reflect.Value, value interface{}) (bool, error) { var pointer interface{} if reflectValue.Kind() != reflect.Ptr && reflectValue.CanAddr() { reflectValueAddr := reflectValue.Addr() if reflectValueAddr.IsNil() || !reflectValueAddr.IsValid() { - return nil, false + return false, nil } // Not a pointer, but can token address, that makes it can be unmarshalled. pointer = reflectValue.Addr().Interface() } else { if reflectValue.IsNil() || !reflectValue.IsValid() { - return nil, false + return false, nil } pointer = reflectValue.Interface() } // UnmarshalValue. if v, ok := pointer.(iUnmarshalValue); ok { - return v.UnmarshalValue(value), ok + return ok, v.UnmarshalValue(value) } // UnmarshalText. if v, ok := pointer.(iUnmarshalText); ok { @@ -583,7 +529,7 @@ func bindVarToReflectValueWithInterfaceCheck(reflectValue reflect.Value, value i valueBytes = []byte(f.String()) } if len(valueBytes) > 0 { - return v.UnmarshalText(valueBytes), ok + return ok, v.UnmarshalText(valueBytes) } } // UnmarshalJSON. @@ -606,14 +552,14 @@ func bindVarToReflectValueWithInterfaceCheck(reflectValue reflect.Value, value i copy(newValueBytes[1:], valueBytes) valueBytes = newValueBytes } - return v.UnmarshalJSON(valueBytes), ok + return ok, v.UnmarshalJSON(valueBytes) } } if v, ok := pointer.(iSet); ok { v.Set(value) - return nil, ok + return ok, nil } - return nil, false + return false, nil } // bindVarToReflectValue sets `value` to reflect value object `structFieldValue`. @@ -621,7 +567,7 @@ func bindVarToReflectValue( structFieldValue reflect.Value, value interface{}, paramKeyToAttrMap map[string]string, ) (err error) { // JSON content converting. - err, ok := doStructWithJsonCheck(value, structFieldValue) + ok, err := doConvertWithJsonCheck(value, structFieldValue) if err != nil { return err } @@ -755,7 +701,7 @@ func bindVarToReflectValue( if structFieldValue.IsNil() || structFieldValue.IsZero() { // Nil or empty pointer, it creates a new one. item := reflect.New(structFieldValue.Type().Elem()) - if err, ok = bindVarToReflectValueWithInterfaceCheck(item, value); ok { + if ok, err = bindVarToReflectValueWithInterfaceCheck(item, value); ok { structFieldValue.Set(item) return err } diff --git a/util/gconv/gconv_structs.go b/util/gconv/gconv_structs.go index f8e93656ce4..b6722477a8d 100644 --- a/util/gconv/gconv_structs.go +++ b/util/gconv/gconv_structs.go @@ -11,7 +11,6 @@ import ( "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" - "github.com/gogf/gf/v2/internal/json" ) // Structs converts any slice to given struct slice. @@ -42,18 +41,6 @@ func StructsTag(params interface{}, pointer interface{}, priorityTag string) (er func doStructs( params interface{}, pointer interface{}, paramKeyToAttrMap map[string]string, priorityTag string, ) (err error) { - if params == nil { - // If `params` is nil, no conversion. - return nil - } - if pointer == nil { - return gerror.NewCode(gcode.CodeInvalidParameter, "object pointer cannot be nil") - } - - if doStructsByDirectReflectSet(params, pointer) { - return nil - } - defer func() { // Catch the panic, especially the reflection operation panics. if exception := recover(); exception != nil { @@ -64,35 +51,16 @@ func doStructs( } } }() - // If given `params` is JSON, it then uses json.Unmarshal doing the converting. - switch r := params.(type) { - case []byte: - if json.Valid(r) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - return json.UnmarshalUseNumber(r, rv.Interface()) - } - } else { - return json.UnmarshalUseNumber(r, pointer) - } - } - case string: - if paramsBytes := []byte(r); json.Valid(paramsBytes) { - if rv, ok := pointer.(reflect.Value); ok { - if rv.Kind() == reflect.Ptr { - return json.UnmarshalUseNumber(paramsBytes, rv.Interface()) - } - } else { - return json.UnmarshalUseNumber(paramsBytes, pointer) - } - } - } + // Pointer type check. pointerRv, ok := pointer.(reflect.Value) if !ok { pointerRv = reflect.ValueOf(pointer) if kind := pointerRv.Kind(); kind != reflect.Ptr { - return gerror.NewCodef(gcode.CodeInvalidParameter, "pointer should be type of pointer, but got: %v", kind) + return gerror.NewCodef( + gcode.CodeInvalidParameter, + "pointer should be type of pointer, but got: %v", kind, + ) } } // Converting `params` to map slice. @@ -163,17 +131,3 @@ func doStructs( pointerRv.Elem().Set(reflectElemArray) return nil } - -// doStructsByDirectReflectSet do the converting directly using reflect Set. -// It returns true if success, or else false. -func doStructsByDirectReflectSet(params interface{}, pointer interface{}) (ok bool) { - v1 := reflect.ValueOf(pointer) - v2 := reflect.ValueOf(params) - if v1.Kind() == reflect.Ptr { - if elem := v1.Elem(); elem.IsValid() && elem.Type() == v2.Type() { - elem.Set(v2) - ok = true - } - } - return ok -} diff --git a/util/gconv/gconv_z_unit_issue_test.go b/util/gconv/gconv_z_unit_issue_test.go index 6caddd70fac..52c695d1747 100644 --- a/util/gconv/gconv_z_unit_issue_test.go +++ b/util/gconv/gconv_z_unit_issue_test.go @@ -7,6 +7,7 @@ package gconv_test import ( + "math/big" "testing" "time" @@ -115,6 +116,25 @@ func Test_Issue1227(t *testing.T) { }) } +// https://github.com/gogf/gf/issues/1607 +func Test_Issue1607(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Demo struct { + B Float64 + } + rat := &big.Rat{} + rat.SetFloat64(1.5) + + var demos = make([]Demo, 1) + err := gconv.Scan([]map[string]interface{}{ + {"A": 1, "B": rat}, + }, &demos) + t.AssertNil(err) + t.Assert(demos[0].B, 1.5) + }) +} + +// https://github.com/gogf/gf/issues/1946 func Test_Issue1946(t *testing.T) { gtest.C(t, func(t *gtest.T) { type B struct { @@ -292,3 +312,17 @@ func Test_Issue2371(t *testing.T) { t.Assert(s.Time.UTC(), `2022-12-15 08:11:34 +0000 UTC`) }) } + +func Test_Issue2901(t *testing.T) { + type GameApp2 struct { + ForceUpdateTime *time.Time + } + gtest.C(t, func(t *gtest.T) { + src := map[string]interface{}{ + "FORCE_UPDATE_TIME": time.Now(), + } + m := GameApp2{} + err := gconv.Scan(src, &m) + t.AssertNil(err) + }) +} diff --git a/util/gconv/gconv_z_unit_scan_test.go b/util/gconv/gconv_z_unit_scan_test.go index 560d39461d6..36bf1a27fcc 100644 --- a/util/gconv/gconv_z_unit_scan_test.go +++ b/util/gconv/gconv_z_unit_scan_test.go @@ -641,19 +641,52 @@ func (f *Float64) UnmarshalValue(value interface{}) error { return nil } -func Test_Issue1607(t *testing.T) { +func Test_Scan_AutoCreatingPointerElem(t *testing.T) { + type A struct { + Name string + } gtest.C(t, func(t *gtest.T) { - type Demo struct { - B Float64 + var dst A + var src = A{ + Name: "john", } - rat := &big.Rat{} - rat.SetFloat64(1.5) - - var demos = make([]Demo, 1) - err := gconv.Scan([]map[string]interface{}{ - {"A": 1, "B": rat}, - }, &demos) + err := gconv.Scan(src, &dst) + t.AssertNil(err) + t.Assert(src, dst) + }) + gtest.C(t, func(t *gtest.T) { + var dst = &A{ + Name: "smith", + } + var src = A{ + Name: "john", + } + err := gconv.Scan(src, &dst) + t.AssertNil(err) + t.Assert(src, dst) + }) + gtest.C(t, func(t *gtest.T) { + var dst = A{ + Name: "smith", + } + var src = &A{ + Name: "john", + } + err := gconv.Scan(src, &dst) t.AssertNil(err) - t.Assert(demos[0].B, 1.5) + t.Assert(src, dst) + }) + gtest.C(t, func(t *gtest.T) { + var dst *A + var src = &A{ + Name: "john", + } + err := gconv.Scan(src, &dst) + t.AssertNil(err) + t.Assert(src, dst) + + // Note that the dst points to src. + src.Name = "smith" + t.Assert(src, dst) }) } diff --git a/util/gconv/gconv_z_unit_time_test.go b/util/gconv/gconv_z_unit_time_test.go index e78c988d8f1..5310e58546f 100644 --- a/util/gconv/gconv_z_unit_time_test.go +++ b/util/gconv/gconv_z_unit_time_test.go @@ -78,17 +78,3 @@ func Test_Time_Slice_Attribute(t *testing.T) { t.Assert(s.Arr[1], "2021-01-12 12:34:57") }) } - -func Test_Issue2901(t *testing.T) { - type GameApp2 struct { - ForceUpdateTime *time.Time - } - gtest.C(t, func(t *gtest.T) { - src := map[string]interface{}{ - "FORCE_UPDATE_TIME": time.Now(), - } - m := GameApp2{} - err := gconv.Scan(src, &m) - t.AssertNil(err) - }) -} diff --git a/util/gutil/gutil.go b/util/gutil/gutil.go index fdfcb8a138d..b77d6c2fd7f 100644 --- a/util/gutil/gutil.go +++ b/util/gutil/gutil.go @@ -10,7 +10,6 @@ package gutil import ( "reflect" - "github.com/gogf/gf/v2/internal/empty" "github.com/gogf/gf/v2/util/gconv" ) @@ -18,13 +17,6 @@ const ( dumpIndent = ` ` ) -// IsEmpty checks given `value` empty or not. -// It returns false if `value` is: integer(0), bool(false), slice/map(len=0), nil; -// or else returns true. -func IsEmpty(value interface{}) bool { - return empty.IsEmpty(value) -} - // Keys retrieves and returns the keys from given map or struct. func Keys(mapOrStruct interface{}) (keysOrAttrs []string) { keysOrAttrs = make([]string, 0) diff --git a/util/gutil/gutil_is.go b/util/gutil/gutil_is.go new file mode 100644 index 00000000000..4a7232fb3c4 --- /dev/null +++ b/util/gutil/gutil_is.go @@ -0,0 +1,25 @@ +// 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 gutil + +import ( + "reflect" + + "github.com/gogf/gf/v2/internal/empty" +) + +// IsEmpty checks given `value` empty or not. +// It returns false if `value` is: integer(0), bool(false), slice/map(len=0), nil; +// or else returns true. +func IsEmpty(value interface{}) bool { + return empty.IsEmpty(value) +} + +// IsTypeOf checks and returns whether the type of `value` and `valueInExpectType` equal. +func IsTypeOf(value, valueInExpectType interface{}) bool { + return reflect.TypeOf(value) == reflect.TypeOf(valueInExpectType) +} diff --git a/util/gutil/gutil_z_unit_is_test.go b/util/gutil/gutil_z_unit_is_test.go new file mode 100644 index 00000000000..43c802c06b6 --- /dev/null +++ b/util/gutil/gutil_z_unit_is_test.go @@ -0,0 +1,46 @@ +// 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 gutil_test + +import ( + "testing" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gutil" +) + +func Test_IsEmpty(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.Assert(gutil.IsEmpty(1), false) + }) +} + +func Test_IsTypeOf(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.Assert(gutil.IsTypeOf(1, 0), true) + t.Assert(gutil.IsTypeOf(1.1, 0.1), true) + t.Assert(gutil.IsTypeOf(1.1, 1), false) + t.Assert(gutil.IsTypeOf(true, false), true) + t.Assert(gutil.IsTypeOf(true, 1), false) + }) + gtest.C(t, func(t *gtest.T) { + type A struct { + Name string + } + type B struct { + Name string + } + t.Assert(gutil.IsTypeOf(1, A{}), false) + t.Assert(gutil.IsTypeOf(A{}, B{}), false) + t.Assert(gutil.IsTypeOf(A{Name: "john"}, &A{Name: "john"}), false) + t.Assert(gutil.IsTypeOf(A{Name: "john"}, A{Name: "john"}), true) + t.Assert(gutil.IsTypeOf(A{Name: "john"}, A{}), true) + t.Assert(gutil.IsTypeOf(&A{Name: "john"}, &A{}), true) + t.Assert(gutil.IsTypeOf(&A{Name: "john"}, &B{}), false) + t.Assert(gutil.IsTypeOf(A{Name: "john"}, B{Name: "john"}), false) + }) +} diff --git a/util/gutil/gutil_z_unit_test.go b/util/gutil/gutil_z_unit_test.go index bc7241dbb7d..20971aff882 100755 --- a/util/gutil/gutil_z_unit_test.go +++ b/util/gutil/gutil_z_unit_test.go @@ -62,12 +62,6 @@ func Test_TryCatch(t *testing.T) { }) } -func Test_IsEmpty(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - t.Assert(gutil.IsEmpty(1), false) - }) -} - func Test_Throw(t *testing.T) { gtest.C(t, func(t *gtest.T) { defer func() {