This commit is contained in:
ansionfor 2021-11-11 17:51:33 +08:00
commit ae819ad44b
42 changed files with 1908 additions and 632 deletions

View File

@ -59,7 +59,7 @@ func New(limit ...int) *Queue {
}
// asyncLoopFromListToChannel starts an asynchronous goroutine,
// which handles the data synchronization from list <q.list> to channel <q.C>.
// 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 {

View File

@ -0,0 +1,135 @@
// 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.Pop())
fmt.Println(q.Pop())
fmt.Println(q.Pop())
// Output:
// 0
// 1
// 2
}
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
// <nil>
}
func ExampleQueue_Close() {
q := gqueue.New()
for i := 0; i < 10; i++ {
q.Push(i)
}
time.Sleep(time.Millisecond)
q.Close()
fmt.Println(q.Len())
fmt.Println(q.Pop())
// Output:
// 0
// <nil>
}
func ExampleQueue_Len() {
q := gqueue.New()
q.Push(1)
q.Push(2)
fmt.Println(q.Len())
// May Output:
// 2
}
func ExampleQueue_Size() {
q := gqueue.New()
q.Push(1)
q.Push(2)
// Size is alias of Len.
fmt.Println(q.Size())
// May Output:
// 2
}

View File

@ -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.

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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")
})

View File

@ -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.

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
})
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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) {

View File

@ -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),
},
},
})
}

View File

@ -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,

View File

@ -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)
}

View File

@ -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"))
})

View File

@ -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")
})
}

View File

@ -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)

View File

@ -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"}}`)
})
}

View File

@ -223,7 +223,7 @@ func ExampleCache_UpdateExpire() {
expire1, _ := c.GetExpire(ctx, "k1")
fmt.Println(expire1)
// Output:
// May Output:
// 1s
// 500ms
}

View File

@ -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)

View File

@ -82,6 +82,7 @@ const (
TagNameMethod = `method`
TagNameMime = `mime`
TagNameType = `type`
TagNameDomain = `domain`
TagNameValidate = `v`
)

View File

@ -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

View File

