From bc1d76a796b53425f51b546f21eeafdcb175bfba Mon Sep 17 00:00:00 2001 From: mingzaily Date: Wed, 3 Nov 2021 11:41:34 +0800 Subject: [PATCH 01/15] add example for gqueue --- container/gqueue/gqueue_z_example_test.go | 115 ++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 container/gqueue/gqueue_z_example_test.go diff --git a/container/gqueue/gqueue_z_example_test.go b/container/gqueue/gqueue_z_example_test.go new file mode 100644 index 000000000..f3f414fb7 --- /dev/null +++ b/container/gqueue/gqueue_z_example_test.go @@ -0,0 +1,115 @@ +// 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 gqueue_test + +import ( + "context" + "fmt" + "github.com/gogf/gf/v2/container/gqueue" + "github.com/gogf/gf/v2/os/gtimer" + "time" +) + +func ExampleNew() { + n := 10 + q := gqueue.New() + + // Producer + for i := 0; i < n; i++ { + q.Push(i) + } + + fmt.Println(q.Len()) + + // Close the queue in three seconds. + gtimer.SetTimeout(context.Background(), time.Second*3, func(ctx context.Context) { + q.Close() + }) + + // The consumer constantly reads the queue data. + // If there is no data in the queue, it will block. + // The queue is read using the queue.C property exposed + // by the queue object and the selectIO multiplexing syntax + // example: + // for { + // select { + // case v := <-queue.C: + // if v != nil { + // fmt.Println(v) + // } else { + // return + // } + // } + // } + for { + if v := q.Pop(); v != nil { + fmt.Print(v) + } else { + break + } + } + + // Output: + // 10 + // 0123456789 +} + +func ExampleQueue_Push() { + q := gqueue.New() + + for i := 0; i < 10; i++ { + q.Push(i) + } + + fmt.Println(q.Len()) + + // Output: + // 10 +} + +func ExampleQueue_Pop() { + q := gqueue.New() + + for i := 0; i < 10; i++ { + q.Push(i) + } + + fmt.Println(q.Pop()) + q.Close() + fmt.Println(q.Pop()) + + // Output: + // 0 + // +} + +func ExampleQueue_Close() { + q := gqueue.New() + + for i := 0; i < 10; i++ { + q.Push(i) + } + + q.Close() + + fmt.Println(q.Pop()) + + // Output: + // +} + +func ExampleQueue_Len() { + q := gqueue.New() + + q.Push(1) + q.Push(2) + + fmt.Println(q.Len()) + + // Output: + // 2 +} From 9c05682605a9126af1c47f23caa2bed4c8dc93a9 Mon Sep 17 00:00:00 2001 From: mingzaily Date: Wed, 3 Nov 2021 16:16:03 +0800 Subject: [PATCH 02/15] change some example for gqueue --- container/gqueue/gqueue_z_example_test.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/container/gqueue/gqueue_z_example_test.go b/container/gqueue/gqueue_z_example_test.go index f3f414fb7..ba99c51d9 100644 --- a/container/gqueue/gqueue_z_example_test.go +++ b/container/gqueue/gqueue_z_example_test.go @@ -94,11 +94,14 @@ func ExampleQueue_Close() { q.Push(i) } + time.Sleep(time.Millisecond) q.Close() + fmt.Println(q.Len()) fmt.Println(q.Pop()) // Output: + // 0 // } @@ -108,8 +111,13 @@ func ExampleQueue_Len() { q.Push(1) q.Push(2) + time.Sleep(time.Millisecond) + fmt.Println(q.Len()) + q.Pop() + q.Close() fmt.Println(q.Len()) // Output: // 2 + // 0 } From 4acc0e987660a43cedc2ac8413c4a7c1b1086890 Mon Sep 17 00:00:00 2001 From: John Guo Date: Fri, 5 Nov 2021 01:15:11 +0800 Subject: [PATCH 03/15] gconv.ScanList --- util/gconv/gconv_scan.go | 402 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) diff --git a/util/gconv/gconv_scan.go b/util/gconv/gconv_scan.go index 8df73d0e7..76ebcb149 100644 --- a/util/gconv/gconv_scan.go +++ b/util/gconv/gconv_scan.go @@ -7,8 +7,12 @@ 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/text/gstr" + "github.com/gogf/gf/v2/util/gutil" "reflect" ) @@ -94,3 +98,401 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string) return doStruct(params, pointer, keyToAttributeNameMapping, "") } } + +// ScanList converts `data` 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(&users, "User") +// ScanList(&users, "User", "uid") +// ScanList(&users, "UserDetail", "User", "uid:Uid") +// ScanList(&users, "UserScores", "User", "uid:Uid") +// ScanList(&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(&users) +// ScanList(&users, "UserDetail", "uid") +// ScanList(&users, "UserScores", "uid") +func ScanList(data 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(data, structSlicePointer, bindToAttrName, relationAttrName, relationFields) +} + +// doScanList converts `data` to struct slice which contains other complex struct attributes recursively. +// The parameter `model` is used for recursively scanning purpose, which means, it can scan the attribute struct/structs recursively, +// but it needs the Model for database accessing. +// Note that the parameter `structSlicePointer` should be type of *[]struct/*[]*struct. +func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName, relationAttrName, relationFields string) (err error) { + if data == nil { + 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, + ) + } + 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]Value + 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 := gstr.SplitAndTrim(relationFields, "=") + if len(array) == 1 { + // Compatible with old splitting char ':'. + array = gstr.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, _ := gutil.MapPossibleItemByKey(result[0].Map(), 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 = result.MapKeyValue(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, _ := gutil.MapPossibleItemByKey(gconv.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) + for _, v := range relationDataMap[gconv.String(relationFromAttrField.Interface())].Slice() { + results = append(results, v.(Record)) + } + if err = results.Structs(bindToAttrValue.Addr()); err != nil { + return err + } + // Recursively Scan. + if model != nil { + if err = model.doWithScanStructs(bindToAttrValue.Addr()); err != nil { + return nil + } + } + } 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[gconv.String(relationFromAttrField.Interface())] + if v == nil { + // There's no relational data. + continue + } + if v.IsSlice() { + if err = v.Slice()[0].(Record).Struct(element); err != nil { + return err + } + } else { + if err = v.Val().(Record).Struct(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(result) { + // There's no relational data. + continue + } + v := result[i] + if v == nil { + // There's no relational data. + continue + } + if err = v.Struct(element); err != nil { + return err + } + } + // Recursively Scan. + if model != nil { + if err = model.doWithScanStruct(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[gconv.String(relationFromAttrField.Interface())] + if relationDataItem == nil { + // There's no relational data. + continue + } + if relationDataItem.IsSlice() { + if err = relationDataItem.Slice()[0].(Record).Struct(bindToAttrValue); err != nil { + return err + } + } else { + if err = relationDataItem.Val().(Record).Struct(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(result) { + // There's no relational data. + continue + } + relationDataItem := result[i] + if relationDataItem == nil { + // There's no relational data. + continue + } + if err = relationDataItem.Struct(bindToAttrValue); err != nil { + return err + } + } + // Recursively Scan. + if model != nil { + if err = model.doWithScanStruct(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 +} From 38b797b42fbd5a454d396a634ae2a15c56f09786 Mon Sep 17 00:00:00 2001 From: mingzaily Date: Sat, 6 Nov 2021 16:27:17 +0800 Subject: [PATCH 04/15] finish example for gqueue --- container/gqueue/gqueue_z_example_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/container/gqueue/gqueue_z_example_test.go b/container/gqueue/gqueue_z_example_test.go index ba99c51d9..119b23d2c 100644 --- a/container/gqueue/gqueue_z_example_test.go +++ b/container/gqueue/gqueue_z_example_test.go @@ -111,13 +111,8 @@ func ExampleQueue_Len() { q.Push(1) q.Push(2) - time.Sleep(time.Millisecond) - fmt.Println(q.Len()) - q.Pop() - q.Close() fmt.Println(q.Len()) // Output: // 2 - // 0 } From 42c4b32720925df525be5b1e822aa46719da7c2a Mon Sep 17 00:00:00 2001 From: John Guo Date: Sun, 7 Nov 2021 00:16:14 +0800 Subject: [PATCH 05/15] add gconv.ScanList --- .../gdb_z_mysql_association_scanlist_test.go | 2 +- internal/utils/utils_list.go | 37 +++ internal/utils/utils_map.go | 26 ++ text/gstr/gstr.go | 9 +- util/gconv/gconv_scan.go | 141 +++++---- util/gconv/gconv_z_unit_scan_test.go | 282 ++++++++++++++++++ util/gutil/gutil_dump.go | 7 +- util/gutil/gutil_list.go | 7 + util/gutil/gutil_map.go | 14 +- 9 files changed, 438 insertions(+), 87 deletions(-) create mode 100644 internal/utils/utils_list.go create mode 100644 internal/utils/utils_map.go 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 76ebcb149..5472bdd83 100644 --- a/util/gconv/gconv_scan.go +++ b/util/gconv/gconv_scan.go @@ -11,8 +11,7 @@ import ( "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/text/gstr" - "github.com/gogf/gf/v2/util/gutil" + "github.com/gogf/gf/v2/internal/utils" "reflect" ) @@ -42,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 ( @@ -99,7 +109,7 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string) } } -// ScanList converts `data` to struct slice which contains other complex struct attributes. +// 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: @@ -123,11 +133,11 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string) // UserScores []*EntityUserScores // } // var users []*Entity -// ScanList(&users, "User") -// ScanList(&users, "User", "uid") -// ScanList(&users, "UserDetail", "User", "uid:Uid") -// ScanList(&users, "UserScores", "User", "uid:Uid") -// ScanList(&users, "UserScores", "User", "uid") +// 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: @@ -151,10 +161,21 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string) // } // // var users []*Entity -// ScanList(&users) -// ScanList(&users, "UserDetail", "uid") -// ScanList(&users, "UserScores", "uid") -func ScanList(data interface{}, structSlicePointer interface{}, bindToAttrName string, relationAttrNameAndFields ...string) (err error) { +// 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 @@ -166,15 +187,16 @@ func ScanList(data interface{}, structSlicePointer interface{}, bindToAttrName s case 1: relationFields = relationAttrNameAndFields[0] } - return doScanList(data, structSlicePointer, bindToAttrName, relationAttrName, relationFields) + return doScanList(structSlice, structSlicePointer, bindToAttrName, relationAttrName, relationFields) } -// doScanList converts `data` to struct slice which contains other complex struct attributes recursively. -// The parameter `model` is used for recursively scanning purpose, which means, it can scan the attribute struct/structs recursively, -// but it needs the Model for database accessing. +// 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(data interface{}, structSlicePointer interface{}, bindToAttrName, relationAttrName, relationFields string) (err error) { - if data == nil { +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. @@ -210,6 +232,19 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName 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 @@ -226,17 +261,17 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName // Relation variables. var ( - relationDataMap map[string]Value + 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 := gstr.SplitAndTrim(relationFields, "=") + array := utils.SplitAndTrim(relationFields, "=") if len(array) == 1 { // Compatible with old splitting char ':'. - array = gstr.SplitAndTrim(relationFields, ":") + array = utils.SplitAndTrim(relationFields, ":") } if len(array) == 1 { // The relation names are the same. @@ -249,7 +284,7 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName // uid:UserId relationFromFieldName = array[0] relationBindToFieldName = array[1] - if key, _ := gutil.MapPossibleItemByKey(result[0].Map(), relationFromFieldName); key == "" { + 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"`, @@ -267,7 +302,7 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName } if relationFromFieldName != "" { // Note that the value might be type of slice. - relationDataMap = result.MapKeyValue(relationFromFieldName) + relationDataMap = utils.ListToMapByKey(maps, relationFromFieldName) } if len(relationDataMap) == 0 { return gerror.NewCodef( @@ -353,7 +388,7 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName RecursiveOption: structs.RecursiveOptionEmbeddedNoTag, }) ) - if key, _ := gutil.MapPossibleItemByKey(gconv.Map(filedMap), relationBindToFieldName); key == "" { + 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"`, @@ -371,19 +406,15 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName if len(relationDataMap) > 0 { relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) if relationFromAttrField.IsValid() { - results := make(Result, 0) - for _, v := range relationDataMap[gconv.String(relationFromAttrField.Interface())].Slice() { - results = append(results, v.(Record)) + //results := make(Result, 0) + results := make([]interface{}, 0) + for _, v := range SliceAny(relationDataMap[String(relationFromAttrField.Interface())]) { + item := v + results = append(results, item) } - if err = results.Structs(bindToAttrValue.Addr()); err != nil { + if err = Structs(results, bindToAttrValue.Addr()); err != nil { return err } - // Recursively Scan. - if model != nil { - if err = model.doWithScanStructs(bindToAttrValue.Addr()); err != nil { - return nil - } - } } else { // Maybe the attribute does not exist yet. return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) @@ -406,17 +437,17 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName if len(relationDataMap) > 0 { relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) if relationFromAttrField.IsValid() { - v := relationDataMap[gconv.String(relationFromAttrField.Interface())] + v := relationDataMap[String(relationFromAttrField.Interface())] if v == nil { // There's no relational data. continue } - if v.IsSlice() { - if err = v.Slice()[0].(Record).Struct(element); err != nil { + if utils.IsSlice(v) { + if err = Struct(SliceAny(v)[0], element); err != nil { return err } } else { - if err = v.Val().(Record).Struct(element); err != nil { + if err = Struct(v, element); err != nil { return err } } @@ -425,22 +456,16 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) } } else { - if i >= len(result) { + if i >= len(maps) { // There's no relational data. continue } - v := result[i] + v := maps[i] if v == nil { // There's no relational data. continue } - if err = v.Struct(element); err != nil { - return err - } - } - // Recursively Scan. - if model != nil { - if err = model.doWithScanStruct(element); err != nil { + if err = Struct(v, element); err != nil { return err } } @@ -450,17 +475,17 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName if len(relationDataMap) > 0 { relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName) if relationFromAttrField.IsValid() { - relationDataItem := relationDataMap[gconv.String(relationFromAttrField.Interface())] + relationDataItem := relationDataMap[String(relationFromAttrField.Interface())] if relationDataItem == nil { // There's no relational data. continue } - if relationDataItem.IsSlice() { - if err = relationDataItem.Slice()[0].(Record).Struct(bindToAttrValue); err != nil { + if utils.IsSlice(relationDataItem) { + if err = Struct(SliceAny(relationDataItem)[0], bindToAttrValue); err != nil { return err } } else { - if err = relationDataItem.Val().(Record).Struct(bindToAttrValue); err != nil { + if err = Struct(relationDataItem, bindToAttrValue); err != nil { return err } } @@ -469,22 +494,16 @@ func doScanList(data interface{}, structSlicePointer interface{}, bindToAttrName return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields) } } else { - if i >= len(result) { + if i >= len(maps) { // There's no relational data. continue } - relationDataItem := result[i] + relationDataItem := maps[i] if relationDataItem == nil { // There's no relational data. continue } - if err = relationDataItem.Struct(bindToAttrValue); err != nil { - return err - } - } - // Recursively Scan. - if model != nil { - if err = model.doWithScanStruct(bindToAttrValue); err != nil { + if err = Struct(relationDataItem, bindToAttrValue); err != nil { return err } } 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..0403ca55d 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -10,7 +10,6 @@ import ( "bytes" "fmt" "github.com/gogf/gf/v2/internal/structs" - "github.com/gogf/gf/v2/text/gstr" "reflect" "strings" ) @@ -151,7 +150,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 +158,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) @@ -202,7 +201,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`. From 3ca2cad449565ee66007dda3796518585c086bd0 Mon Sep 17 00:00:00 2001 From: John Guo Date: Sun, 7 Nov 2021 00:32:16 +0800 Subject: [PATCH 06/15] improve gutil.Dump --- util/gutil/gutil_dump.go | 38 +++++++- util/gutil/gutil_z_unit_dump_test.go | 128 +++++++++++++++++++++++++++ util/gutil/gutil_z_unit_test.go | 117 ------------------------ 3 files changed, 163 insertions(+), 120 deletions(-) create mode 100755 util/gutil/gutil_z_unit_dump_test.go diff --git a/util/gutil/gutil_dump.go b/util/gutil/gutil_dump.go index 0403ca55d..0e331426f 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -10,10 +10,21 @@ import ( "bytes" "fmt" "github.com/gogf/gf/v2/internal/structs" + "github.com/gogf/gf/v2/text/gstr" "reflect" "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. @@ -172,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 } 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` From df02ca764fffb64898ef75663248afe9dbc53243 Mon Sep 17 00:00:00 2001 From: mingzaily Date: Sun, 7 Nov 2021 16:52:28 +0800 Subject: [PATCH 07/15] add size example for gqueue --- container/gqueue/gqueue_z_example_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/container/gqueue/gqueue_z_example_test.go b/container/gqueue/gqueue_z_example_test.go index 119b23d2c..cb2b4696d 100644 --- a/container/gqueue/gqueue_z_example_test.go +++ b/container/gqueue/gqueue_z_example_test.go @@ -116,3 +116,16 @@ func ExampleQueue_Len() { // Output: // 2 } + +func ExampleQueue_Size() { + q := gqueue.New() + + q.Push(1) + q.Push(2) + + // Size is alias of Len. + fmt.Println(q.Size()) + + // Output: + // 2 +} From 748040fb0b886cb77fcabfa24293a6d9252f3f35 Mon Sep 17 00:00:00 2001 From: John Guo Date: Sun, 7 Nov 2021 21:31:33 +0800 Subject: [PATCH 08/15] improve group router for package ghttp --- frame/gins/gins_server.go | 2 +- net/ghttp/ghttp_server_domain.go | 57 +++-- net/ghttp/ghttp_server_router.go | 46 +++- net/ghttp/ghttp_server_router_group.go | 158 +++++++----- net/ghttp/ghttp_server_router_hook.go | 43 +++- net/ghttp/ghttp_server_router_middleware.go | 32 ++- net/ghttp/ghttp_server_service_handler.go | 54 +++-- net/ghttp/ghttp_server_service_object.go | 225 ++++++++++-------- .../ghttp_unit_router_domain_basic_test.go | 8 +- .../ghttp_unit_router_group_hook_test.go | 30 --- net/ghttp/ghttp_unit_router_group_test.go | 33 --- ...ghttp_unit_router_handler_extended_test.go | 136 +++++++++++ protocol/goai/goai.go | 1 + 13 files changed, 545 insertions(+), 280 deletions(-) diff --git a/frame/gins/gins_server.go b/frame/gins/gins_server.go index 91b0fe38f..e2f35cf46 100644 --- a/frame/gins/gins_server.go +++ b/frame/gins/gins_server.go @@ -60,7 +60,7 @@ func Server(name ...interface{}) *ghttp.Server { } } else { // The configuration is not necessary, so it just prints internal logs. - intlog.Printf(ctx, `missing configuration for HTTP server "%s"`, instanceName) + intlog.Printf(ctx, `missing configuration from configuration component for HTTP server "%s"`, instanceName) } // Server logger configuration checks. diff --git a/net/ghttp/ghttp_server_domain.go b/net/ghttp/ghttp_server_domain.go index e160c0687..74a77c4a1 100644 --- a/net/ghttp/ghttp_server_domain.go +++ b/net/ghttp/ghttp_server_domain.go @@ -35,9 +35,15 @@ func (d *Domain) BindHandler(pattern string, handler interface{}) { } } -func (d *Domain) doBindHandler(ctx context.Context, pattern string, funcInfo handlerFuncInfo, middleware []HandlerFunc, source string) { +func (d *Domain) doBindHandler(ctx context.Context, in doBindHandlerInput) { for domain, _ := range d.domains { - d.server.doBindHandler(ctx, pattern+"@"+domain, funcInfo, middleware, source) + d.server.doBindHandler(ctx, doBindHandlerInput{ + Prefix: in.Prefix, + Pattern: in.Pattern + "@" + domain, + FuncInfo: in.FuncInfo, + Middleware: in.Middleware, + Source: in.Source, + }) } } @@ -47,9 +53,16 @@ func (d *Domain) BindObject(pattern string, obj interface{}, methods ...string) } } -func (d *Domain) doBindObject(ctx context.Context, pattern string, obj interface{}, methods string, middleware []HandlerFunc, source string) { +func (d *Domain) doBindObject(ctx context.Context, in doBindObjectInput) { for domain, _ := range d.domains { - d.server.doBindObject(ctx, pattern+"@"+domain, obj, methods, middleware, source) + d.server.doBindObject(ctx, doBindObjectInput{ + Prefix: in.Prefix, + Pattern: in.Pattern + "@" + domain, + Object: in.Object, + Method: in.Method, + Middleware: in.Middleware, + Source: in.Source, + }) } } @@ -59,13 +72,16 @@ func (d *Domain) BindObjectMethod(pattern string, obj interface{}, method string } } -func (d *Domain) doBindObjectMethod( - ctx context.Context, - pattern string, obj interface{}, method string, - middleware []HandlerFunc, source string, -) { +func (d *Domain) doBindObjectMethod(ctx context.Context, in doBindObjectMethodInput) { for domain, _ := range d.domains { - d.server.doBindObjectMethod(ctx, pattern+"@"+domain, obj, method, middleware, source) + d.server.doBindObjectMethod(ctx, doBindObjectMethodInput{ + Prefix: in.Prefix, + Pattern: in.Pattern + "@" + domain, + Object: in.Object, + Method: in.Method, + Middleware: in.Middleware, + Source: in.Source, + }) } } @@ -75,9 +91,16 @@ func (d *Domain) BindObjectRest(pattern string, obj interface{}) { } } -func (d *Domain) doBindObjectRest(ctx context.Context, pattern string, obj interface{}, middleware []HandlerFunc, source string) { +func (d *Domain) doBindObjectRest(ctx context.Context, in doBindObjectInput) { for domain, _ := range d.domains { - d.server.doBindObjectRest(ctx, pattern+"@"+domain, obj, middleware, source) + d.server.doBindObjectRest(ctx, doBindObjectInput{ + Prefix: in.Prefix, + Pattern: in.Pattern + "@" + domain, + Object: in.Object, + Method: in.Method, + Middleware: in.Middleware, + Source: in.Source, + }) } } @@ -87,9 +110,15 @@ func (d *Domain) BindHookHandler(pattern string, hook string, handler HandlerFun } } -func (d *Domain) doBindHookHandler(ctx context.Context, pattern string, hook string, handler HandlerFunc, source string) { +func (d *Domain) doBindHookHandler(ctx context.Context, in doBindHookHandlerInput) { for domain, _ := range d.domains { - d.server.doBindHookHandler(ctx, pattern+"@"+domain, hook, handler, source) + d.server.doBindHookHandler(ctx, doBindHookHandlerInput{ + Prefix: in.Prefix, + Pattern: in.Pattern + "@" + domain, + HookName: in.HookName, + Handler: in.Handler, + Source: in.Source, + }) } } diff --git a/net/ghttp/ghttp_server_router.go b/net/ghttp/ghttp_server_router.go index ac6b361ca..50be1545e 100644 --- a/net/ghttp/ghttp_server_router.go +++ b/net/ghttp/ghttp_server_router.go @@ -12,6 +12,9 @@ import ( "github.com/gogf/gf/v2/container/gtype" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/protocol/goai" + "github.com/gogf/gf/v2/util/gmeta" + "reflect" "strings" "github.com/gogf/gf/v2/debug/gdebug" @@ -63,11 +66,22 @@ func (s *Server) parsePattern(pattern string) (domain, method, path string, err return } +type setHandlerInput struct { + Prefix string + Pattern string + HandlerItem *handlerItem +} + // setHandler creates router item with given handler and pattern and registers the handler to the router tree. // The router tree can be treated as a multilayer hash table, please refer to the comment in following codes. // This function is called during server starts up, which cares little about the performance. What really cares // is the well-designed router storage structure for router searching when the request is under serving. -func (s *Server) setHandler(ctx context.Context, pattern string, handler *handlerItem) { +func (s *Server) setHandler(ctx context.Context, in setHandlerInput) { + var ( + prefix = in.Prefix + pattern = in.Pattern + handler = in.HandlerItem + ) handler.Id = handlerIdGenerator.Add(1) if handler.Source == "" { _, file, line := gdebug.CallerWithFilter(stackFilterKey) @@ -78,6 +92,28 @@ func (s *Server) setHandler(ctx context.Context, pattern string, handler *handle s.Logger().Fatalf(ctx, `invalid pattern "%s", %+v`, pattern, err) return } + + // Change the registered route according meta info from its request structure. + if handler.Info.Type != nil && handler.Info.Type.NumIn() == 2 { + var ( + objectReq = reflect.New(handler.Info.Type.In(1)) + ) + if v := gmeta.Get(objectReq, goai.TagNamePath); !v.IsEmpty() { + uri = v.String() + } + if v := gmeta.Get(objectReq, goai.TagNameMethod); !v.IsEmpty() { + method = v.String() + } + if v := gmeta.Get(objectReq, goai.TagNameDomain); !v.IsEmpty() { + domain = v.String() + } + } + + // Prefix for URI feature. + if prefix != "" { + uri = prefix + "/" + strings.TrimLeft(uri, "/") + } + if len(uri) == 0 || uri[0] != '/' { s.Logger().Fatalf(ctx, `invalid pattern "%s", URI should lead with '/'`, pattern) return @@ -177,7 +213,7 @@ func (s *Server) setHandler(ctx context.Context, pattern string, handler *handle for e := l.Front(); e != nil; e = e.Next() { item = e.Value.(*handlerItem) // Checks the priority whether inserting the route item before current item, - // which means it has more higher priority. + // which means it has higher priority. if s.compareRouterPriority(handler, item) { l.InsertBefore(e, handler) pushed = true @@ -211,11 +247,11 @@ func (s *Server) setHandler(ctx context.Context, pattern string, handler *handle // compareRouterPriority compares the priority between `newItem` and `oldItem`. It returns true // if `newItem`'s priority is higher than `oldItem`, else it returns false. The higher priority -// item will be insert into the router list before the other one. +// item will be inserted into the router list before the other one. // // Comparison rules: // 1. The middleware has the most high priority. -// 2. URI: The deeper the higher (simply check the count of char '/' in the URI). +// 2. URI: The deeper, the higher (simply check the count of char '/' in the URI). // 3. Route type: {xxx} > :xxx > *xxx. func (s *Server) compareRouterPriority(newItem *handlerItem, oldItem *handlerItem) bool { // If they're all type of middleware, the priority is according their registered sequence. @@ -226,7 +262,7 @@ func (s *Server) compareRouterPriority(newItem *handlerItem, oldItem *handlerIte if newItem.Type == HandlerTypeMiddleware && oldItem.Type != HandlerTypeMiddleware { return true } - // URI: The deeper the higher (simply check the count of char '/' in the URI). + // URI: The deeper, the higher (simply check the count of char '/' in the URI). if newItem.Router.Priority > oldItem.Router.Priority { return true } diff --git a/net/ghttp/ghttp_server_router_group.go b/net/ghttp/ghttp_server_router_group.go index 4f88fde2f..1e5d7d7e2 100644 --- a/net/ghttp/ghttp_server_router_group.go +++ b/net/ghttp/ghttp_server_router_group.go @@ -10,10 +10,9 @@ import ( "context" "fmt" "github.com/gogf/gf/v2/debug/gdebug" - "reflect" - "strings" - + "github.com/gogf/gf/v2/internal/utils" "github.com/gogf/gf/v2/text/gstr" + "reflect" "github.com/gogf/gf/v2/util/gconv" ) @@ -28,9 +27,6 @@ type ( middleware []HandlerFunc // Middleware array. } - // GroupItem is item for router group. - GroupItem = []interface{} - // preBindItem is item for lazy registering feature of router group. preBindItem is not really registered // to server when route function of the group called but is lazily registered when server starts. preBindItem struct { @@ -104,16 +100,17 @@ func (d *Domain) Group(prefix string, groups ...func(group *RouterGroup)) *Route if prefix == "/" { prefix = "" } - group := &RouterGroup{ + routerGroup := &RouterGroup{ domain: d, + server: d.server, prefix: prefix, } if len(groups) > 0 { - for _, v := range groups { - v(group) + for _, nestedGroup := range groups { + nestedGroup(routerGroup) } } - return group + return routerGroup } // Group creates and returns a sub-group of current router group. @@ -153,34 +150,26 @@ func (g *RouterGroup) Clone() *RouterGroup { } // Bind does batch route registering feature for router group. -func (g *RouterGroup) Bind(items []GroupItem) *RouterGroup { +func (g *RouterGroup) Bind(handlerOrObject ...interface{}) *RouterGroup { var ( ctx = context.TODO() group = g.Clone() ) - for _, item := range items { - if len(item) < 3 { - g.server.Logger().Fatalf(ctx, "invalid router item: %s", item) - } - bindType := gstr.ToUpper(gconv.String(item[0])) - switch bindType { - case groupBindTypeRest: - group.preBindToLocalArray(groupBindTypeRest, gconv.String(item[0])+":"+gconv.String(item[1]), item[2]) - - case groupBindTypeMiddleware: - group.preBindToLocalArray(groupBindTypeMiddleware, gconv.String(item[0])+":"+gconv.String(item[1]), item[2]) + for _, v := range handlerOrObject { + var ( + item = v + originValueAndKind = utils.OriginValueAndKind(item) + ) + switch originValueAndKind.OriginKind { + case reflect.Func, reflect.Struct: + group = group.preBindToLocalArray( + groupBindTypeHandler, + "/", + item, + ) default: - if strings.EqualFold(bindType, "ALL") { - bindType = "" - } else { - bindType += ":" - } - if len(item) > 3 { - group.preBindToLocalArray(groupBindTypeHandler, bindType+gconv.String(item[1]), item[2], item[3]) - } else { - group.preBindToLocalArray(groupBindTypeHandler, bindType+gconv.String(item[1]), item[2]) - } + g.server.Logger().Fatalf(ctx, "invalid bind parameter type: %v", originValueAndKind.InputValue.Type()) } } return group @@ -188,7 +177,12 @@ func (g *RouterGroup) Bind(items []GroupItem) *RouterGroup { // ALL registers a http handler to given route pattern and all http methods. func (g *RouterGroup) ALL(pattern string, object interface{}, params ...interface{}) *RouterGroup { - return g.Clone().preBindToLocalArray(groupBindTypeHandler, defaultMethod+":"+pattern, object, params...) + return g.Clone().preBindToLocalArray( + groupBindTypeHandler, + defaultMethod+":"+pattern, + object, + params..., + ) } // ALLMap registers http handlers for http methods using map. @@ -312,10 +306,10 @@ func (g *RouterGroup) doBindRoutersToServer(ctx context.Context, item *preBindIt domain = "" } if bindType == groupBindTypeRest { - pattern = prefix + "/" + strings.TrimLeft(path, "/") + pattern = path } else { pattern = g.server.serveHandlerKey( - method, prefix+"/"+strings.TrimLeft(path, "/"), domain, + method, path, domain, ) } } @@ -337,57 +331,95 @@ func (g *RouterGroup) doBindRoutersToServer(ctx context.Context, item *preBindIt g.server.Logger().Fatal(ctx, err.Error()) return g } - if g.server != nil { - g.server.doBindHandler(ctx, pattern, funcInfo, g.middleware, source) + in := doBindHandlerInput{ + Prefix: prefix, + Pattern: pattern, + FuncInfo: funcInfo, + Middleware: g.middleware, + Source: source, + } + if g.domain != nil { + g.domain.doBindHandler(ctx, in) } else { - g.domain.doBindHandler(ctx, pattern, funcInfo, g.middleware, source) + g.server.doBindHandler(ctx, in) } } else { if len(extras) > 0 { - if g.server != nil { - if gstr.Contains(extras[0], ",") { - g.server.doBindObject( - ctx, pattern, object, extras[0], g.middleware, source, - ) + if gstr.Contains(extras[0], ",") { + in := doBindObjectInput{ + Prefix: prefix, + Pattern: pattern, + Object: object, + Method: extras[0], + Middleware: g.middleware, + Source: source, + } + if g.domain != nil { + g.domain.doBindObject(ctx, in) } else { - g.server.doBindObjectMethod( - ctx, pattern, object, extras[0], g.middleware, source, - ) + g.server.doBindObject(ctx, in) } } else { - if gstr.Contains(extras[0], ",") { - g.domain.doBindObject( - ctx, pattern, object, extras[0], g.middleware, source, - ) + in := doBindObjectMethodInput{ + Prefix: prefix, + Pattern: pattern, + Object: object, + Method: extras[0], + Middleware: g.middleware, + Source: source, + } + if g.domain != nil { + g.domain.doBindObjectMethod(ctx, in) } else { - g.domain.doBindObjectMethod( - ctx, pattern, object, extras[0], g.middleware, source, - ) + g.server.doBindObjectMethod(ctx, in) } } } else { + in := doBindObjectInput{ + Prefix: prefix, + Pattern: pattern, + Object: object, + Method: "", + Middleware: g.middleware, + Source: source, + } // At last, it treats the `object` as Object registering type. - if g.server != nil { - g.server.doBindObject(ctx, pattern, object, "", g.middleware, source) + if g.domain != nil { + g.domain.doBindObject(ctx, in) } else { - g.domain.doBindObject(ctx, pattern, object, "", g.middleware, source) + g.server.doBindObject(ctx, in) } } } case groupBindTypeRest: - if g.server != nil { - g.server.doBindObjectRest(ctx, pattern, object, g.middleware, source) + in := doBindObjectInput{ + Prefix: prefix, + Pattern: pattern, + Object: object, + Method: "", + Middleware: g.middleware, + Source: source, + } + if g.domain != nil { + g.domain.doBindObjectRest(ctx, in) } else { - g.domain.doBindObjectRest(ctx, pattern, object, g.middleware, source) + g.server.doBindObjectRest(ctx, in) } case groupBindTypeHook: - if h, ok := object.(HandlerFunc); ok { - if g.server != nil { - g.server.doBindHookHandler(ctx, pattern, extras[0], h, source) + if handler, ok := object.(HandlerFunc); ok { + in := doBindHookHandlerInput{ + Prefix: prefix, + Pattern: pattern, + HookName: extras[0], + Handler: handler, + Source: source, + } + if g.domain != nil { + g.domain.doBindHookHandler(ctx, in) } else { - g.domain.doBindHookHandler(ctx, pattern, extras[0], h, source) + g.server.doBindHookHandler(ctx, in) } } else { g.server.Logger().Fatalf(ctx, "invalid hook handler for pattern: %s", pattern) diff --git a/net/ghttp/ghttp_server_router_hook.go b/net/ghttp/ghttp_server_router_hook.go index 2138af086..2be2dd087 100644 --- a/net/ghttp/ghttp_server_router_hook.go +++ b/net/ghttp/ghttp_server_router_hook.go @@ -15,20 +15,41 @@ import ( // BindHookHandler registers handler for specified hook. func (s *Server) BindHookHandler(pattern string, hook string, handler HandlerFunc) { - s.doBindHookHandler(context.TODO(), pattern, hook, handler, "") + s.doBindHookHandler(context.TODO(), doBindHookHandlerInput{ + Prefix: "", + Pattern: pattern, + HookName: hook, + Handler: handler, + Source: "", + }) } -func (s *Server) doBindHookHandler(ctx context.Context, pattern string, hook string, handler HandlerFunc, source string) { - s.setHandler(ctx, pattern, &handlerItem{ - Type: HandlerTypeHook, - Name: gdebug.FuncPath(handler), - Info: handlerFuncInfo{ - Func: handler, - Type: reflect.TypeOf(handler), +type doBindHookHandlerInput struct { + Prefix string + Pattern string + HookName string + Handler HandlerFunc + Source string +} + +func (s *Server) doBindHookHandler(ctx context.Context, in doBindHookHandlerInput) { + s.setHandler( + ctx, + setHandlerInput{ + Prefix: in.Prefix, + Pattern: in.Pattern, + HandlerItem: &handlerItem{ + Type: HandlerTypeHook, + Name: gdebug.FuncPath(in.Handler), + Info: handlerFuncInfo{ + Func: in.Handler, + Type: reflect.TypeOf(in.Handler), + }, + HookName: in.HookName, + Source: in.Source, + }, }, - HookName: hook, - Source: source, - }) + ) } func (s *Server) BindHookHandlerByMap(pattern string, hookMap map[string]HandlerFunc) { diff --git a/net/ghttp/ghttp_server_router_middleware.go b/net/ghttp/ghttp_server_router_middleware.go index 2e4c99f0b..99fb2acaf 100644 --- a/net/ghttp/ghttp_server_router_middleware.go +++ b/net/ghttp/ghttp_server_router_middleware.go @@ -26,12 +26,16 @@ func (s *Server) BindMiddleware(pattern string, handlers ...HandlerFunc) { ctx = context.TODO() ) for _, handler := range handlers { - s.setHandler(ctx, pattern, &handlerItem{ - Type: HandlerTypeMiddleware, - Name: gdebug.FuncPath(handler), - Info: handlerFuncInfo{ - Func: handler, - Type: reflect.TypeOf(handler), + s.setHandler(ctx, setHandlerInput{ + Prefix: "", + Pattern: pattern, + HandlerItem: &handlerItem{ + Type: HandlerTypeMiddleware, + Name: gdebug.FuncPath(handler), + Info: handlerFuncInfo{ + Func: handler, + Type: reflect.TypeOf(handler), + }, }, }) } @@ -45,12 +49,16 @@ func (s *Server) BindMiddlewareDefault(handlers ...HandlerFunc) { ctx = context.TODO() ) for _, handler := range handlers { - s.setHandler(ctx, defaultMiddlewarePattern, &handlerItem{ - Type: HandlerTypeMiddleware, - Name: gdebug.FuncPath(handler), - Info: handlerFuncInfo{ - Func: handler, - Type: reflect.TypeOf(handler), + s.setHandler(ctx, setHandlerInput{ + Prefix: "", + Pattern: defaultMiddlewarePattern, + HandlerItem: &handlerItem{ + Type: HandlerTypeMiddleware, + Name: gdebug.FuncPath(handler), + Info: handlerFuncInfo{ + Func: handler, + Type: reflect.TypeOf(handler), + }, }, }) } diff --git a/net/ghttp/ghttp_server_service_handler.go b/net/ghttp/ghttp_server_service_handler.go index 0c87469e9..281f3a45c 100644 --- a/net/ghttp/ghttp_server_service_handler.go +++ b/net/ghttp/ghttp_server_service_handler.go @@ -19,9 +19,10 @@ import ( ) // BindHandler registers a handler function to server with given pattern. -// The parameter `handler` can be type of: -// func(*ghttp.Request) -// func(context.Context, Request)(Response, error) +// +// Note that the parameter `handler` can be type of: +// 1. func(*ghttp.Request) +// 2. func(context.Context, BizRequest)(BizResponse, error) func (s *Server) BindHandler(pattern string, handler interface{}) { var ( ctx = context.TODO() @@ -30,26 +31,49 @@ func (s *Server) BindHandler(pattern string, handler interface{}) { if err != nil { s.Logger().Fatalf(ctx, `%+v`, err) } - s.doBindHandler(ctx, pattern, funcInfo, nil, "") + s.doBindHandler(ctx, doBindHandlerInput{ + Prefix: "", + Pattern: pattern, + FuncInfo: funcInfo, + Middleware: nil, + Source: "", + }) +} + +type doBindHandlerInput struct { + Prefix string + Pattern string + FuncInfo handlerFuncInfo + Middleware []HandlerFunc + Source string } // doBindHandler registers a handler function to server with given pattern. +// // The parameter `pattern` is like: // /user/list, put:/user, delete:/user, post:/user@goframe.org -func (s *Server) doBindHandler(ctx context.Context, pattern string, funcInfo handlerFuncInfo, middleware []HandlerFunc, source string) { - s.setHandler(ctx, pattern, &handlerItem{ - Name: gdebug.FuncPath(funcInfo.Func), - Type: HandlerTypeHandler, - Info: funcInfo, - Middleware: middleware, - Source: source, +func (s *Server) doBindHandler(ctx context.Context, in doBindHandlerInput) { + s.setHandler(ctx, setHandlerInput{ + Prefix: in.Prefix, + Pattern: in.Pattern, + HandlerItem: &handlerItem{ + Name: gdebug.FuncPath(in.FuncInfo.Func), + Type: HandlerTypeHandler, + Info: in.FuncInfo, + Middleware: in.Middleware, + Source: in.Source, + }, }) } // bindHandlerByMap registers handlers to server using map. -func (s *Server) bindHandlerByMap(ctx context.Context, m map[string]*handlerItem) { - for p, h := range m { - s.setHandler(ctx, p, h) +func (s *Server) bindHandlerByMap(ctx context.Context, prefix string, m map[string]*handlerItem) { + for pattern, handler := range m { + s.setHandler(ctx, setHandlerInput{ + Prefix: prefix, + Pattern: pattern, + HandlerItem: handler, + }) } } @@ -162,6 +186,7 @@ func (s *Server) checkAndCreateFuncInfo(f interface{}, pkgPath, structName, meth return } + // The request struct should be named as `xxxReq`. if !gstr.HasSuffix(reflectType.In(1).String(), `Req`) { err = gerror.NewCodef( gcode.CodeInvalidParameter, @@ -171,6 +196,7 @@ func (s *Server) checkAndCreateFuncInfo(f interface{}, pkgPath, structName, meth return } + // The response struct should be named as `xxxRes`. if !gstr.HasSuffix(reflectType.Out(0).String(), `Res`) { err = gerror.NewCodef( gcode.CodeInvalidParameter, diff --git a/net/ghttp/ghttp_server_service_object.go b/net/ghttp/ghttp_server_service_object.go index eea76edcd..13e141fab 100644 --- a/net/ghttp/ghttp_server_service_object.go +++ b/net/ghttp/ghttp_server_service_object.go @@ -21,8 +21,6 @@ import ( // // The optional parameter `method` is used to specify the method to be registered, which // supports multiple method names, multiple methods are separated by char ',', case-sensitive. -// -// Note that the route method should be defined as ghttp.HandlerFunc. func (s *Server) BindObject(pattern string, object interface{}, method ...string) { var ( bindMethod = "" @@ -30,97 +28,124 @@ func (s *Server) BindObject(pattern string, object interface{}, method ...string if len(method) > 0 { bindMethod = method[0] } - s.doBindObject(context.TODO(), pattern, object, bindMethod, nil, "") + s.doBindObject(context.TODO(), doBindObjectInput{ + Prefix: "", + Pattern: pattern, + Object: object, + Method: bindMethod, + Middleware: nil, + Source: "", + }) } // BindObjectMethod registers specified method of object to server routes with given pattern. // // The optional parameter `method` is used to specify the method to be registered, which // does not supports multiple method names but only one, case-sensitive. -// -// Note that the route method should be defined as ghttp.HandlerFunc. func (s *Server) BindObjectMethod(pattern string, object interface{}, method string) { - s.doBindObjectMethod(context.TODO(), pattern, object, method, nil, "") + s.doBindObjectMethod(context.TODO(), doBindObjectMethodInput{ + Prefix: "", + Pattern: pattern, + Object: object, + Method: method, + Middleware: nil, + Source: "", + }) } // BindObjectRest registers object in REST API styles to server with specified pattern. -// Note that the route method should be defined as ghttp.HandlerFunc. func (s *Server) BindObjectRest(pattern string, object interface{}) { - s.doBindObjectRest(context.TODO(), pattern, object, nil, "") + s.doBindObjectRest(context.TODO(), doBindObjectInput{ + Prefix: "", + Pattern: pattern, + Object: object, + Method: "", + Middleware: nil, + Source: "", + }) } -func (s *Server) doBindObject(ctx context.Context, pattern string, object interface{}, method string, middleware []HandlerFunc, source string) { +type doBindObjectInput struct { + Prefix string + Pattern string + Object interface{} + Method string + Middleware []HandlerFunc + Source string +} + +func (s *Server) doBindObject(ctx context.Context, in doBindObjectInput) { // Convert input method to map for convenience and high performance searching purpose. var ( methodMap map[string]bool ) - if len(method) > 0 { + if len(in.Method) > 0 { methodMap = make(map[string]bool) - for _, v := range strings.Split(method, ",") { + for _, v := range strings.Split(in.Method, ",") { methodMap[strings.TrimSpace(v)] = true } } // If the `method` in `pattern` is `defaultMethod`, // it removes for convenience for next statement control. - domain, method, path, err := s.parsePattern(pattern) + domain, method, path, err := s.parsePattern(in.Pattern) if err != nil { s.Logger().Fatalf(ctx, `%+v`, err) return } if strings.EqualFold(method, defaultMethod) { - pattern = s.serveHandlerKey("", path, domain) + in.Pattern = s.serveHandlerKey("", path, domain) } var ( - m = make(map[string]*handlerItem) - v = reflect.ValueOf(object) - t = v.Type() - initFunc func(*Request) - shutFunc func(*Request) + handlerMap = make(map[string]*handlerItem) + reflectValue = reflect.ValueOf(in.Object) + reflectType = reflectValue.Type() + initFunc func(*Request) + shutFunc func(*Request) ) // If given `object` is not pointer, it then creates a temporary one, // of which the value is `v`. - if v.Kind() == reflect.Struct { - newValue := reflect.New(t) - newValue.Elem().Set(v) - v = newValue - t = v.Type() + if reflectValue.Kind() == reflect.Struct { + newValue := reflect.New(reflectType) + newValue.Elem().Set(reflectValue) + reflectValue = newValue + reflectType = reflectValue.Type() } - structName := t.Elem().Name() - if v.MethodByName("Init").IsValid() { - initFunc = v.MethodByName("Init").Interface().(func(*Request)) + structName := reflectType.Elem().Name() + if reflectValue.MethodByName("Init").IsValid() { + initFunc = reflectValue.MethodByName("Init").Interface().(func(*Request)) } - if v.MethodByName("Shut").IsValid() { - shutFunc = v.MethodByName("Shut").Interface().(func(*Request)) + if reflectValue.MethodByName("Shut").IsValid() { + shutFunc = reflectValue.MethodByName("Shut").Interface().(func(*Request)) } - pkgPath := t.Elem().PkgPath() + pkgPath := reflectType.Elem().PkgPath() pkgName := gfile.Basename(pkgPath) - for i := 0; i < v.NumMethod(); i++ { - methodName := t.Method(i).Name + for i := 0; i < reflectValue.NumMethod(); i++ { + methodName := reflectType.Method(i).Name if methodMap != nil && !methodMap[methodName] { continue } if methodName == "Init" || methodName == "Shut" { continue } - objName := gstr.Replace(t.String(), fmt.Sprintf(`%s.`, pkgName), "") + objName := gstr.Replace(reflectType.String(), fmt.Sprintf(`%s.`, pkgName), "") if objName[0] == '*' { objName = fmt.Sprintf(`(%s)`, objName) } - funcInfo, err := s.checkAndCreateFuncInfo(v.Method(i).Interface(), pkgPath, objName, methodName) + funcInfo, err := s.checkAndCreateFuncInfo(reflectValue.Method(i).Interface(), pkgPath, objName, methodName) if err != nil { s.Logger().Fatalf(ctx, `%+v`, err) } - key := s.mergeBuildInNameToPattern(pattern, structName, methodName, true) - m[key] = &handlerItem{ + key := s.mergeBuildInNameToPattern(in.Pattern, structName, methodName, true) + handlerMap[key] = &handlerItem{ Name: fmt.Sprintf(`%s.%s.%s`, pkgPath, objName, methodName), Type: HandlerTypeObject, Info: funcInfo, InitFunc: initFunc, ShutFunc: shutFunc, - Middleware: middleware, - Source: source, + Middleware: in.Middleware, + Source: in.Source, } // If there's "Index" method, then an additional route is automatically added // to match the main URI, for example: @@ -129,61 +154,70 @@ func (s *Server) doBindObject(ctx context.Context, pattern string, object interf // // Note that if there's built-in variables in pattern, this route will not be added // automatically. - if strings.EqualFold(methodName, "Index") && !gregex.IsMatchString(`\{\.\w+\}`, pattern) { + if strings.EqualFold(methodName, "Index") && !gregex.IsMatchString(`\{\.\w+\}`, in.Pattern) { p := gstr.PosRI(key, "/index") k := key[0:p] + key[p+6:] if len(k) == 0 || k[0] == '@' { k = "/" + k } - m[k] = &handlerItem{ + handlerMap[k] = &handlerItem{ Name: fmt.Sprintf(`%s.%s.%s`, pkgPath, objName, methodName), Type: HandlerTypeObject, Info: funcInfo, InitFunc: initFunc, ShutFunc: shutFunc, - Middleware: middleware, - Source: source, + Middleware: in.Middleware, + Source: in.Source, } } } - s.bindHandlerByMap(ctx, m) + s.bindHandlerByMap(ctx, in.Prefix, handlerMap) } -func (s *Server) doBindObjectMethod(ctx context.Context, pattern string, object interface{}, method string, middleware []HandlerFunc, source string) { +type doBindObjectMethodInput struct { + Prefix string + Pattern string + Object interface{} + Method string + Middleware []HandlerFunc + Source string +} + +func (s *Server) doBindObjectMethod(ctx context.Context, in doBindObjectMethodInput) { var ( - m = make(map[string]*handlerItem) - v = reflect.ValueOf(object) - t = v.Type() - initFunc func(*Request) - shutFunc func(*Request) + handlerMap = make(map[string]*handlerItem) + reflectValue = reflect.ValueOf(in.Object) + reflectType = reflectValue.Type() + initFunc func(*Request) + shutFunc func(*Request) ) // If given `object` is not pointer, it then creates a temporary one, // of which the value is `v`. - if v.Kind() == reflect.Struct { - newValue := reflect.New(t) - newValue.Elem().Set(v) - v = newValue - t = v.Type() + if reflectValue.Kind() == reflect.Struct { + newValue := reflect.New(reflectType) + newValue.Elem().Set(reflectValue) + reflectValue = newValue + reflectType = reflectValue.Type() } var ( - structName = t.Elem().Name() - methodName = strings.TrimSpace(method) - methodValue = v.MethodByName(methodName) + structName = reflectType.Elem().Name() + methodName = strings.TrimSpace(in.Method) + methodValue = reflectValue.MethodByName(methodName) ) if !methodValue.IsValid() { s.Logger().Fatalf(ctx, "invalid method name: %s", methodName) return } - if v.MethodByName("Init").IsValid() { - initFunc = v.MethodByName("Init").Interface().(func(*Request)) + if reflectValue.MethodByName("Init").IsValid() { + initFunc = reflectValue.MethodByName("Init").Interface().(func(*Request)) } - if v.MethodByName("Shut").IsValid() { - shutFunc = v.MethodByName("Shut").Interface().(func(*Request)) + if reflectValue.MethodByName("Shut").IsValid() { + shutFunc = reflectValue.MethodByName("Shut").Interface().(func(*Request)) } var ( - pkgPath = t.Elem().PkgPath() + pkgPath = reflectType.Elem().PkgPath() pkgName = gfile.Basename(pkgPath) - objName = gstr.Replace(t.String(), fmt.Sprintf(`%s.`, pkgName), "") + objName = gstr.Replace(reflectType.String(), fmt.Sprintf(`%s.`, pkgName), "") ) if objName[0] == '*' { objName = fmt.Sprintf(`(%s)`, objName) @@ -194,70 +228,75 @@ func (s *Server) doBindObjectMethod(ctx context.Context, pattern string, object s.Logger().Fatalf(ctx, `%+v`, err) } - key := s.mergeBuildInNameToPattern(pattern, structName, methodName, false) - m[key] = &handlerItem{ + key := s.mergeBuildInNameToPattern(in.Pattern, structName, methodName, false) + handlerMap[key] = &handlerItem{ Name: fmt.Sprintf(`%s.%s.%s`, pkgPath, objName, methodName), Type: HandlerTypeObject, Info: funcInfo, InitFunc: initFunc, ShutFunc: shutFunc, - Middleware: middleware, - Source: source, + Middleware: in.Middleware, + Source: in.Source, } - s.bindHandlerByMap(ctx, m) + s.bindHandlerByMap(ctx, in.Prefix, handlerMap) } -func (s *Server) doBindObjectRest(ctx context.Context, pattern string, object interface{}, middleware []HandlerFunc, source string) { +func (s *Server) doBindObjectRest(ctx context.Context, in doBindObjectInput) { var ( - m = make(map[string]*handlerItem) - v = reflect.ValueOf(object) - t = v.Type() - initFunc func(*Request) - shutFunc func(*Request) + handlerMap = make(map[string]*handlerItem) + reflectValue = reflect.ValueOf(in.Object) + reflectType = reflectValue.Type() + initFunc func(*Request) + shutFunc func(*Request) ) // If given `object` is not pointer, it then creates a temporary one, // of which the value is `v`. - if v.Kind() == reflect.Struct { - newValue := reflect.New(t) - newValue.Elem().Set(v) - v = newValue - t = v.Type() + if reflectValue.Kind() == reflect.Struct { + newValue := reflect.New(reflectType) + newValue.Elem().Set(reflectValue) + reflectValue = newValue + reflectType = reflectValue.Type() } - structName := t.Elem().Name() - if v.MethodByName(methodNameInit).IsValid() { - initFunc = v.MethodByName(methodNameInit).Interface().(func(*Request)) + structName := reflectType.Elem().Name() + if reflectValue.MethodByName(methodNameInit).IsValid() { + initFunc = reflectValue.MethodByName(methodNameInit).Interface().(func(*Request)) } - if v.MethodByName(methodNameShut).IsValid() { - shutFunc = v.MethodByName(methodNameShut).Interface().(func(*Request)) + if reflectValue.MethodByName(methodNameShut).IsValid() { + shutFunc = reflectValue.MethodByName(methodNameShut).Interface().(func(*Request)) } - pkgPath := t.Elem().PkgPath() - for i := 0; i < v.NumMethod(); i++ { - methodName := t.Method(i).Name + pkgPath := reflectType.Elem().PkgPath() + for i := 0; i < reflectValue.NumMethod(); i++ { + methodName := reflectType.Method(i).Name if _, ok := methodsMap[strings.ToUpper(methodName)]; !ok { continue } pkgName := gfile.Basename(pkgPath) - objName := gstr.Replace(t.String(), fmt.Sprintf(`%s.`, pkgName), "") + objName := gstr.Replace(reflectType.String(), fmt.Sprintf(`%s.`, pkgName), "") if objName[0] == '*' { objName = fmt.Sprintf(`(%s)`, objName) } - funcInfo, err := s.checkAndCreateFuncInfo(v.Method(i).Interface(), pkgPath, objName, methodName) + funcInfo, err := s.checkAndCreateFuncInfo( + reflectValue.Method(i).Interface(), + pkgPath, + objName, + methodName, + ) if err != nil { s.Logger().Fatalf(ctx, `%+v`, err) } - key := s.mergeBuildInNameToPattern(methodName+":"+pattern, structName, methodName, false) - m[key] = &handlerItem{ + key := s.mergeBuildInNameToPattern(methodName+":"+in.Pattern, structName, methodName, false) + handlerMap[key] = &handlerItem{ Name: fmt.Sprintf(`%s.%s.%s`, pkgPath, objName, methodName), Type: HandlerTypeObject, Info: funcInfo, InitFunc: initFunc, ShutFunc: shutFunc, - Middleware: middleware, - Source: source, + Middleware: in.Middleware, + Source: in.Source, } } - s.bindHandlerByMap(ctx, m) + s.bindHandlerByMap(ctx, in.Prefix, handlerMap) } diff --git a/net/ghttp/ghttp_unit_router_domain_basic_test.go b/net/ghttp/ghttp_unit_router_domain_basic_test.go index fd96cb3e5..1c589cc22 100644 --- a/net/ghttp/ghttp_unit_router_domain_basic_test.go +++ b/net/ghttp/ghttp_unit_router_domain_basic_test.go @@ -333,16 +333,16 @@ func Test_Router_DomainGroup(t *testing.T) { s := g.Server(p) d := s.Domain("localhost, local") d.Group("/", func(group *ghttp.RouterGroup) { - group.Group("/app", func(gApp *ghttp.RouterGroup) { - gApp.GET("/{table}/list/{page}.html", func(r *ghttp.Request) { + group.Group("/app", func(group *ghttp.RouterGroup) { + group.GET("/{table}/list/{page}.html", func(r *ghttp.Request) { intlog.Print(r.Context(), "/{table}/list/{page}.html") r.Response.Write(r.Get("table"), "&", r.Get("page")) }) - gApp.GET("/order/info/{order_id}", func(r *ghttp.Request) { + group.GET("/order/info/{order_id}", func(r *ghttp.Request) { intlog.Print(r.Context(), "/order/info/{order_id}") r.Response.Write(r.Get("order_id")) }) - gApp.DELETE("/comment/{id}", func(r *ghttp.Request) { + group.DELETE("/comment/{id}", func(r *ghttp.Request) { intlog.Print(r.Context(), "/comment/{id}") r.Response.Write(r.Get("id")) }) diff --git a/net/ghttp/ghttp_unit_router_group_hook_test.go b/net/ghttp/ghttp_unit_router_group_hook_test.go index 9a7d6be9e..edea6a80d 100644 --- a/net/ghttp/ghttp_unit_router_group_hook_test.go +++ b/net/ghttp/ghttp_unit_router_group_hook_test.go @@ -74,33 +74,3 @@ func Test_Router_Group_Hook2(t *testing.T) { t.Assert(client.PostContent(ctx, "/api/ThisDoesNotExist"), "Not Found") }) } - -func Test_Router_Group_Hook3(t *testing.T) { - p, _ := ports.PopRand() - s := g.Server(p) - s.Group("/api").Bind([]g.Slice{ - {"ALL", "handler", func(r *ghttp.Request) { - r.Response.Write("1") - }}, - {"ALL", "/*", func(r *ghttp.Request) { - r.Response.Write("0") - }, ghttp.HookBeforeServe}, - {"ALL", "/*", func(r *ghttp.Request) { - r.Response.Write("2") - }, ghttp.HookAfterServe}, - }) - - s.SetPort(p) - s.SetDumpRouterMap(false) - s.Start() - defer s.Shutdown() - - time.Sleep(100 * time.Millisecond) - gtest.C(t, func(t *gtest.T) { - client := g.Client() - client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) - t.Assert(client.GetContent(ctx, "/api/handler"), "012") - t.Assert(client.PostContent(ctx, "/api/handler"), "012") - t.Assert(client.DeleteContent(ctx, "/api/ThisDoesNotExist"), "02") - }) -} diff --git a/net/ghttp/ghttp_unit_router_group_test.go b/net/ghttp/ghttp_unit_router_group_test.go index ef2dabf12..195d8da99 100644 --- a/net/ghttp/ghttp_unit_router_group_test.go +++ b/net/ghttp/ghttp_unit_router_group_test.go @@ -78,39 +78,6 @@ func Test_Router_GroupBasic1(t *testing.T) { }) } -func Test_Router_GroupBasic2(t *testing.T) { - p, _ := ports.PopRand() - s := g.Server(p) - obj := new(GroupObject) - // 分组路由批量注册 - s.Group("/api").Bind([]g.Slice{ - {"ALL", "/handler", Handler}, - {"ALL", "/obj", obj}, - {"GET", "/obj/my-show", obj, "Show"}, - {"REST", "/obj/rest", obj}, - }) - s.SetPort(p) - s.SetDumpRouterMap(false) - s.Start() - defer s.Shutdown() - - time.Sleep(100 * time.Millisecond) - gtest.C(t, func(t *gtest.T) { - client := g.Client() - client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) - - t.Assert(client.GetContent(ctx, "/api/handler"), "Handler") - - t.Assert(client.GetContent(ctx, "/api/obj/delete"), "1Object Delete2") - t.Assert(client.GetContent(ctx, "/api/obj/my-show"), "1Object Show2") - t.Assert(client.GetContent(ctx, "/api/obj/show"), "1Object Show2") - t.Assert(client.DeleteContent(ctx, "/api/obj/rest"), "1Object Delete2") - - t.Assert(client.DeleteContent(ctx, "/ThisDoesNotExist"), "Not Found") - t.Assert(client.DeleteContent(ctx, "/api/ThisDoesNotExist"), "Not Found") - }) -} - func Test_Router_GroupBuildInVar(t *testing.T) { p, _ := ports.PopRand() s := g.Server(p) diff --git a/net/ghttp/ghttp_unit_router_handler_extended_test.go b/net/ghttp/ghttp_unit_router_handler_extended_test.go index 801b3c358..0b0c3c937 100644 --- a/net/ghttp/ghttp_unit_router_handler_extended_test.go +++ b/net/ghttp/ghttp_unit_router_handler_extended_test.go @@ -59,3 +59,139 @@ func Test_Router_Handler_Extended_Handler_WithObject(t *testing.T) { t.Assert(client.GetContent(ctx, "/test/error"), `{"code":50,"message":"error","data":null}`) }) } + +type TestForHandlerWithObjectAndMeta1Req struct { + g.Meta `path:"/custom-test1" method:"get"` + Age int + Name string +} +type TestForHandlerWithObjectAndMeta1Res struct { + Id int + Age int +} + +type TestForHandlerWithObjectAndMeta2Req struct { + g.Meta `path:"/custom-test2" method:"get"` + Age int + Name string +} +type TestForHandlerWithObjectAndMeta2Res struct { + Id int + Name string +} + +type ControllerForHandlerWithObjectAndMeta1 struct{} + +func (ControllerForHandlerWithObjectAndMeta1) Test1(ctx context.Context, req *TestForHandlerWithObjectAndMeta1Req) (res *TestForHandlerWithObjectAndMeta1Res, err error) { + return &TestForHandlerWithObjectAndMeta1Res{ + Id: 1, + Age: req.Age, + }, nil +} + +func (ControllerForHandlerWithObjectAndMeta1) Test2(ctx context.Context, req *TestForHandlerWithObjectAndMeta2Req) (res *TestForHandlerWithObjectAndMeta2Res, err error) { + return &TestForHandlerWithObjectAndMeta2Res{ + Id: 1, + Name: req.Name, + }, nil +} + +type TestForHandlerWithObjectAndMeta3Req struct { + g.Meta `path:"/custom-test3" method:"get"` + Age int + Name string +} +type TestForHandlerWithObjectAndMeta3Res struct { + Id int + Age int +} + +type TestForHandlerWithObjectAndMeta4Req struct { + g.Meta `path:"/custom-test4" method:"get"` + Age int + Name string +} +type TestForHandlerWithObjectAndMeta4Res struct { + Id int + Name string +} + +type ControllerForHandlerWithObjectAndMeta2 struct{} + +func (ControllerForHandlerWithObjectAndMeta2) Test3(ctx context.Context, req *TestForHandlerWithObjectAndMeta3Req) (res *TestForHandlerWithObjectAndMeta3Res, err error) { + return &TestForHandlerWithObjectAndMeta3Res{ + Id: 1, + Age: req.Age, + }, nil +} + +func (ControllerForHandlerWithObjectAndMeta2) Test4(ctx context.Context, req *TestForHandlerWithObjectAndMeta4Req) (res *TestForHandlerWithObjectAndMeta4Res, err error) { + return &TestForHandlerWithObjectAndMeta4Res{ + Id: 1, + Name: req.Name, + }, nil +} +func Test_Router_Handler_Extended_Handler_WithObjectAndMeta(t *testing.T) { + p, _ := ports.PopRand() + s := g.Server(p) + s.Use(ghttp.MiddlewareHandlerResponse) + s.Group("/", func(group *ghttp.RouterGroup) { + group.ALL("/", new(ControllerForHandlerWithObjectAndMeta1)) + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + client := g.Client() + client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + + t.Assert(client.GetContent(ctx, "/"), `{"code":0,"message":"","data":null}`) + t.Assert(client.GetContent(ctx, "/custom-test1?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Age":18}}`) + t.Assert(client.GetContent(ctx, "/custom-test2?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Name":"john"}}`) + t.Assert(client.PostContent(ctx, "/custom-test2?age=18&name=john"), `{"code":0,"message":"","data":null}`) + }) +} + +func Test_Router_Handler_Extended_Handler_Group_Bind(t *testing.T) { + p, _ := ports.PopRand() + s := g.Server(p) + s.Use(ghttp.MiddlewareHandlerResponse) + s.Group("/api/v1", func(group *ghttp.RouterGroup) { + group.Bind( + new(ControllerForHandlerWithObjectAndMeta1), + new(ControllerForHandlerWithObjectAndMeta2), + ) + }) + s.Group("/api/v2", func(group *ghttp.RouterGroup) { + group.Bind( + new(ControllerForHandlerWithObjectAndMeta1), + new(ControllerForHandlerWithObjectAndMeta2), + ) + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + client := g.Client() + client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + + t.Assert(client.GetContent(ctx, "/"), `{"code":0,"message":"","data":null}`) + t.Assert(client.GetContent(ctx, "/api/v1/custom-test1?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Age":18}}`) + t.Assert(client.GetContent(ctx, "/api/v1/custom-test2?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Name":"john"}}`) + t.Assert(client.PostContent(ctx, "/api/v1/custom-test2?age=18&name=john"), `{"code":0,"message":"","data":null}`) + + t.Assert(client.GetContent(ctx, "/api/v1/custom-test3?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Age":18}}`) + t.Assert(client.GetContent(ctx, "/api/v1/custom-test4?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Name":"john"}}`) + + t.Assert(client.GetContent(ctx, "/api/v2/custom-test1?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Age":18}}`) + t.Assert(client.GetContent(ctx, "/api/v2/custom-test2?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Name":"john"}}`) + t.Assert(client.GetContent(ctx, "/api/v2/custom-test3?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Age":18}}`) + t.Assert(client.GetContent(ctx, "/api/v2/custom-test4?age=18&name=john"), `{"code":0,"message":"","data":{"Id":1,"Name":"john"}}`) + }) +} diff --git a/protocol/goai/goai.go b/protocol/goai/goai.go index 1f9036c57..231123b86 100644 --- a/protocol/goai/goai.go +++ b/protocol/goai/goai.go @@ -82,6 +82,7 @@ const ( TagNameMethod = `method` TagNameMime = `mime` TagNameType = `type` + TagNameDomain = `domain` TagNameValidate = `v` ) From 091df972f1b5a90c3d948935aaa0c84539e719ed Mon Sep 17 00:00:00 2001 From: John Guo Date: Sun, 7 Nov 2021 21:52:27 +0800 Subject: [PATCH 09/15] improve example for package gcache --- os/gcache/gcache_z_example_cache_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os/gcache/gcache_z_example_cache_test.go b/os/gcache/gcache_z_example_cache_test.go index d28acefba..3b2a47364 100644 --- a/os/gcache/gcache_z_example_cache_test.go +++ b/os/gcache/gcache_z_example_cache_test.go @@ -223,7 +223,7 @@ func ExampleCache_UpdateExpire() { expire1, _ := c.GetExpire(ctx, "k1") fmt.Println(expire1) - // Output: + // May Output: // 1s // 500ms } From d1b69142c4551fcd05661e9a3d0a4c0c1af30d7c Mon Sep 17 00:00:00 2001 From: John Guo Date: Mon, 8 Nov 2021 19:51:32 +0800 Subject: [PATCH 10/15] improve unit testing case for package gcache --- os/gcache/gcache_z_unit_basic_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/os/gcache/gcache_z_unit_basic_test.go b/os/gcache/gcache_z_unit_basic_test.go index 61b02a1ad..59ce23a0e 100644 --- a/os/gcache/gcache_z_unit_basic_test.go +++ b/os/gcache/gcache_z_unit_basic_test.go @@ -134,7 +134,7 @@ func TestCache_UpdateExpire(t *testing.T) { newExpire := 10 * time.Second oldExpire2, err := cache.UpdateExpire(ctx, 1, newExpire) t.AssertNil(err) - t.Assert(oldExpire2, oldExpire) + t.AssertIN(oldExpire2, g.Slice{oldExpire, `2.999s`}) e, _ := cache.GetExpire(ctx, 1) t.AssertNE(e, oldExpire) From e0db3c87cfdde93f44564ca9d28ed649950dd11f Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 14:31:16 +0800 Subject: [PATCH 11/15] improve RuleFunc for package gvalid --- util/gvalid/gvalid_custom_rule.go | 46 ++++++++++++++----- util/gvalid/gvalid_validator_check_value.go | 8 +++- util/gvalid/gvalid_z_example_test.go | 14 +++--- util/gvalid/gvalid_z_unit_custom_rule_test.go | 44 +++++++++--------- 4 files changed, 69 insertions(+), 43 deletions(-) diff --git a/util/gvalid/gvalid_custom_rule.go b/util/gvalid/gvalid_custom_rule.go index 418e9f95f..63b1020b4 100644 --- a/util/gvalid/gvalid_custom_rule.go +++ b/util/gvalid/gvalid_custom_rule.go @@ -6,16 +6,29 @@ package gvalid -import "context" +import ( + "context" + "github.com/gogf/gf/v2/container/gvar" +) // RuleFunc is the custom function for data validation. -// -// The parameter `rule` specifies the validation rule string, like "required", "between:1,100", etc. -// The parameter `value` specifies the value for this rule to validate. -// The parameter `message` specifies the custom error message or configured i18n message for this rule. -// The parameter `data` specifies the `data` which is passed to the Validator. It might be a type of map/struct or a nil value. -// You can ignore the parameter `data` if you do not really need it in your custom validation rule. -type RuleFunc func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error +type RuleFunc func(ctx context.Context, in RuleFuncInput) error + +// RuleFuncInput holds the input parameters that passed to custom rule function RuleFunc. +type RuleFuncInput struct { + // Rule specifies the validation rule string, like "required", "between:1,100", etc. + Rule string + + // Message specifies the custom error message or configured i18n message for this rule. + Message string + + // Value specifies the value for this rule to validate. + Value *gvar.Var + + // Data specifies the `data` which is passed to the Validator. It might be a type of map/struct or a nil value. + // You can ignore the parameter `Data` if you do not really need it in your custom validation rule. + Data *gvar.Var +} var ( // customRuleFuncMap stores the custom rule functions. @@ -24,13 +37,22 @@ var ( ) // RegisterRule registers custom validation rule and function for package. -// It returns error if there's already the same rule registered previously. func RegisterRule(rule string, f RuleFunc) error { customRuleFuncMap[rule] = f return nil } -// DeleteRule deletes custom defined validation rule and its function from global package. -func DeleteRule(rule string) { - delete(customRuleFuncMap, rule) +// RegisterRuleByMap registers custom validation rules using map for package. +func RegisterRuleByMap(m map[string]RuleFunc) error { + for k, v := range m { + customRuleFuncMap[k] = v + } + return nil +} + +// DeleteRule deletes custom defined validation one or more rules and associated functions from global package. +func DeleteRule(rules ...string) { + for _, rule := range rules { + delete(customRuleFuncMap, rule) + } } diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 36176f361..182a10d10 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -8,6 +8,7 @@ package gvalid import ( "context" + "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gstr" @@ -133,7 +134,12 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if customRuleFunc != nil { // It checks custom validation rules with most priority. message := v.getErrorMessageByRule(ctx, ruleKey, customMsgMap) - if err := customRuleFunc(ctx, ruleItems[index], input.Value, message, input.DataRaw); err != nil { + if err := customRuleFunc(ctx, RuleFuncInput{ + Rule: ruleItems[index], + Message: message, + Value: gvar.New(input.Value), + Data: gvar.New(input.DataRaw), + }); err != nil { match = false errorMsgArray[ruleKey] = err.Error() } else { diff --git a/util/gvalid/gvalid_z_example_test.go b/util/gvalid/gvalid_z_example_test.go index f163b1b56..c9c8c0128 100644 --- a/util/gvalid/gvalid_z_example_test.go +++ b/util/gvalid/gvalid_z_example_test.go @@ -127,17 +127,17 @@ func ExampleRegisterRule() { } rule := "unique-name" - gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { var ( - id = data.(*User).Id - name = gconv.String(value) + id = in.Data.Val().(*User).Id + name = gconv.String(in.Value) ) n, err := g.Model("user").Where("id != ? and name = ?", id, name).Count() if err != nil { return err } if n > 0 { - return errors.New(message) + return errors.New(in.Message) } return nil }) @@ -149,8 +149,8 @@ func ExampleRegisterRule() { func ExampleRegisterRule_OverwriteRequired() { rule := "required" - gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - reflectValue := reflect.ValueOf(value) + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + reflectValue := reflect.ValueOf(in.Value.Val()) if reflectValue.Kind() == reflect.Ptr { reflectValue = reflectValue.Elem() } @@ -171,7 +171,7 @@ func ExampleRegisterRule_OverwriteRequired() { isEmpty = reflectValue.Len() == 0 } if isEmpty { - return errors.New(message) + return errors.New(in.Message) } return nil }) diff --git a/util/gvalid/gvalid_z_unit_custom_rule_test.go b/util/gvalid/gvalid_z_unit_custom_rule_test.go index 75285813c..cd27fb46c 100644 --- a/util/gvalid/gvalid_z_unit_custom_rule_test.go +++ b/util/gvalid/gvalid_z_unit_custom_rule_test.go @@ -12,8 +12,6 @@ import ( "testing" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/gvalid" ) @@ -22,14 +20,14 @@ func Test_CustomRule1(t *testing.T) { rule := "custom" err := gvalid.RegisterRule( rule, - func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - pass := gconv.String(value) + func(ctx context.Context, in gvalid.RuleFuncInput) error { + pass := in.Value.String() if len(pass) != 6 { - return errors.New(message) + return errors.New(in.Message) } - m := gconv.Map(data) + m := in.Data.Map() if m["data"] != pass { - return errors.New(message) + return errors.New(in.Message) } return nil }, @@ -71,10 +69,10 @@ func Test_CustomRule1(t *testing.T) { func Test_CustomRule2(t *testing.T) { rule := "required-map" - err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - m := gconv.Map(value) + err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + m := in.Value.Map() if len(m) == 0 { - return errors.New(message) + return errors.New(in.Message) } return nil }) @@ -115,12 +113,12 @@ func Test_CustomRule2(t *testing.T) { func Test_CustomRule_AllowEmpty(t *testing.T) { rule := "allow-empty-str" - err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - s := gconv.String(value) + err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + s := in.Value.String() if len(s) == 0 || s == "gf" { return nil } - return errors.New(message) + return errors.New(in.Message) }) gtest.Assert(err, nil) // Check. @@ -160,13 +158,13 @@ func Test_CustomRule_AllowEmpty(t *testing.T) { func TestValidator_RuleFunc(t *testing.T) { ruleName := "custom_1" - ruleFunc := func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - pass := gconv.String(value) + ruleFunc := func(ctx context.Context, in gvalid.RuleFuncInput) error { + pass := in.Value.String() if len(pass) != 6 { - return errors.New(message) + return errors.New(in.Message) } - if m := gconv.Map(data); m["data"] != pass { - return errors.New(message) + if m := in.Data.Map(); m["data"] != pass { + return errors.New(in.Message) } return nil } @@ -214,13 +212,13 @@ func TestValidator_RuleFunc(t *testing.T) { func TestValidator_RuleFuncMap(t *testing.T) { ruleName := "custom_1" - ruleFunc := func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - pass := gconv.String(value) + ruleFunc := func(ctx context.Context, in gvalid.RuleFuncInput) error { + pass := in.Value.String() if len(pass) != 6 { - return errors.New(message) + return errors.New(in.Message) } - if m := gconv.Map(data); m["data"] != pass { - return errors.New(message) + if m := in.Data.Map(); m["data"] != pass { + return errors.New(in.Message) } return nil } From 02e1d01f29faca1787a22235a68ecacab45a78b6 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 15:22:17 +0800 Subject: [PATCH 12/15] improve RuleFunc for package gvalid --- util/gvalid/gvalid_custom_rule.go | 6 ++---- util/gvalid/gvalid_z_unit_custom_rule_test.go | 10 ++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/util/gvalid/gvalid_custom_rule.go b/util/gvalid/gvalid_custom_rule.go index 63b1020b4..11fe6da87 100644 --- a/util/gvalid/gvalid_custom_rule.go +++ b/util/gvalid/gvalid_custom_rule.go @@ -37,17 +37,15 @@ var ( ) // RegisterRule registers custom validation rule and function for package. -func RegisterRule(rule string, f RuleFunc) error { +func RegisterRule(rule string, f RuleFunc) { customRuleFuncMap[rule] = f - return nil } // RegisterRuleByMap registers custom validation rules using map for package. -func RegisterRuleByMap(m map[string]RuleFunc) error { +func RegisterRuleByMap(m map[string]RuleFunc) { for k, v := range m { customRuleFuncMap[k] = v } - return nil } // DeleteRule deletes custom defined validation one or more rules and associated functions from global package. diff --git a/util/gvalid/gvalid_z_unit_custom_rule_test.go b/util/gvalid/gvalid_z_unit_custom_rule_test.go index cd27fb46c..21ad7dcce 100644 --- a/util/gvalid/gvalid_z_unit_custom_rule_test.go +++ b/util/gvalid/gvalid_z_unit_custom_rule_test.go @@ -18,7 +18,7 @@ import ( func Test_CustomRule1(t *testing.T) { rule := "custom" - err := gvalid.RegisterRule( + gvalid.RegisterRule( rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { pass := in.Value.String() @@ -32,7 +32,7 @@ func Test_CustomRule1(t *testing.T) { return nil }, ) - gtest.Assert(err, nil) + gtest.C(t, func(t *gtest.T) { err := gvalid.CheckValue(context.TODO(), "123456", rule, "custom message") t.Assert(err.String(), "custom message") @@ -69,14 +69,13 @@ func Test_CustomRule1(t *testing.T) { func Test_CustomRule2(t *testing.T) { rule := "required-map" - err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { m := in.Value.Map() if len(m) == 0 { return errors.New(in.Message) } return nil }) - gtest.Assert(err, nil) // Check. gtest.C(t, func(t *gtest.T) { errStr := "data map should not be empty" @@ -113,14 +112,13 @@ func Test_CustomRule2(t *testing.T) { func Test_CustomRule_AllowEmpty(t *testing.T) { rule := "allow-empty-str" - err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { s := in.Value.String() if len(s) == 0 || s == "gf" { return nil } return errors.New(in.Message) }) - gtest.Assert(err, nil) // Check. gtest.C(t, func(t *gtest.T) { errStr := "error" From e0a0fcbde2da99b778972370d6ae6ab440e176d9 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 16:06:31 +0800 Subject: [PATCH 13/15] improve cache feature for package gdb --- database/gdb/gdb_model.go | 7 +- database/gdb/gdb_model_cache.go | 36 ++++---- database/gdb/gdb_model_select.go | 8 +- database/gdb/gdb_z_mysql_model_basic_test.go | 90 ++++++++++++++++---- 4 files changed, 102 insertions(+), 39 deletions(-) diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index c8a51945d..579130e12 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -9,10 +9,8 @@ package gdb import ( "context" "fmt" - "github.com/gogf/gf/v2/util/gconv" - "time" - "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/text/gstr" ) @@ -45,8 +43,7 @@ type Model struct { distinct string // Force the query to only return distinct results. lockInfo string // Lock for update or in shared lock. cacheEnabled bool // Enable sql result cache feature. - cacheDuration time.Duration // Cache TTL duration (< 1 for removing cache, >= 0 for saving cache). - cacheName string // Cache name for custom operation. + cacheOption CacheOption // Cache option for query statement. unscoped bool // Disables soft deleting features when select/delete operations. safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement. diff --git a/database/gdb/gdb_model_cache.go b/database/gdb/gdb_model_cache.go index d505abc3b..d5637518e 100644 --- a/database/gdb/gdb_model_cache.go +++ b/database/gdb/gdb_model_cache.go @@ -11,26 +11,32 @@ import ( "time" ) +type CacheOption struct { + // Duration is the TTL for the cache. + // If the parameter `Duration` < 0, which means it clear the cache with given `Name`. + // If the parameter `Duration` = 0, which means it never expires. + // If the parameter `Duration` > 0, which means it expires after `Duration`. + Duration time.Duration + + // Name is an optional unique name for the cache. + // The Name is used to bind a name to the cache, which means you can later control the cache + // like changing the `duration` or clearing the cache with specified Name. + Name string + + // Force caches the query result whatever the result is nil or not. + // It is used to avoid Cache Penetration. + Force bool +} + // Cache sets the cache feature for the model. It caches the result of the sql, which means // if there's another same sql request, it just reads and returns the result from cache, it // but not committed and executed into the database. // -// If the parameter `duration` < 0, which means it clear the cache with given `name`. -// If the parameter `duration` = 0, which means it never expires. -// If the parameter `duration` > 0, which means it expires after `duration`. -// -// The optional parameter `name` is used to bind a name to the cache, which means you can -// later control the cache like changing the `duration` or clearing the cache with specified -// `name`. -// // Note that, the cache feature is disabled if the model is performing select statement // on a transaction. -func (m *Model) Cache(duration time.Duration, name ...string) *Model { +func (m *Model) Cache(option CacheOption) *Model { model := m.getModel() - model.cacheDuration = duration - if len(name) > 0 { - model.cacheName = name[0] - } + model.cacheOption = option model.cacheEnabled = true return model } @@ -38,9 +44,9 @@ func (m *Model) Cache(duration time.Duration, name ...string) *Model { // checkAndRemoveCache checks and removes the cache in insert/update/delete statement if // cache feature is enabled. func (m *Model) checkAndRemoveCache() { - if m.cacheEnabled && m.cacheDuration < 0 && len(m.cacheName) > 0 { + if m.cacheEnabled && m.cacheOption.Duration < 0 && len(m.cacheOption.Name) > 0 { var ctx = m.GetCtx() - _, err := m.db.GetCache().Remove(ctx, m.cacheName) + _, err := m.db.GetCache().Remove(ctx, m.cacheOption.Name) if err != nil { intlog.Error(ctx, err) } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 70f449d44..098f514fb 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -437,7 +437,7 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e ) // Retrieve from cache. if m.cacheEnabled && m.tx == nil { - cacheKey = m.cacheName + cacheKey = m.cacheOption.Name if len(cacheKey) == 0 { cacheKey = sql + ", @PARAMS:" + gconv.String(args) } @@ -461,16 +461,16 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e ) // Cache the result. if cacheKey != "" && err == nil { - if m.cacheDuration < 0 { + if m.cacheOption.Duration < 0 { if _, err := cacheObj.Remove(ctx, cacheKey); err != nil { intlog.Error(m.GetCtx(), err) } } else { // In case of Cache Penetration. - if result == nil { + if result.IsEmpty() && m.cacheOption.Force { result = Result{} } - if err := cacheObj.Set(ctx, cacheKey, result, m.cacheDuration); err != nil { + if err := cacheObj.Set(ctx, cacheKey, result, m.cacheOption.Duration); err != nil { intlog.Error(m.GetCtx(), err) } } diff --git a/database/gdb/gdb_z_mysql_model_basic_test.go b/database/gdb/gdb_z_mysql_model_basic_test.go index c1e7496ef..2d055a3fc 100644 --- a/database/gdb/gdb_z_mysql_model_basic_test.go +++ b/database/gdb/gdb_z_mysql_model_basic_test.go @@ -2479,7 +2479,11 @@ func Test_Model_Cache(t *testing.T) { defer dropTable(table) gtest.C(t, func(t *gtest.T) { - one, err := db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_1") @@ -2489,63 +2493,107 @@ func Test_Model_Cache(t *testing.T) { t.AssertNil(err) t.Assert(n, 1) - one, err = db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_1") time.Sleep(time.Second * 2) - one, err = db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_100") }) gtest.C(t, func(t *gtest.T) { - one, err := db.Model(table).Cache(time.Second, "test2").WherePri(2).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test2", + Force: false, + }).WherePri(2).One() t.AssertNil(err) t.Assert(one["passport"], "user_2") - r, err := db.Model(table).Data("passport", "user_200").Cache(-1, "test2").WherePri(2).Update() + r, err := db.Model(table).Data("passport", "user_200").Cache(gdb.CacheOption{ + Duration: -1, + Name: "test2", + Force: false, + }).WherePri(2).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) t.Assert(n, 1) - one, err = db.Model(table).Cache(time.Second, "test2").WherePri(2).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test2", + Force: false, + }).WherePri(2).One() t.AssertNil(err) t.Assert(one["passport"], "user_200") }) // transaction. gtest.C(t, func(t *gtest.T) { // make cache for id 3 - one, err := db.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_3") - r, err := db.Model(table).Data("passport", "user_300").Cache(time.Second, "test3").WherePri(3).Update() + r, err := db.Model(table).Data("passport", "user_300").Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) t.Assert(n, 1) err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { - one, err := tx.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err := tx.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_300") return nil }) t.AssertNil(err) - one, err = db.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_3") }) gtest.C(t, func(t *gtest.T) { // make cache for id 4 - one, err := db.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_4") - r, err := db.Model(table).Data("passport", "user_400").Cache(time.Second, "test3").WherePri(4).Update() + r, err := db.Model(table).Data("passport", "user_400").Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(4).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) @@ -2553,12 +2601,20 @@ func Test_Model_Cache(t *testing.T) { err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { // Cache feature disabled. - one, err := tx.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err := tx.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_400") // Update the cache. r, err := tx.Model(table).Data("passport", "user_4000"). - Cache(-1, "test4").WherePri(4).Update() + Cache(gdb.CacheOption{ + Duration: -1, + Name: "test4", + Force: false, + }).WherePri(4).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) @@ -2567,7 +2623,11 @@ func Test_Model_Cache(t *testing.T) { }) t.AssertNil(err) // Read from db. - one, err = db.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_4000") }) From 95a4690e692d24cb8b0421f38964a22afca8b99d Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 17:50:14 +0800 Subject: [PATCH 14/15] improve error feature for package gvalid --- internal/utils/utils_is.go | 2 +- util/gutil/gutil_dump.go | 8 +- util/gvalid/gvalid_error.go | 112 ++++++++++-------- util/gvalid/gvalid_validator_check_map.go | 11 +- util/gvalid/gvalid_validator_check_struct.go | 12 +- util/gvalid/gvalid_validator_check_value.go | 54 +++------ util/gvalid/gvalid_z_example_test.go | 4 +- util/gvalid/gvalid_z_unit_basic_all_test.go | 2 +- util/gvalid/gvalid_z_unit_customerror_test.go | 11 +- 9 files changed, 105 insertions(+), 111 deletions(-) diff --git a/internal/utils/utils_is.go b/internal/utils/utils_is.go index b525df226..ceb4f74e8 100644 --- a/internal/utils/utils_is.go +++ b/internal/utils/utils_is.go @@ -13,7 +13,7 @@ import ( // IsNil checks whether `value` is nil. func IsNil(value interface{}) bool { - return value == nil + return empty.IsNil(value) } // IsEmpty checks whether `value` is empty. diff --git a/util/gutil/gutil_dump.go b/util/gutil/gutil_dump.go index 0e331426f..fb41864ae 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -196,8 +196,12 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE if structContentStr == "" { structContentStr = "{}" } else { - structContentStr = fmt.Sprintf(`"%s"`, gstr.AddSlashes(structContentStr)) - attributeCountStr = fmt.Sprintf(`%d`, len(structContentStr)-2) + if strings.HasPrefix(structContentStr, `"`) && strings.HasSuffix(structContentStr, `"`) { + attributeCountStr = fmt.Sprintf(`%d`, len(structContentStr)) + } else { + structContentStr = fmt.Sprintf(`"%s"`, gstr.AddSlashes(structContentStr)) + attributeCountStr = fmt.Sprintf(`%d`, len(structContentStr)-2) + } } if option.WithoutType { buffer.WriteString(structContentStr) diff --git a/util/gvalid/gvalid_error.go b/util/gvalid/gvalid_error.go index 08f553fd7..34a740e0a 100644 --- a/util/gvalid/gvalid_error.go +++ b/util/gvalid/gvalid_error.go @@ -19,50 +19,62 @@ type Error interface { Code() gcode.Code Current() error Error() string - FirstItem() (key string, messages map[string]string) - FirstRule() (rule string, err string) - FirstString() (err string) - Items() (items []map[string]map[string]string) - Map() map[string]string - Maps() map[string]map[string]string + FirstItem() (key string, messages map[string]error) + FirstRule() (rule string, err error) + FirstError() (err error) + Items() (items []map[string]map[string]error) + Map() map[string]error + Maps() map[string]map[string]error String() string Strings() (errs []string) } // validationError is the validation error for validation result. type validationError struct { - code gcode.Code // Error code. - rules []fieldRule // Rules by sequence, which is used for keeping error sequence. - errors map[string]map[string]string // Error map:map[field]map[rule]message - firstKey string // The first error rule key(empty in default). - firstItem map[string]string // The first error rule value(nil in default). + code gcode.Code // Error code. + rules []fieldRule // Rules by sequence, which is used for keeping error sequence. + errors map[string]map[string]error // Error map:map[field]map[rule]message + firstKey string // The first error rule key(empty in default). + firstItem map[string]error // The first error rule value(nil in default). } -// newError creates and returns a validation error. -func newError(code gcode.Code, rules []fieldRule, errors map[string]map[string]string) *validationError { - for field, m := range errors { - for k, v := range m { - v = strings.Replace(v, ":attribute", field, -1) - v, _ = gregex.ReplaceString(`\s{2,}`, ` `, v) - v = gstr.Trim(v) - m[k] = v +// newValidationError creates and returns a validation error. +func newValidationError(code gcode.Code, rules []fieldRule, fieldRuleErrorMap map[string]map[string]error) *validationError { + var ( + s string + ) + for field, ruleErrorMap := range fieldRuleErrorMap { + for rule, err := range ruleErrorMap { + if !gerror.HasStack(err) { + s = strings.Replace(err.Error(), ":attribute", field, -1) + s, _ = gregex.ReplaceString(`\s{2,}`, ` `, s) + ruleErrorMap[rule] = gerror.NewOption(gerror.Option{ + Stack: false, + Text: gstr.Trim(s), + Code: code, + }) + } } - errors[field] = m + fieldRuleErrorMap[field] = ruleErrorMap } return &validationError{ code: code, rules: rules, - errors: errors, + errors: fieldRuleErrorMap, } } -// newErrorStr creates and returns a validation error by string. -func newErrorStr(key, err string) *validationError { - return newError(gcode.CodeInternalError, nil, map[string]map[string]string{ - internalErrorMapKey: { - key: err, +// newValidationErrorByStr creates and returns a validation error by string. +func newValidationErrorByStr(key string, err error) *validationError { + return newValidationError( + gcode.CodeInternalError, + nil, + map[string]map[string]error{ + internalErrorMapKey: { + key: err, + }, }, - }) + ) } // Code returns the error code of current validation error. @@ -74,16 +86,16 @@ func (e *validationError) Code() gcode.Code { } // Map returns the first error message as map. -func (e *validationError) Map() map[string]string { +func (e *validationError) Map() map[string]error { if e == nil { - return map[string]string{} + return map[string]error{} } _, m := e.FirstItem() return m } // Maps returns all error messages as map. -func (e *validationError) Maps() map[string]map[string]string { +func (e *validationError) Maps() map[string]map[string]error { if e == nil { return nil } @@ -92,16 +104,16 @@ func (e *validationError) Maps() map[string]map[string]string { // Items retrieves and returns error items array in sequence if possible, // or else it returns error items with no sequence . -func (e *validationError) Items() (items []map[string]map[string]string) { +func (e *validationError) Items() (items []map[string]map[string]error) { if e == nil { - return []map[string]map[string]string{} + return []map[string]map[string]error{} } - items = make([]map[string]map[string]string, 0) + items = make([]map[string]map[string]error, 0) // By sequence. if len(e.rules) > 0 { for _, v := range e.rules { if errorItemMap, ok := e.errors[v.Name]; ok { - items = append(items, map[string]map[string]string{ + items = append(items, map[string]map[string]error{ v.Name: errorItemMap, }) } @@ -110,7 +122,7 @@ func (e *validationError) Items() (items []map[string]map[string]string) { } // No sequence. for name, errorRuleMap := range e.errors { - items = append(items, map[string]map[string]string{ + items = append(items, map[string]map[string]error{ name: errorRuleMap, }) } @@ -118,9 +130,9 @@ func (e *validationError) Items() (items []map[string]map[string]string) { } // FirstItem returns the field name and error messages for the first validation rule error. -func (e *validationError) FirstItem() (key string, messages map[string]string) { +func (e *validationError) FirstItem() (key string, messages map[string]error) { if e == nil { - return "", map[string]string{} + return "", map[string]error{} } if e.firstItem != nil { return e.firstKey, e.firstItem @@ -145,9 +157,9 @@ func (e *validationError) FirstItem() (key string, messages map[string]string) { } // FirstRule returns the first error rule and message string. -func (e *validationError) FirstRule() (rule string, err string) { +func (e *validationError) FirstRule() (rule string, err error) { if e == nil { - return "", "" + return "", nil } // By sequence. if len(e.rules) > 0 { @@ -169,26 +181,22 @@ func (e *validationError) FirstRule() (rule string, err string) { return k, v } } - return "", "" + return "", nil } -// FirstString returns the first error message as string. +// FirstError returns the first error message as string. // Note that the returned message might be different if it has no sequence. -func (e *validationError) FirstString() (err string) { +func (e *validationError) FirstError() (err error) { if e == nil { - return "" + return nil } _, err = e.FirstRule() return } -// Current is alis of FirstString, which implements interface gerror.iCurrent. +// Current is alis of FirstError, which implements interface gerror.iCurrent. func (e *validationError) Current() error { - if e == nil { - return nil - } - _, err := e.FirstRule() - return gerror.NewCode(e.code, err) + return e.FirstError() } // String returns all error messages as string, multiple error messages joined using char ';'. @@ -221,13 +229,13 @@ func (e *validationError) Strings() (errs []string) { for _, ruleItem := range strings.Split(v.Rule, "|") { ruleItem = strings.TrimSpace(strings.Split(ruleItem, ":")[0]) if err, ok := errorItemMap[ruleItem]; ok { - errs = append(errs, err) + errs = append(errs, err.Error()) } } // internal error checks. for k, _ := range internalErrKeyMap { if err, ok := errorItemMap[k]; ok { - errs = append(errs, err) + errs = append(errs, err.Error()) } } } @@ -237,7 +245,7 @@ func (e *validationError) Strings() (errs []string) { // No sequence. for _, errorItemMap := range e.errors { for _, err := range errorItemMap { - errs = append(errs, err) + errs = append(errs, err.Error()) } } return diff --git a/util/gvalid/gvalid_validator_check_map.go b/util/gvalid/gvalid_validator_check_map.go index e3f10fa56..4e5b7ad0d 100644 --- a/util/gvalid/gvalid_validator_check_map.go +++ b/util/gvalid/gvalid_validator_check_map.go @@ -8,6 +8,7 @@ package gvalid import ( "context" + "errors" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/util/gconv" "strings" @@ -27,7 +28,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { var ( checkRules = make([]fieldRule, 0) customMessage = make(CustomMsg) // map[RuleKey]ErrorMsg. - errorMaps = make(map[string]map[string]string) + errorMaps = make(map[string]map[string]error) ) switch assertValue := v.rules.(type) { // Sequence tag: []sequence tag @@ -80,9 +81,9 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { } data := gconv.Map(params) if data == nil { - return newErrorStr( + return newValidationErrorByStr( internalParamsErrRuleName, - "invalid params type: convert to map failed", + errors.New("invalid params type: convert to map failed"), ) } if msg, ok := v.messages.(CustomMsg); ok && len(msg) > 0 { @@ -139,7 +140,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { } } if _, ok := errorMaps[checkRuleItem.Name]; !ok { - errorMaps[checkRuleItem.Name] = make(map[string]string) + errorMaps[checkRuleItem.Name] = make(map[string]error) } for ruleKey, errorItemMsgMap := range errorItem { errorMaps[checkRuleItem.Name][ruleKey] = errorItemMsgMap @@ -150,7 +151,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { } } if len(errorMaps) > 0 { - return newError(gcode.CodeValidationFailed, checkRules, errorMaps) + return newValidationError(gcode.CodeValidationFailed, checkRules, errorMaps) } return nil } diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 878e8a728..647a856c1 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -23,8 +23,8 @@ func (v *Validator) CheckStruct(ctx context.Context, object interface{}) Error { func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error { var ( - errorMaps = make(map[string]map[string]string) // Returning error. - fieldToAliasNameMap = make(map[string]string) // Field names to alias name map. + errorMaps = make(map[string]map[string]error) // Returning error. + fieldToAliasNameMap = make(map[string]string) // Field names to alias name map. ) fieldMap, err := structs.FieldMap(structs.FieldMapInput{ Pointer: object, @@ -32,7 +32,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error RecursiveOption: structs.RecursiveOptionEmbedded, }) if err != nil { - return newErrorStr(internalObjectErrRuleName, err.Error()) + return newValidationErrorByStr(internalObjectErrRuleName, err) } // It checks the struct recursively if its attribute is an embedded struct. for _, field := range fieldMap { @@ -59,7 +59,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // It here must use structs.TagFields not structs.FieldMap to ensure error sequence. tagField, err := structs.TagFields(object, structTagPriority) if err != nil { - return newErrorStr(internalObjectErrRuleName, err.Error()) + return newValidationErrorByStr(internalObjectErrRuleName, err) } // If there's no struct tag and validation rules, it does nothing and returns quickly. if len(tagField) == 0 && v.messages == nil { @@ -278,7 +278,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } if _, ok := errorMaps[checkRuleItem.Name]; !ok { - errorMaps[checkRuleItem.Name] = make(map[string]string) + errorMaps[checkRuleItem.Name] = make(map[string]error) } for ruleKey, errorItemMsgMap := range errorItem { errorMaps[checkRuleItem.Name][ruleKey] = errorItemMsgMap @@ -289,7 +289,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } if len(errorMaps) > 0 { - return newError(gcode.CodeValidationFailed, checkRules, errorMaps) + return newValidationError(gcode.CodeValidationFailed, checkRules, errorMaps) } return nil } diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 182a10d10..948601395 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -8,9 +8,9 @@ package gvalid import ( "context" + "errors" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gstr" "strconv" "strings" @@ -61,7 +61,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E // It converts value to string and then does the validation. var ( // Do not trim it as the space is also part of the value. - errorMsgArray = make(map[string]string) + ruleErrorMap = make(map[string]error) ) // Custom error messages handling. var ( @@ -87,9 +87,9 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E ruleItems[i-1] += "|" + ruleItems[i] ruleItems = append(ruleItems[:i], ruleItems[i+1:]...) } else { - return newErrorStr( + return newValidationErrorByStr( internalRulesErrRuleName, - internalRulesErrRuleName+": "+input.Rule, + errors.New(internalRulesErrRuleName+": "+input.Rule), ) } } else { @@ -141,7 +141,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E Data: gvar.New(input.DataRaw), }); err != nil { match = false - errorMsgArray[ruleKey] = err.Error() + ruleErrorMap[ruleKey] = err } else { match = true } @@ -160,7 +160,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E }, ) if !match && err != nil { - errorMsgArray[ruleKey] = err.Error() + ruleErrorMap[ruleKey] = err } } @@ -168,8 +168,8 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if !match { // It does nothing if the error message for this rule // is already set in previous validation. - if _, ok := errorMsgArray[ruleKey]; !ok { - errorMsgArray[ruleKey] = v.getErrorMessageByRule(ctx, ruleKey, customMsgMap) + if _, ok := ruleErrorMap[ruleKey]; !ok { + ruleErrorMap[ruleKey] = errors.New(v.getErrorMessageByRule(ctx, ruleKey, customMsgMap)) } // If it is with error and there's bail rule, // it then does not continue validating for left rules. @@ -179,12 +179,12 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } index++ } - if len(errorMsgArray) > 0 { - return newError( + if len(ruleErrorMap) > 0 { + return newValidationError( gcode.CodeValidationFailed, []fieldRule{{Name: input.Name, Rule: input.Rule}}, - map[string]map[string]string{ - input.Name: errorMsgArray, + map[string]map[string]error{ + input.Name: ruleErrorMap, }, ) } @@ -223,10 +223,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI "max-length", "size": if msg := v.checkLength(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } else { match = true } @@ -237,10 +234,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI "max", "between": if msg := v.checkRange(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } else { match = true } @@ -288,10 +282,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI ) msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) msg = strings.Replace(msg, ":format", input.RulePattern, -1) - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } // Values of two fields should be equal as string. @@ -306,10 +297,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI var msg string msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) msg = strings.Replace(msg, ":field", input.RulePattern, -1) - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } // Values of two fields should not be equal as string. @@ -325,10 +313,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI var msg string msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) msg = strings.Replace(msg, ":field", input.RulePattern, -1) - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } // Field value should be in range of. @@ -511,10 +496,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}[\-:]){5}[0-9A-Fa-f]{2}$`, valueStr) default: - return match, gerror.NewOption(gerror.Option{ - Text: "Invalid rule name: " + input.RuleKey, - Code: gcode.CodeInvalidParameter, - }) + return match, errors.New("Invalid rule name: " + input.RuleKey) } return match, nil } diff --git a/util/gvalid/gvalid_z_example_test.go b/util/gvalid/gvalid_z_example_test.go index c9c8c0128..b4e3e3eec 100644 --- a/util/gvalid/gvalid_z_example_test.go +++ b/util/gvalid/gvalid_z_example_test.go @@ -33,7 +33,7 @@ func ExampleCheckMap() { if e := gvalid.CheckMap(gctx.New(), params, rules); e != nil { fmt.Println(e.Map()) fmt.Println(e.FirstItem()) - fmt.Println(e.FirstString()) + fmt.Println(e.FirstError()) } // May Output: // map[required:账号不能为空 length:账号长度应当在6到16之间] @@ -55,7 +55,7 @@ func ExampleCheckMap2() { if e := gvalid.CheckMap(gctx.New(), params, rules); e != nil { fmt.Println(e.Map()) fmt.Println(e.FirstItem()) - fmt.Println(e.FirstString()) + fmt.Println(e.FirstError()) } // Output: // map[same:两次密码输入不相等] diff --git a/util/gvalid/gvalid_z_unit_basic_all_test.go b/util/gvalid/gvalid_z_unit_basic_all_test.go index 83741d477..647316230 100755 --- a/util/gvalid/gvalid_z_unit_basic_all_test.go +++ b/util/gvalid/gvalid_z_unit_basic_all_test.go @@ -1037,7 +1037,7 @@ func Test_InternalError_String(t *testing.T) { t.Assert(err.String(), "InvalidRules: hh") t.Assert(err.Strings(), g.Slice{"InvalidRules: hh"}) - t.Assert(err.FirstString(), "InvalidRules: hh") + t.Assert(err.FirstError(), "InvalidRules: hh") t.Assert(gerror.Current(err), "InvalidRules: hh") }) } diff --git a/util/gvalid/gvalid_z_unit_customerror_test.go b/util/gvalid/gvalid_z_unit_customerror_test.go index 51941c171..b1a30be68 100755 --- a/util/gvalid/gvalid_z_unit_customerror_test.go +++ b/util/gvalid/gvalid_z_unit_customerror_test.go @@ -35,9 +35,8 @@ func Test_FirstString(t *testing.T) { rule = "ipv4" val = "0.0.0" err = gvalid.CheckValue(context.TODO(), val, rule, nil) - n = err.FirstString() ) - t.Assert(n, "The value must be a valid IPv4 address") + t.Assert(err.FirstError(), "The value must be a valid IPv4 address") }) } @@ -52,12 +51,12 @@ func Test_CustomError1(t *testing.T) { t.Error("规则校验失败") } else { if v, ok := e.Map()["integer"]; ok { - if strings.Compare(v, msgs["integer"]) != 0 { + if strings.Compare(v.Error(), msgs["integer"]) != 0 { t.Error("错误信息不匹配") } } if v, ok := e.Map()["length"]; ok { - if strings.Compare(v, msgs["length"]) != 0 { + if strings.Compare(v.Error(), msgs["length"]) != 0 { t.Error("错误信息不匹配") } } @@ -72,12 +71,12 @@ func Test_CustomError2(t *testing.T) { t.Error("规则校验失败") } else { if v, ok := e.Map()["integer"]; ok { - if strings.Compare(v, "请输入一个整数") != 0 { + if strings.Compare(v.Error(), "请输入一个整数") != 0 { t.Error("错误信息不匹配") } } if v, ok := e.Map()["length"]; ok { - if strings.Compare(v, "参数长度不对啊老铁") != 0 { + if strings.Compare(v.Error(), "参数长度不对啊老铁") != 0 { t.Error("错误信息不匹配") } } From 31d793fafb2a2540488cf9a717bceceaf837706a Mon Sep 17 00:00:00 2001 From: John Guo Date: Wed, 10 Nov 2021 00:20:00 +0800 Subject: [PATCH 15/15] improve example for package gqueue --- container/gqueue/gqueue.go | 15 ++++++++------- container/gqueue/gqueue_z_example_test.go | 12 ++++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/container/gqueue/gqueue.go b/container/gqueue/gqueue.go index de2a06504..a778709b3 100644 --- a/container/gqueue/gqueue.go +++ b/container/gqueue/gqueue.go @@ -59,7 +59,7 @@ func New(limit ...int) *Queue { } // asyncLoopFromListToChannel starts an asynchronous goroutine, -// which handles the data synchronization from list to channel . +// which handles the data synchronization from list `q.list` to channel `q.C`. func (q *Queue) asyncLoopFromListToChannel() { defer func() { if q.closed.Val() { @@ -87,13 +87,13 @@ func (q *Queue) asyncLoopFromListToChannel() { <-q.events } } - // It should be here to close q.C if `q` is unlimited size. + // It should be here to close `q.C` if `q` is unlimited size. // It's the sender's responsibility to close channel when it should be closed. close(q.C) } // Push pushes the data `v` into the queue. -// Note that it would panics if Push is called after the queue is closed. +// Note that it would panic if Push is called after the queue is closed. func (q *Queue) Push(v interface{}) { if q.limit > 0 { q.C <- v @@ -121,14 +121,15 @@ func (q *Queue) Close() { } if q.limit > 0 { close(q.C) - } - for i := 0; i < defaultBatchSize; i++ { - q.Pop() + } else { + for i := 0; i < defaultBatchSize; i++ { + q.Pop() + } } } // Len returns the length of the queue. -// Note that the result might not be accurate as there's a +// Note that the result might not be accurate as there's an // asynchronous channel reading the list constantly. func (q *Queue) Len() (length int) { if q.list != nil { diff --git a/container/gqueue/gqueue_z_example_test.go b/container/gqueue/gqueue_z_example_test.go index cb2b4696d..d561335ee 100644 --- a/container/gqueue/gqueue_z_example_test.go +++ b/container/gqueue/gqueue_z_example_test.go @@ -65,10 +65,14 @@ func ExampleQueue_Push() { q.Push(i) } - fmt.Println(q.Len()) + fmt.Println(q.Pop()) + fmt.Println(q.Pop()) + fmt.Println(q.Pop()) // Output: - // 10 + // 0 + // 1 + // 2 } func ExampleQueue_Pop() { @@ -113,7 +117,7 @@ func ExampleQueue_Len() { fmt.Println(q.Len()) - // Output: + // May Output: // 2 } @@ -126,6 +130,6 @@ func ExampleQueue_Size() { // Size is alias of Len. fmt.Println(q.Size()) - // Output: + // May Output: // 2 }