diff --git a/database/gdb/gdb_z_mysql_association_scanlist_test.go b/database/gdb/gdb_z_mysql_association_scanlist_test.go index dee305cfc..5016d60a4 100644 --- a/database/gdb/gdb_z_mysql_association_scanlist_test.go +++ b/database/gdb/gdb_z_mysql_association_scanlist_test.go @@ -1776,7 +1776,7 @@ CREATE TABLE %s ( t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[4])["uid"], 3) t.Assert(gconv.Map(all.MapKeyValue("uid")["3"].Slice()[4])["score"], 5) }) - db.SetDebug(true) + // Result ScanList with struct elements and pointer attributes. gtest.C(t, func(t *gtest.T) { var users []Entity diff --git a/internal/utils/utils_list.go b/internal/utils/utils_list.go new file mode 100644 index 000000000..355ad9f8e --- /dev/null +++ b/internal/utils/utils_list.go @@ -0,0 +1,37 @@ +// 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 utils + +import "fmt" + +// ListToMapByKey converts `list` to a map[string]interface{} of which key is specified by `key`. +// Note that the item value may be type of slice. +func ListToMapByKey(list []map[string]interface{}, key string) map[string]interface{} { + var ( + s = "" + m = make(map[string]interface{}) + tempMap = make(map[string][]interface{}) + hasMultiValues bool + ) + for _, item := range list { + if k, ok := item[key]; ok { + s = fmt.Sprintf(`%v`, k) + tempMap[s] = append(tempMap[s], item) + if len(tempMap[s]) > 1 { + hasMultiValues = true + } + } + } + for k, v := range tempMap { + if hasMultiValues { + m[k] = v + } else { + m[k] = v[0] + } + } + return m +} diff --git a/internal/utils/utils_map.go b/internal/utils/utils_map.go new file mode 100644 index 000000000..6cb1b6067 --- /dev/null +++ b/internal/utils/utils_map.go @@ -0,0 +1,26 @@ +// 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 utils + +// MapPossibleItemByKey tries to find the possible key-value pair for given key ignoring cases and symbols. +// +// Note that this function might be of low performance. +func MapPossibleItemByKey(data map[string]interface{}, key string) (foundKey string, foundValue interface{}) { + if len(data) == 0 { + return + } + if v, ok := data[key]; ok { + return key, v + } + // Loop checking. + for k, v := range data { + if EqualFoldWithoutChars(k, key) { + return k, v + } + } + return "", nil +} diff --git a/text/gstr/gstr.go b/text/gstr/gstr.go index 7a08c0cf4..bc7b4d7bb 100644 --- a/text/gstr/gstr.go +++ b/text/gstr/gstr.go @@ -327,14 +327,7 @@ func Split(str, delimiter string) []string { // and calls Trim to every element of this array. It ignores the elements // which are empty after Trim. func SplitAndTrim(str, delimiter string, characterMask ...string) []string { - array := make([]string, 0) - for _, v := range strings.Split(str, delimiter) { - v = Trim(v, characterMask...) - if v != "" { - array = append(array, v) - } - } - return array + return utils.SplitAndTrim(str, delimiter, characterMask...) } // Join concatenates the elements of `array` to create a single string. The separator string diff --git a/util/gconv/gconv_scan.go b/util/gconv/gconv_scan.go index 8df73d0e7..5472bdd83 100644 --- a/util/gconv/gconv_scan.go +++ b/util/gconv/gconv_scan.go @@ -7,8 +7,11 @@ package gconv import ( + "database/sql" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/internal/structs" + "github.com/gogf/gf/v2/internal/utils" "reflect" ) @@ -38,7 +41,18 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string) } pointerKind = pointerType.Kind() if pointerKind != reflect.Ptr { - return gerror.NewCodef(gcode.CodeInvalidParameter, "params should be type of pointer, but got type: %v", pointerKind) + if pointerValue.CanAddr() { + pointerValue = pointerValue.Addr() + pointerType = pointerValue.Type() + pointerKind = pointerType.Kind() + } else { + return gerror.NewCodef( + gcode.CodeInvalidParameter, + "params should be type of pointer, but got type: %v", + pointerType, + ) + } + } // Direct assignment checks! var ( @@ -94,3 +108,410 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string) 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 +// ScanList(records, &users, "User") +// ScanList(records, &users, "User", "uid") +// ScanList(records, &users, "UserDetail", "User", "uid:Uid") +// ScanList(records, &users, "UserScores", "User", "uid:Uid") +// ScanList(records, &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 users []*Entity +// ScanList(records, &users) +// ScanList(records, &users, "UserDetail", "uid") +// ScanList(records, &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 filed 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 ( + filedMap, _ = structs.FieldMap(structs.FieldMapInput{ + Pointer: relationFromAttrValue, + RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, + }) + ) + if key, _ := utils.MapPossibleItemByKey(Map(filedMap), 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_z_unit_scan_test.go b/util/gconv/gconv_z_unit_scan_test.go index 65248bced..e34f91d99 100644 --- a/util/gconv/gconv_z_unit_scan_test.go +++ b/util/gconv/gconv_z_unit_scan_test.go @@ -319,3 +319,285 @@ func Test_Scan_SameType_Just_Assign(t *testing.T) { t.Assert(*m1["int"], *m2["int"]) }) } + +func Test_ScanList_Basic(t *testing.T) { + // Struct attribute. + gtest.C(t, func(t *gtest.T) { + 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 { + User EntityUser + UserDetail EntityUserDetail + UserScores []EntityUserScores + } + + var ( + err error + entities []Entity + entityUsers = []EntityUser{ + {Uid: 1, Name: "name1"}, + {Uid: 2, Name: "name2"}, + {Uid: 3, Name: "name3"}, + } + userDetails = []EntityUserDetail{ + {Uid: 1, Address: "address1"}, + {Uid: 2, Address: "address2"}, + } + userScores = []EntityUserScores{ + {Id: 10, Uid: 1, Score: 100}, + {Id: 11, Uid: 1, Score: 60}, + {Id: 20, Uid: 2, Score: 99}, + } + ) + err = gconv.ScanList(entityUsers, &entities, "User") + t.AssertNil(err) + + err = gconv.ScanList(userDetails, &entities, "UserDetail", "User", "uid") + t.AssertNil(err) + + err = gconv.ScanList(userScores, &entities, "UserScores", "User", "uid") + t.AssertNil(err) + + t.Assert(len(entities), 3) + t.Assert(entities[0].User, entityUsers[0]) + t.Assert(entities[1].User, entityUsers[1]) + t.Assert(entities[2].User, entityUsers[2]) + + t.Assert(entities[0].UserDetail, userDetails[0]) + t.Assert(entities[1].UserDetail, userDetails[1]) + t.Assert(entities[2].UserDetail, EntityUserDetail{}) + + t.Assert(len(entities[0].UserScores), 2) + t.Assert(entities[0].UserScores[0], userScores[0]) + t.Assert(entities[0].UserScores[1], userScores[1]) + + t.Assert(len(entities[1].UserScores), 1) + t.Assert(entities[1].UserScores[0], userScores[2]) + + t.Assert(len(entities[2].UserScores), 0) + }) + // Pointer attribute. + gtest.C(t, func(t *gtest.T) { + 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 { + User *EntityUser + UserDetail *EntityUserDetail + UserScores []*EntityUserScores + } + + var ( + err error + entities []*Entity + entityUsers = []*EntityUser{ + {Uid: 1, Name: "name1"}, + {Uid: 2, Name: "name2"}, + {Uid: 3, Name: "name3"}, + } + userDetails = []*EntityUserDetail{ + {Uid: 1, Address: "address1"}, + {Uid: 2, Address: "address2"}, + } + userScores = []*EntityUserScores{ + {Id: 10, Uid: 1, Score: 100}, + {Id: 11, Uid: 1, Score: 60}, + {Id: 20, Uid: 2, Score: 99}, + } + ) + err = gconv.ScanList(entityUsers, &entities, "User") + t.AssertNil(err) + + err = gconv.ScanList(userDetails, &entities, "UserDetail", "User", "uid") + t.AssertNil(err) + + err = gconv.ScanList(userScores, &entities, "UserScores", "User", "uid") + t.AssertNil(err) + + t.Assert(len(entities), 3) + t.Assert(entities[0].User, entityUsers[0]) + t.Assert(entities[1].User, entityUsers[1]) + t.Assert(entities[2].User, entityUsers[2]) + + t.Assert(entities[0].UserDetail, userDetails[0]) + t.Assert(entities[1].UserDetail, userDetails[1]) + t.Assert(entities[2].UserDetail, nil) + + t.Assert(len(entities[0].UserScores), 2) + t.Assert(entities[0].UserScores[0], userScores[0]) + t.Assert(entities[0].UserScores[1], userScores[1]) + + t.Assert(len(entities[1].UserScores), 1) + t.Assert(entities[1].UserScores[0], userScores[2]) + + t.Assert(len(entities[2].UserScores), 0) + }) +} + +func Test_ScanList_Embedded(t *testing.T) { + // Struct attribute. + gtest.C(t, func(t *gtest.T) { + 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 ( + err error + entities []Entity + entityUsers = []EntityUser{ + {Uid: 1, Name: "name1"}, + {Uid: 2, Name: "name2"}, + {Uid: 3, Name: "name3"}, + } + userDetails = []EntityUserDetail{ + {Uid: 1, Address: "address1"}, + {Uid: 2, Address: "address2"}, + } + userScores = []EntityUserScores{ + {Id: 10, Uid: 1, Score: 100}, + {Id: 11, Uid: 1, Score: 60}, + {Id: 20, Uid: 2, Score: 99}, + } + ) + err = gconv.Scan(entityUsers, &entities) + t.AssertNil(err) + + err = gconv.ScanList(userDetails, &entities, "UserDetail", "uid") + t.AssertNil(err) + + err = gconv.ScanList(userScores, &entities, "UserScores", "uid") + t.AssertNil(err) + + t.Assert(len(entities), 3) + t.Assert(entities[0].EntityUser, entityUsers[0]) + t.Assert(entities[1].EntityUser, entityUsers[1]) + t.Assert(entities[2].EntityUser, entityUsers[2]) + + t.Assert(entities[0].UserDetail, userDetails[0]) + t.Assert(entities[1].UserDetail, userDetails[1]) + t.Assert(entities[2].UserDetail, EntityUserDetail{}) + + t.Assert(len(entities[0].UserScores), 2) + t.Assert(entities[0].UserScores[0], userScores[0]) + t.Assert(entities[0].UserScores[1], userScores[1]) + + t.Assert(len(entities[1].UserScores), 1) + t.Assert(entities[1].UserScores[0], userScores[2]) + + t.Assert(len(entities[2].UserScores), 0) + }) + // Pointer attribute. + gtest.C(t, func(t *gtest.T) { + 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 ( + err error + entities []Entity + entityUsers = []EntityUser{ + {Uid: 1, Name: "name1"}, + {Uid: 2, Name: "name2"}, + {Uid: 3, Name: "name3"}, + } + userDetails = []EntityUserDetail{ + {Uid: 1, Address: "address1"}, + {Uid: 2, Address: "address2"}, + } + userScores = []EntityUserScores{ + {Id: 10, Uid: 1, Score: 100}, + {Id: 11, Uid: 1, Score: 60}, + {Id: 20, Uid: 2, Score: 99}, + } + ) + err = gconv.Scan(entityUsers, &entities) + t.AssertNil(err) + + err = gconv.ScanList(userDetails, &entities, "UserDetail", "uid") + t.AssertNil(err) + + err = gconv.ScanList(userScores, &entities, "UserScores", "uid") + t.AssertNil(err) + + t.Assert(len(entities), 3) + t.Assert(entities[0].EntityUser, entityUsers[0]) + t.Assert(entities[1].EntityUser, entityUsers[1]) + t.Assert(entities[2].EntityUser, entityUsers[2]) + + t.Assert(entities[0].UserDetail, userDetails[0]) + t.Assert(entities[1].UserDetail, userDetails[1]) + t.Assert(entities[2].UserDetail, nil) + + t.Assert(len(entities[0].UserScores), 2) + t.Assert(entities[0].UserScores[0], userScores[0]) + t.Assert(entities[0].UserScores[1], userScores[1]) + + t.Assert(len(entities[1].UserScores), 1) + t.Assert(entities[1].UserScores[0], userScores[2]) + + t.Assert(len(entities[2].UserScores), 0) + }) +} diff --git a/util/gutil/gutil_dump.go b/util/gutil/gutil_dump.go index 06ed56fd5..0e331426f 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -15,6 +15,16 @@ import ( "strings" ) +// iString is used for type assert api for String(). +type iString interface { + String() string +} + +// iMarshalJSON is the interface for custom Json marshaling. +type iMarshalJSON interface { + MarshalJSON() ([]byte, error) +} + // ExportOption specifies the behavior of function Export. type ExportOption struct { WithoutType bool // WithoutType specifies exported content has no type information. @@ -151,7 +161,7 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE "%s%v:%s", newIndent, mapKeyStr, - gstr.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), + strings.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), )) } else { buffer.WriteString(fmt.Sprintf( @@ -159,7 +169,7 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE newIndent, mapKey.Type().String(), mapKeyStr, - gstr.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), + strings.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), )) } doExport(reflectValue.MapIndex(mapKey).Interface(), newIndent, buffer, option) @@ -173,10 +183,31 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, }) if len(structFields) == 0 { - if option.WithoutType { - buffer.WriteString("{}") + var ( + structContentStr = "" + attributeCountStr = "0" + ) + if v, ok := value.(iString); ok { + structContentStr = v.String() + } else if v, ok := value.(iMarshalJSON); ok { + b, _ := v.MarshalJSON() + structContentStr = string(b) + } + if structContentStr == "" { + structContentStr = "{}" } else { - buffer.WriteString(fmt.Sprintf("%s(0) {}", reflectTypeName)) + structContentStr = fmt.Sprintf(`"%s"`, gstr.AddSlashes(structContentStr)) + attributeCountStr = fmt.Sprintf(`%d`, len(structContentStr)-2) + } + if option.WithoutType { + buffer.WriteString(structContentStr) + } else { + buffer.WriteString(fmt.Sprintf( + "%s(%s) %s", + reflectTypeName, + attributeCountStr, + structContentStr, + )) } return } @@ -202,7 +233,7 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE "%s%s:%s", newIndent, field.Name(), - gstr.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), + strings.Repeat(" ", maxSpaceNum-tmpSpaceNum+1), )) doExport(field.Value.Interface(), newIndent, buffer, option) buffer.WriteString(",\n") diff --git a/util/gutil/gutil_list.go b/util/gutil/gutil_list.go index 14fd8e2f6..12a8735e3 100644 --- a/util/gutil/gutil_list.go +++ b/util/gutil/gutil_list.go @@ -7,6 +7,7 @@ package gutil import ( + "github.com/gogf/gf/v2/internal/utils" "reflect" ) @@ -130,3 +131,9 @@ func ListItemValuesUnique(list interface{}, key string, subKey ...interface{}) [ } return values } + +// ListToMapByKey converts `list` to a map[string]interface{} of which key is specified by `key`. +// Note that the item value may be type of slice. +func ListToMapByKey(list []map[string]interface{}, key string) map[string]interface{} { + return utils.ListToMapByKey(list, key) +} diff --git a/util/gutil/gutil_map.go b/util/gutil/gutil_map.go index 1a9b96029..2b5246e91 100644 --- a/util/gutil/gutil_map.go +++ b/util/gutil/gutil_map.go @@ -67,19 +67,7 @@ func MapMergeCopy(src ...map[string]interface{}) (copy map[string]interface{}) { // // Note that this function might be of low performance. func MapPossibleItemByKey(data map[string]interface{}, key string) (foundKey string, foundValue interface{}) { - if len(data) == 0 { - return - } - if v, ok := data[key]; ok { - return key, v - } - // Loop checking. - for k, v := range data { - if utils.EqualFoldWithoutChars(k, key) { - return k, v - } - } - return "", nil + return utils.MapPossibleItemByKey(data, key) } // MapContainsPossibleKey checks if the given `key` is contained in given map `data`. diff --git a/util/gutil/gutil_z_unit_dump_test.go b/util/gutil/gutil_z_unit_dump_test.go new file mode 100755 index 000000000..236d07add --- /dev/null +++ b/util/gutil/gutil_z_unit_dump_test.go @@ -0,0 +1,128 @@ +// 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 ( + "github.com/gogf/gf/v2/net/ghttp" + "github.com/gogf/gf/v2/os/gtime" + "github.com/gogf/gf/v2/util/gmeta" + "testing" + + "github.com/gogf/gf/v2/test/gtest" + "github.com/gogf/gf/v2/util/gutil" +) + +func Test_Dump(t *testing.T) { + type CommonReq struct { + AppId int64 `json:"appId" v:"required" in:"path" des:"应用Id" sum:"应用Id Summary"` + ResourceId string `json:"resourceId" in:"query" des:"资源Id" sum:"资源Id Summary"` + } + type SetSpecInfo struct { + StorageType string `v:"required|in:CLOUD_PREMIUM,CLOUD_SSD,CLOUD_HSSD" des:"StorageType"` + Shards int32 `des:"shards 分片数" sum:"Shards Summary"` + Params []string `des:"默认参数(json 串-ClickHouseParams)" sum:"Params Summary"` + } + type CreateResourceReq struct { + CommonReq + gmeta.Meta `path:"/CreateResourceReq" method:"POST" tags:"default" sum:"CreateResourceReq sum"` + Name string + CreatedAt *gtime.Time + SetMap map[string]*SetSpecInfo + SetSlice []SetSpecInfo + Handler ghttp.HandlerFunc + internal string + } + req := &CreateResourceReq{ + CommonReq: CommonReq{ + AppId: 12345678, + ResourceId: "tdchqy-xxx", + }, + Name: "john", + CreatedAt: gtime.Now(), + SetMap: map[string]*SetSpecInfo{ + "test1": { + StorageType: "ssd", + Shards: 2, + Params: []string{"a", "b", "c"}, + }, + "test2": { + StorageType: "hssd", + Shards: 10, + Params: []string{}, + }, + }, + SetSlice: []SetSpecInfo{ + { + StorageType: "hssd", + Shards: 10, + Params: []string{"h"}, + }, + }, + } + gtest.C(t, func(t *gtest.T) { + gutil.Dump(map[int]int{ + 100: 100, + }) + gutil.Dump(req) + }) +} + +func TestDumpWithType(t *testing.T) { + type CommonReq struct { + AppId int64 `json:"appId" v:"required" in:"path" des:"应用Id" sum:"应用Id Summary"` + ResourceId string `json:"resourceId" in:"query" des:"资源Id" sum:"资源Id Summary"` + } + type SetSpecInfo struct { + StorageType string `v:"required|in:CLOUD_PREMIUM,CLOUD_SSD,CLOUD_HSSD" des:"StorageType"` + Shards int32 `des:"shards 分片数" sum:"Shards Summary"` + Params []string `des:"默认参数(json 串-ClickHouseParams)" sum:"Params Summary"` + } + type CreateResourceReq struct { + CommonReq + gmeta.Meta `path:"/CreateResourceReq" method:"POST" tags:"default" sum:"CreateResourceReq sum"` + Name string + CreatedAt *gtime.Time + SetMap map[string]*SetSpecInfo `v:"required" des:"配置Map"` + SetSlice []SetSpecInfo `v:"required" des:"配置Slice"` + Handler ghttp.HandlerFunc + internal string + } + req := &CreateResourceReq{ + CommonReq: CommonReq{ + AppId: 12345678, + ResourceId: "tdchqy-xxx", + }, + Name: "john", + CreatedAt: gtime.Now(), + SetMap: map[string]*SetSpecInfo{ + "test1": { + StorageType: "ssd", + Shards: 2, + Params: []string{"a", "b", "c"}, + }, + "test2": { + StorageType: "hssd", + Shards: 10, + Params: []string{}, + }, + }, + SetSlice: []SetSpecInfo{ + { + StorageType: "hssd", + Shards: 10, + Params: []string{"h"}, + }, + }, + } + gtest.C(t, func(t *gtest.T) { + gutil.DumpWithType(map[int]int{ + 100: 100, + }) + gutil.DumpWithType(req) + gutil.DumpWithType([][]byte{[]byte("hello")}) + }) +} diff --git a/util/gutil/gutil_z_unit_test.go b/util/gutil/gutil_z_unit_test.go index 253b89373..0459ad4f7 100755 --- a/util/gutil/gutil_z_unit_test.go +++ b/util/gutil/gutil_z_unit_test.go @@ -8,129 +8,12 @@ package gutil_test import ( "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/net/ghttp" - "github.com/gogf/gf/v2/util/gmeta" "testing" "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/gutil" ) -func Test_Dump(t *testing.T) { - type CommonReq struct { - AppId int64 `json:"appId" v:"required" in:"path" des:"应用Id" sum:"应用Id Summary"` - ResourceId string `json:"resourceId" in:"query" des:"资源Id" sum:"资源Id Summary"` - } - type SetSpecInfo struct { - StorageType string `v:"required|in:CLOUD_PREMIUM,CLOUD_SSD,CLOUD_HSSD" des:"StorageType"` - Shards int32 `des:"shards 分片数" sum:"Shards Summary"` - Params []string `des:"默认参数(json 串-ClickHouseParams)" sum:"Params Summary"` - } - type CreateResourceReq struct { - CommonReq - gmeta.Meta `path:"/CreateResourceReq" method:"POST" tags:"default" sum:"CreateResourceReq sum"` - Name string `des:"实例名称"` - Product string `des:"业务类型"` - Region string `v:"required" des:"区域"` - SetMap map[string]*SetSpecInfo `v:"required" des:"配置Map"` - SetSlice []SetSpecInfo `v:"required" des:"配置Slice"` - Handler ghttp.HandlerFunc - internal string - } - req := &CreateResourceReq{ - CommonReq: CommonReq{ - AppId: 12345678, - ResourceId: "tdchqy-xxx", - }, - Name: "john", - Product: "goframe", - Region: "cd", - SetMap: map[string]*SetSpecInfo{ - "test1": { - StorageType: "ssd", - Shards: 2, - Params: []string{"a", "b", "c"}, - }, - "test2": { - StorageType: "hssd", - Shards: 10, - Params: []string{}, - }, - }, - SetSlice: []SetSpecInfo{ - { - StorageType: "hssd", - Shards: 10, - Params: []string{"h"}, - }, - }, - } - gtest.C(t, func(t *gtest.T) { - gutil.Dump(map[int]int{ - 100: 100, - }) - gutil.Dump(req) - }) -} - -func TestDumpWithType(t *testing.T) { - type CommonReq struct { - AppId int64 `json:"appId" v:"required" in:"path" des:"应用Id" sum:"应用Id Summary"` - ResourceId string `json:"resourceId" in:"query" des:"资源Id" sum:"资源Id Summary"` - } - type SetSpecInfo struct { - StorageType string `v:"required|in:CLOUD_PREMIUM,CLOUD_SSD,CLOUD_HSSD" des:"StorageType"` - Shards int32 `des:"shards 分片数" sum:"Shards Summary"` - Params []string `des:"默认参数(json 串-ClickHouseParams)" sum:"Params Summary"` - } - type CreateResourceReq struct { - CommonReq - gmeta.Meta `path:"/CreateResourceReq" method:"POST" tags:"default" sum:"CreateResourceReq sum"` - Name string `des:"实例名称"` - Product string `des:"业务类型"` - Region string `v:"required" des:"区域"` - SetMap map[string]*SetSpecInfo `v:"required" des:"配置Map"` - SetSlice []SetSpecInfo `v:"required" des:"配置Slice"` - Handler ghttp.HandlerFunc - internal string - } - req := &CreateResourceReq{ - CommonReq: CommonReq{ - AppId: 12345678, - ResourceId: "tdchqy-xxx", - }, - Name: "john", - Product: "goframe", - Region: "cd", - SetMap: map[string]*SetSpecInfo{ - "test1": { - StorageType: "ssd", - Shards: 2, - Params: []string{"a", "b", "c"}, - }, - "test2": { - StorageType: "hssd", - Shards: 10, - Params: []string{}, - }, - }, - SetSlice: []SetSpecInfo{ - { - StorageType: "hssd", - Shards: 10, - Params: []string{"h"}, - }, - }, - } - gtest.C(t, func(t *gtest.T) { - gutil.DumpWithType(map[int]int{ - 100: 100, - }) - gutil.DumpWithType(req) - gutil.DumpWithType([][]byte{[]byte("hello")}) - }) -} - func Test_Try(t *testing.T) { gtest.C(t, func(t *gtest.T) { s := `gutil Try test`