@ -7,8 +7,11 @@
package gconv
import (
"database/sql"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/structs"
"github.com/gogf/gf/v2/internal/utils"
"reflect"
)
@ -38,7 +41,18 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string)
}
pointerKind = pointerType.Kind()
if pointerKind != reflect.Ptr {
return gerror.NewCodef(gcode.CodeInvalidParameter, "params should be type of pointer, but got type: %v", pointerKind)
if pointerValue.CanAddr() {
pointerValue = pointerValue.Addr()
pointerType = pointerValue.Type()
pointerKind = pointerType.Kind()
} else {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
"params should be type of pointer, but got type: %v",
pointerType,
)
}
}
// Direct assignment checks!
var (
@ -94,3 +108,410 @@ func Scan(params interface{}, pointer interface{}, mapping ...map[string]string)
return doStruct(params, pointer, keyToAttributeNameMapping, "")
}
}
// ScanList converts `structSlice` to struct slice which contains other complex struct attributes.
// Note that the parameter `structSlicePointer` should be type of *[]struct/*[]*struct.
//
// Usage example 1: Normal attribute struct relation:
// type EntityUser struct {
// Uid int
// Name string
// }
// type EntityUserDetail struct {
// Uid int
// Address string
// }
// type EntityUserScores struct {
// Id int
// Uid int
// Score int
// Course string
// }
// type Entity struct {
// User *EntityUser
// UserDetail *EntityUserDetail
// UserScores []*EntityUserScores
// }
// var users []*Entity
// ScanList(records, &users, "User")
// ScanList(records, &users, "User", "uid")
// ScanList(records, &users, "UserDetail", "User", "uid:Uid")
// ScanList(records, &users, "UserScores", "User", "uid:Uid")
// ScanList(records, &users, "UserScores", "User", "uid")
//
//
// Usage example 2: Embedded attribute struct relation:
// type EntityUser struct {
// Uid int
// Name string
// }
// type EntityUserDetail struct {
// Uid int
// Address string
// }
// type EntityUserScores struct {
// Id int
// Uid int
// Score int
// }
// type Entity struct {
// EntityUser
// UserDetail EntityUserDetail
// UserScores []EntityUserScores
// }
//
// var users []*Entity
// ScanList(records, &users)
// ScanList(records, &users, "UserDetail", "uid")
// ScanList(records, &users, "UserScores", "uid")
//
//
// The parameters "User/UserDetail/UserScores" in the example codes specify the target attribute struct
// that current result will be bound to.
//
// The "uid" in the example codes is the table field name of the result, and the "Uid" is the relational
// struct attribute name - not the attribute name of the bound to target. In the example codes, it's attribute
// name "Uid" of "User" of entity "Entity". It automatically calculates the HasOne/HasMany relationship with
// given `relation` parameter.
//
// See the example or unit testing cases for clear understanding for this function.
func ScanList(structSlice interface{}, structSlicePointer interface{}, bindToAttrName string, relationAttrNameAndFields ...string) (err error) {
var (
relationAttrName string
relationFields string
)
switch len(relationAttrNameAndFields) {
case 2:
relationAttrName = relationAttrNameAndFields[0]
relationFields = relationAttrNameAndFields[1]
case 1:
relationFields = relationAttrNameAndFields[0]
}
return doScanList(structSlice, structSlicePointer, bindToAttrName, relationAttrName, relationFields)
}
// doScanList converts `structSlice` to struct slice which contains other complex struct attributes recursively.
// Note that the parameter `structSlicePointer` should be type of *[]struct/*[]*struct.
func doScanList(structSlice interface{}, structSlicePointer interface{}, bindToAttrName, relationAttrName, relationFields string) (err error) {
var (
maps = Maps(structSlice)
)
if len(maps) == 0 {
return nil
}
// Necessary checks for parameters.
if bindToAttrName == "" {
return gerror.NewCode(gcode.CodeInvalidParameter, `bindToAttrName should not be empty`)
}
if relationAttrName == "." {
relationAttrName = ""
}
var (
reflectValue = reflect.ValueOf(structSlicePointer)
reflectKind = reflectValue.Kind()
)
if reflectKind == reflect.Interface {
reflectValue = reflectValue.Elem()
reflectKind = reflectValue.Kind()
}
if reflectKind != reflect.Ptr {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
"structSlicePointer should be type of *[]struct/*[]*struct, but got: %v",
reflectKind,
)
}
reflectValue = reflectValue.Elem()
reflectKind = reflectValue.Kind()
if reflectKind != reflect.Slice && reflectKind != reflect.Array {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
"structSlicePointer should be type of *[]struct/*[]*struct, but got: %v",
reflectKind,
)
}
length := len(maps)
if length == 0 {
// The pointed slice is not empty.
if reflectValue.Len() > 0 {
// It here checks if it has struct item, which is already initialized.
// It then returns error to warn the developer its empty and no conversion.
if v := reflectValue.Index(0); v.Kind() != reflect.Ptr {
return sql.ErrNoRows
}
}
// Do nothing for empty struct slice.
return nil
}
var (
arrayValue reflect.Value // Like: []*Entity
arrayItemType reflect.Type // Like: *Entity
reflectType = reflect.TypeOf(structSlicePointer)
)
if reflectValue.Len() > 0 {
arrayValue = reflectValue
} else {
arrayValue = reflect.MakeSlice(reflectType.Elem(), length, length)
}
// Slice element item.
arrayItemType = arrayValue.Index(0).Type()
// Relation variables.
var (
relationDataMap map[string]interface{}
relationFromFieldName string // Eg: relationKV: id:uid -> id
relationBindToFieldName string // Eg: relationKV: id:uid -> uid
)
if len(relationFields) > 0 {
// The relation key string of table filed name and attribute name
// can be joined with char '=' or ':'.
array := utils.SplitAndTrim(relationFields, "=")
if len(array) == 1 {
// Compatible with old splitting char ':'.
array = utils.SplitAndTrim(relationFields, ":")
}
if len(array) == 1 {
// The relation names are the same.
array = []string{relationFields, relationFields}
}
if len(array) == 2 {
// Defined table field to relation attribute name.
// Like:
// uid:Uid
// uid:UserId
relationFromFieldName = array[0]
relationBindToFieldName = array[1]
if key, _ := utils.MapPossibleItemByKey(maps[0], relationFromFieldName); key == "" {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`cannot find possible related table field name "%s" from given relation fields "%s"`,
relationFromFieldName,
relationFields,
)
} else {
relationFromFieldName = key
}
} else {
return gerror.NewCode(
gcode.CodeInvalidParameter,
`parameter relationKV should be format of "ResultFieldName:BindToAttrName"`,
)
}
if relationFromFieldName != "" {
// Note that the value might be type of slice.
relationDataMap = utils.ListToMapByKey(maps, relationFromFieldName)
}
if len(relationDataMap) == 0 {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`cannot find the relation data map, maybe invalid relation fields given "%v"`,
relationFields,
)
}
}
// Bind to target attribute.
var (
ok bool
bindToAttrValue reflect.Value
bindToAttrKind reflect.Kind
bindToAttrType reflect.Type
bindToAttrField reflect.StructField
)
if arrayItemType.Kind() == reflect.Ptr {
if bindToAttrField, ok = arrayItemType.Elem().FieldByName(bindToAttrName); !ok {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid parameter bindToAttrName: cannot find attribute with name "%s" from slice element`,
bindToAttrName,
)
}
} else {
if bindToAttrField, ok = arrayItemType.FieldByName(bindToAttrName); !ok {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`invalid parameter bindToAttrName: cannot find attribute with name "%s" from slice element`,
bindToAttrName,
)
}
}
bindToAttrType = bindToAttrField.Type
bindToAttrKind = bindToAttrType.Kind()
// Bind to relation conditions.
var (
relationFromAttrValue reflect.Value
relationFromAttrField reflect.Value
relationBindToFieldNameChecked bool
)
for i := 0; i < arrayValue.Len(); i++ {
arrayElemValue := arrayValue.Index(i)
// The FieldByName should be called on non-pointer reflect.Value.
if arrayElemValue.Kind() == reflect.Ptr {
// Like: []*Entity
arrayElemValue = arrayElemValue.Elem()
if !arrayElemValue.IsValid() {
// The element is nil, then create one and set it to the slice.
// The "reflect.New(itemType.Elem())" creates a new element and returns the address of it.
// For example:
// reflect.New(itemType.Elem()) => *Entity
// reflect.New(itemType.Elem()).Elem() => Entity
arrayElemValue = reflect.New(arrayItemType.Elem()).Elem()
arrayValue.Index(i).Set(arrayElemValue.Addr())
}
} else {
// Like: []Entity
}
bindToAttrValue = arrayElemValue.FieldByName(bindToAttrName)
if relationAttrName != "" {
// Attribute value of current slice element.
relationFromAttrValue = arrayElemValue.FieldByName(relationAttrName)
if relationFromAttrValue.Kind() == reflect.Ptr {
relationFromAttrValue = relationFromAttrValue.Elem()
}
} else {
// Current slice element.
relationFromAttrValue = arrayElemValue
}
if len(relationDataMap) > 0 && !relationFromAttrValue.IsValid() {
return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields)
}
// Check and find possible bind to attribute name.
if relationFields != "" && !relationBindToFieldNameChecked {
relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName)
if !relationFromAttrField.IsValid() {
var (
filedMap, _ = structs.FieldMap(structs.FieldMapInput{
Pointer: relationFromAttrValue,
RecursiveOption: structs.RecursiveOptionEmbeddedNoTag,
})
)
if key, _ := utils.MapPossibleItemByKey(Map(filedMap), relationBindToFieldName); key == "" {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`cannot find possible related attribute name "%s" from given relation fields "%s"`,
relationBindToFieldName,
relationFields,
)
} else {
relationBindToFieldName = key
}
}
relationBindToFieldNameChecked = true
}
switch bindToAttrKind {
case reflect.Array, reflect.Slice:
if len(relationDataMap) > 0 {
relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName)
if relationFromAttrField.IsValid() {
//results := make(Result, 0)
results := make([]interface{}, 0)
for _, v := range SliceAny(relationDataMap[String(relationFromAttrField.Interface())]) {
item := v
results = append(results, item)
}
if err = Structs(results, bindToAttrValue.Addr()); err != nil {
return err
}
} else {
// Maybe the attribute does not exist yet.
return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields)
}
} else {
return gerror.NewCodef(
gcode.CodeInvalidParameter,
`relationKey should not be empty as field "%s" is slice`,
bindToAttrName,
)
}
case reflect.Ptr:
var element reflect.Value
if bindToAttrValue.IsNil() {
element = reflect.New(bindToAttrType.Elem()).Elem()
} else {
element = bindToAttrValue.Elem()
}
if len(relationDataMap) > 0 {
relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName)
if relationFromAttrField.IsValid() {
v := relationDataMap[String(relationFromAttrField.Interface())]
if v == nil {
// There's no relational data.
continue
}
if utils.IsSlice(v) {
if err = Struct(SliceAny(v)[0], element); err != nil {
return err
}
} else {
if err = Struct(v, element); err != nil {
return err
}
}
} else {
// Maybe the attribute does not exist yet.
return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields)
}
} else {
if i >= len(maps) {
// There's no relational data.
continue
}
v := maps[i]
if v == nil {
// There's no relational data.
continue
}
if err = Struct(v, element); err != nil {
return err
}
}
bindToAttrValue.Set(element.Addr())
case reflect.Struct:
if len(relationDataMap) > 0 {
relationFromAttrField = relationFromAttrValue.FieldByName(relationBindToFieldName)
if relationFromAttrField.IsValid() {
relationDataItem := relationDataMap[String(relationFromAttrField.Interface())]
if relationDataItem == nil {
// There's no relational data.
continue
}
if utils.IsSlice(relationDataItem) {
if err = Struct(SliceAny(relationDataItem)[0], bindToAttrValue); err != nil {
return err
}
} else {
if err = Struct(relationDataItem, bindToAttrValue); err != nil {
return err
}
}
} else {
// Maybe the attribute does not exist yet.
return gerror.NewCodef(gcode.CodeInvalidParameter, `invalid relation fields specified: "%v"`, relationFields)
}
} else {
if i >= len(maps) {
// There's no relational data.
continue
}
relationDataItem := maps[i]
if relationDataItem == nil {
// There's no relational data.
continue
}
if err = Struct(relationDataItem, bindToAttrValue); err != nil {
return err
}
}
default:
return gerror.NewCodef(gcode.CodeInvalidParameter, `unsupported attribute type: %s`, bindToAttrKind.String())
}
}
reflect.ValueOf(structSlicePointer).Elem().Set(arrayValue)
return nil
}

View File

@ -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)
})
}

View File

@ -15,6 +15,16 @@ import (
"strings"
)
// iString is used for type assert api for String().
type iString interface {
String() string
}
// iMarshalJSON is the interface for custom Json marshaling.
type iMarshalJSON interface {
MarshalJSON() ([]byte, error)
}
// ExportOption specifies the behavior of function Export.
type ExportOption struct {
WithoutType bool // WithoutType specifies exported content has no type information.
@ -151,7 +161,7 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE
"%s%v:%s",
newIndent,
mapKeyStr,
gstr.Repeat(" ", maxSpaceNum-tmpSpaceNum+1),
strings.Repeat(" ", maxSpaceNum-tmpSpaceNum+1),
))
} else {
buffer.WriteString(fmt.Sprintf(
@ -159,7 +169,7 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE
newIndent,
mapKey.Type().String(),
mapKeyStr,
gstr.Repeat(" ", maxSpaceNum-tmpSpaceNum+1),
strings.Repeat(" ", maxSpaceNum-tmpSpaceNum+1),
))
}
doExport(reflectValue.MapIndex(mapKey).Interface(), newIndent, buffer, option)
@ -173,10 +183,35 @@ 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))
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)
} else {
buffer.WriteString(fmt.Sprintf(
"%s(%s) %s",
reflectTypeName,
attributeCountStr,
structContentStr,
))
}
return
}
@ -202,7 +237,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")

View File

@ -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)
}

View File

@ -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`.

View File

@ -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")})
})
}

View File

@ -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`

View File

@ -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,20 @@ 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 {
func RegisterRule(rule string, f RuleFunc) {
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) {
for k, v := range m {
customRuleFuncMap[k] = v
}
}
// 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)
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -8,8 +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"
@ -60,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 (
@ -86,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 {
@ -133,9 +134,14 @@ 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()
ruleErrorMap[ruleKey] = err
} else {
match = true
}
@ -154,7 +160,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E
},
)
if !match && err != nil {
errorMsgArray[ruleKey] = err.Error()
ruleErrorMap[ruleKey] = err
}
}
@ -162,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.
@ -173,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,
},
)
}
@ -217,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
}
@ -231,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
}
@ -282,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.
@ -300,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.
@ -319,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.
@ -505,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
}

View File

@ -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:两次密码输入不相等]
@ -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
})

View File

@ -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")
})
}

View File

@ -12,29 +12,27 @@ 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"
)
func Test_CustomRule1(t *testing.T) {
rule := "custom"
err := gvalid.RegisterRule(
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
},
)
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")
@ -71,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, rule string, value interface{}, message string, data interface{}) error {
m := gconv.Map(value)
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
})
gtest.Assert(err, nil)
// Check.
gtest.C(t, func(t *gtest.T) {
errStr := "data map should not be empty"
@ -115,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, rule string, value interface{}, message string, data interface{}) error {
s := gconv.String(value)
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.
gtest.C(t, func(t *gtest.T) {
errStr := "error"
@ -160,13 +156,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 +210,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
}

View File

@ -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("错误信息不匹配")
}
}