diff --git a/.example/database/gdb/driver/driver/driver.go b/.example/database/gdb/driver/driver/driver.go index 5fd86304d..71af8399d 100644 --- a/.example/database/gdb/driver/driver/driver.go +++ b/.example/database/gdb/driver/driver/driver.go @@ -1,12 +1,7 @@ -// 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 driver import ( + "context" "database/sql" "github.com/gogf/gf/database/gdb" "github.com/gogf/gf/os/gtime" @@ -47,9 +42,9 @@ func (d *MyDriver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { // DoQuery commits the sql string and its arguments to underlying driver // through given link object and returns the execution result. -func (d *MyDriver) DoQuery(link gdb.Link, sql string, args ...interface{}) (rows *sql.Rows, err error) { +func (d *MyDriver) DoQuery(ctx context.Context, link gdb.Link, sql string, args ...interface{}) (rows *sql.Rows, err error) { tsMilli := gtime.TimestampMilli() - rows, err = d.DriverMysql.DoQuery(link, sql, args...) + rows, err = d.DriverMysql.DoQuery(ctx, link, sql, args...) link.Exec( "INSERT INTO `monitor`(`sql`,`cost`,`time`,`error`) VALUES(?,?,?,?)", gdb.FormatSqlWithArgs(sql, args), @@ -62,9 +57,9 @@ func (d *MyDriver) DoQuery(link gdb.Link, sql string, args ...interface{}) (rows // DoExec commits the query string and its arguments to underlying driver // through given link object and returns the execution result. -func (d *MyDriver) DoExec(link gdb.Link, sql string, args ...interface{}) (result sql.Result, err error) { +func (d *MyDriver) DoExec(ctx context.Context, link gdb.Link, sql string, args ...interface{}) (result sql.Result, err error) { tsMilli := gtime.TimestampMilli() - result, err = d.DriverMysql.DoExec(link, sql, args...) + result, err = d.DriverMysql.DoExec(ctx, link, sql, args...) link.Exec( "INSERT INTO `monitor`(`sql`,`cost`,`time`,`error`) VALUES(?,?,?,?)", gdb.FormatSqlWithArgs(sql, args), diff --git a/.example/database/gdb/mysql/gdb_distinct.go b/.example/database/gdb/mysql/gdb_distinct.go new file mode 100644 index 000000000..189422f6b --- /dev/null +++ b/.example/database/gdb/mysql/gdb_distinct.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" +) + +func main() { + g.DB().Model("user").Distinct().CountColumn("uid,name") +} diff --git a/.example/database/gdb/mysql/gdb_transaction.go b/.example/database/gdb/mysql/gdb_transaction.go new file mode 100644 index 000000000..fe5642ca2 --- /dev/null +++ b/.example/database/gdb/mysql/gdb_transaction.go @@ -0,0 +1,27 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" +) + +func main() { + var ( + db = g.DB() + table = "user" + ) + tx, err := db.Begin() + if err != nil { + panic(err) + } + if err = tx.Begin(); err != nil { + panic(err) + } + _, err = tx.Model(table).Data(g.Map{"id": 1, "name": "john"}).Insert() + if err = tx.Rollback(); err != nil { + panic(err) + } + _, err = tx.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert() + if err = tx.Commit(); err != nil { + panic(err) + } +} diff --git a/.example/database/gdb/mysql/gdb_transaction_closure.go b/.example/database/gdb/mysql/gdb_transaction_closure.go new file mode 100644 index 000000000..aa05d48d4 --- /dev/null +++ b/.example/database/gdb/mysql/gdb_transaction_closure.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/gogf/gf/database/gdb" + "github.com/gogf/gf/frame/g" +) + +func main() { + var ( + err error + db = g.DB() + table = "user" + ) + if err = db.Transaction(func(tx *gdb.TX) error { + // Nested transaction 1. + if err = tx.Transaction(func(tx *gdb.TX) error { + _, err = tx.Model(table).Data(g.Map{"id": 1, "name": "john"}).Insert() + return err + }); err != nil { + return err + } + // Nested transaction 2, panic. + if err = tx.Transaction(func(tx *gdb.TX) error { + _, err = tx.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert() + // Create a panic that can make this transaction rollback automatically. + panic("error") + }); err != nil { + return err + } + return nil + }); err != nil { + panic(err) + } +} diff --git a/.example/database/gdb/mysql/gdb_transaction_savepoint.go b/.example/database/gdb/mysql/gdb_transaction_savepoint.go new file mode 100644 index 000000000..e677f15ab --- /dev/null +++ b/.example/database/gdb/mysql/gdb_transaction_savepoint.go @@ -0,0 +1,40 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" +) + +func main() { + var ( + err error + db = g.DB() + table = "user" + ) + tx, err := db.Begin() + if err != nil { + panic(err) + } + defer func() { + if err := recover(); err != nil { + _ = tx.Rollback() + } + }() + if _, err = tx.Model(table).Data(g.Map{"id": 1, "name": "john"}).Insert(); err != nil { + panic(err) + } + if err = tx.SavePoint("MyPoint"); err != nil { + panic(err) + } + if _, err = tx.Model(table).Data(g.Map{"id": 2, "name": "smith"}).Insert(); err != nil { + panic(err) + } + if _, err = tx.Model(table).Data(g.Map{"id": 3, "name": "green"}).Insert(); err != nil { + panic(err) + } + if err = tx.RollbackTo("MyPoint"); err != nil { + panic(err) + } + if err = tx.Commit(); err != nil { + panic(err) + } +} diff --git a/.example/database/gdb/mysql/gdb_with_insert.go b/.example/database/gdb/mysql/gdb_with_insert.go new file mode 100644 index 000000000..669ab3ce6 --- /dev/null +++ b/.example/database/gdb/mysql/gdb_with_insert.go @@ -0,0 +1,66 @@ +package main + +import ( + "fmt" + "github.com/gogf/gf/database/gdb" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/util/gmeta" +) + +func main() { + type UserDetail struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + } + + type UserScore struct { + gmeta.Meta `orm:"table:user_score"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + type User struct { + gmeta.Meta `orm:"table:user"` + Id int `json:"id"` + Name string `json:"name"` + UserDetail *UserDetail `orm:"with:uid=id"` + UserScores []*UserScore `orm:"with:uid=id"` + } + + db := g.DB() + db.Transaction(func(tx *gdb.TX) error { + for i := 1; i <= 5; i++ { + // User. + user := User{ + Name: fmt.Sprintf(`name_%d`, i), + } + lastInsertId, err := db.Model(user).Data(user).OmitEmpty().InsertAndGetId() + if err != nil { + return err + } + // Detail. + userDetail := UserDetail{ + Uid: int(lastInsertId), + Address: fmt.Sprintf(`address_%d`, lastInsertId), + } + _, err = db.Model(userDetail).Data(userDetail).OmitEmpty().Insert() + if err != nil { + return err + } + // Scores. + for j := 1; j <= 5; j++ { + userScore := UserScore{ + Uid: int(lastInsertId), + Score: j, + } + _, err = db.Model(userScore).Data(userScore).OmitEmpty().Insert() + if err != nil { + return err + } + } + } + return nil + }) +} diff --git a/.example/database/gdb/mysql/gdb_with_slect.go b/.example/database/gdb/mysql/gdb_with_slect.go new file mode 100644 index 000000000..e15cf40f9 --- /dev/null +++ b/.example/database/gdb/mysql/gdb_with_slect.go @@ -0,0 +1,37 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/util/gmeta" +) + +func main() { + type UserDetail struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + } + + type UserScore struct { + gmeta.Meta `orm:"table:user_score"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + type User struct { + gmeta.Meta `orm:"table:user"` + Id int `json:"id"` + Name string `json:"name"` + UserDetail *UserDetail `orm:"with:uid=id"` + UserScores []*UserScore `orm:"with:uid=id"` + } + + db := g.DB() + var user *User + err := db.Model(user).WithAll().Where("id", 3).Scan(&user) + if err != nil { + panic(err) + } + g.Dump(user) +} diff --git a/.example/database/gredis/gredis.go b/.example/database/gredis/gredis.go index 0ba2a4b2c..a276da6f9 100644 --- a/.example/database/gredis/gredis.go +++ b/.example/database/gredis/gredis.go @@ -9,10 +9,11 @@ import ( // 使用原生gredis.New操作redis,但是注意需要自己调用Close方法关闭redis链接池 func main() { - redis := gredis.New(gredis.Config{ + config := &gredis.Config{ Host: "127.0.0.1", Port: 6379, - }) + } + redis := gredis.New(config) defer redis.Close() redis.Do("SET", "k", "v") v, _ := redis.Do("GET", "k") diff --git a/.example/i18n/gi18n/gi18n-dir.go b/.example/i18n/gi18n/gi18n-dir.go index e9359c5ea..552298176 100644 --- a/.example/i18n/gi18n/gi18n-dir.go +++ b/.example/i18n/gi18n/gi18n-dir.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "github.com/gogf/gf/i18n/gi18n" @@ -13,6 +14,6 @@ func main() { if err != nil { panic(err) } - fmt.Println(t.Translate(`hello`)) - fmt.Println(t.Translate(`{#hello}{#world}!`)) + fmt.Println(t.Translate(context.TODO(), `hello`)) + fmt.Println(t.Translate(context.TODO(), `{#hello}{#world}!`)) } diff --git a/.example/i18n/gi18n/gi18n-file.go b/.example/i18n/gi18n/gi18n-file.go index 1a6d337c3..f1a4423ee 100644 --- a/.example/i18n/gi18n/gi18n-file.go +++ b/.example/i18n/gi18n/gi18n-file.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "github.com/gogf/gf/i18n/gi18n" @@ -13,6 +14,6 @@ func main() { if err != nil { panic(err) } - fmt.Println(t.Translate(`hello`)) - fmt.Println(t.Translate(`{#hello}{#world}!`)) + fmt.Println(t.Translate(context.TODO(), `hello`)) + fmt.Println(t.Translate(context.TODO(), `{#hello}{#world}!`)) } diff --git a/.example/i18n/gi18n/gi18n.go b/.example/i18n/gi18n/gi18n.go index 630d5a954..17d830c0d 100644 --- a/.example/i18n/gi18n/gi18n.go +++ b/.example/i18n/gi18n/gi18n.go @@ -1,8 +1,10 @@ package main import ( + "context" "fmt" "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/i18n/gi18n" ) func main() { @@ -10,6 +12,12 @@ func main() { orderId = 865271654 orderAmount = 99.8 ) - fmt.Println(g.I18n().Tfl(`en`, `{#OrderPaid}`, orderId, orderAmount)) - fmt.Println(g.I18n().Tfl(`zh-CN`, `{#OrderPaid}`, orderId, orderAmount)) + fmt.Println(g.I18n().Tf( + gi18n.WithLanguage(context.TODO(), `en`), + `{#OrderPaid}`, orderId, orderAmount, + )) + fmt.Println(g.I18n().Tf( + gi18n.WithLanguage(context.TODO(), `zh-CN`), + `{#OrderPaid}`, orderId, orderAmount, + )) } diff --git a/.example/i18n/gi18n/http_view_i18n.go b/.example/i18n/gi18n/http_view_i18n.go index ca5956c7b..cbf0b6e36 100644 --- a/.example/i18n/gi18n/http_view_i18n.go +++ b/.example/i18n/gi18n/http_view_i18n.go @@ -2,15 +2,19 @@ package main import ( "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/i18n/gi18n" "github.com/gogf/gf/net/ghttp" ) func main() { - g.I18n().SetPath("/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/i18n/gi18n/i18n") s := g.Server() - s.BindHandler("/", func(r *ghttp.Request) { - r.Response.WriteTplContent(`{#hello}{#world}!`, g.Map{ - "I18nLanguage": r.Get("lang", "zh-CN"), + s.Group("/", func(group *ghttp.RouterGroup) { + group.Middleware(func(r *ghttp.Request) { + r.SetCtx(gi18n.WithLanguage(r.Context(), r.GetString("lang", "zh-CN"))) + r.Middleware.Next() + }) + group.ALL("/", func(r *ghttp.Request) { + r.Response.WriteTplContent(`{#hello}{#world}!`) }) }) s.SetPort(8199) diff --git a/.example/i18n/gi18n/i18n/zh-CN.toml b/.example/i18n/gi18n/i18n/zh-CN.toml index 80acf06de..20406d93a 100644 --- a/.example/i18n/gi18n/i18n/zh-CN.toml +++ b/.example/i18n/gi18n/i18n/zh-CN.toml @@ -1 +1,3 @@ -OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。" \ No newline at end of file +OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。" +hello = "你好" +world = "世界" \ No newline at end of file diff --git a/.example/net/ghttp/client/middleware/client.go b/.example/net/ghttp/client/middleware/client.go new file mode 100644 index 000000000..7ba07b4bf --- /dev/null +++ b/.example/net/ghttp/client/middleware/client.go @@ -0,0 +1,64 @@ +package main + +import ( + "bytes" + "fmt" + "github.com/gogf/gf/container/garray" + "github.com/gogf/gf/crypto/gmd5" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/internal/json" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/guid" + "github.com/gogf/gf/util/gutil" + "io/ioutil" + "net/http" +) + +const ( + appId = "123" + appSecret = "456" +) + +// 注入统一的接口签名参数 +func injectSignature(jsonContent []byte) []byte { + var m map[string]interface{} + _ = json.Unmarshal(jsonContent, &m) + if len(m) > 0 { + m["appid"] = appId + m["nonce"] = guid.S() + m["timestamp"] = gtime.Timestamp() + var ( + keyArray = garray.NewSortedStrArrayFrom(gutil.Keys(m)) + sigContent string + ) + keyArray.Iterator(func(k int, v string) bool { + sigContent += v + sigContent += gconv.String(m[v]) + return true + }) + m["signature"] = gmd5.MustEncryptString(gmd5.MustEncryptString(sigContent) + appSecret) + jsonContent, _ = json.Marshal(m) + } + return jsonContent +} + +func main() { + c := g.Client() + c.Use(func(c *ghttp.Client, r *http.Request) (resp *ghttp.ClientResponse, err error) { + bodyBytes, _ := ioutil.ReadAll(r.Body) + if len(bodyBytes) > 0 { + // 注入签名相关参数,修改Request原有的提交参数 + bodyBytes = injectSignature(bodyBytes) + r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) + r.ContentLength = int64(len(bodyBytes)) + } + return c.Next(r) + }) + content := c.ContentJson().PostContent("http://127.0.0.1:8199/", g.Map{ + "name": "goframe", + "site": "https://goframe.org", + }) + fmt.Println(content) +} diff --git a/.example/net/ghttp/client/middleware/server.go b/.example/net/ghttp/client/middleware/server.go new file mode 100644 index 000000000..8770dd634 --- /dev/null +++ b/.example/net/ghttp/client/middleware/server.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" +) + +func main() { + s := g.Server() + s.Group("/", func(group *ghttp.RouterGroup) { + group.ALL("/", func(r *ghttp.Request) { + r.Response.Write(r.GetMap()) + }) + }) + s.SetPort(8199) + s.Run() +} diff --git a/.example/net/ghttp/server/request/json-xml/test2.go b/.example/net/ghttp/server/request/json-xml/test2.go index 5e3aed050..ced00e20d 100644 --- a/.example/net/ghttp/server/request/json-xml/test2.go +++ b/.example/net/ghttp/server/request/json-xml/test2.go @@ -25,7 +25,7 @@ func main() { //fmt.Println(r.GetBody()) if err := r.Parse(&req); err != nil { // Validation error. - if v, ok := err.(*gvalid.Error); ok { + if v, ok := err.(gvalid.Error); ok { r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: v.FirstString(), diff --git a/.example/net/ghttp/server/request/request_validation.go b/.example/net/ghttp/server/request/request_validation.go index fa23190b2..2e180ad77 100644 --- a/.example/net/ghttp/server/request/request_validation.go +++ b/.example/net/ghttp/server/request/request_validation.go @@ -18,13 +18,13 @@ func main() { s.Group("/", func(rgroup *ghttp.RouterGroup) { rgroup.ALL("/user", func(r *ghttp.Request) { user := new(User) - if err := r.GetToStruct(user); err != nil { + if err := r.GetStruct(user); err != nil { r.Response.WriteJsonExit(g.Map{ "message": err, "errcode": 1, }) } - if err := gvalid.CheckStruct(user, nil); err != nil { + if err := gvalid.CheckStruct(r.Context(), user, nil); err != nil { r.Response.WriteJsonExit(g.Map{ "message": err.Maps(), "errcode": 1, diff --git a/.example/net/ghttp/server/request/validation/validation1.go b/.example/net/ghttp/server/request/validation/validation1/validation1.go similarity index 100% rename from .example/net/ghttp/server/request/validation/validation1.go rename to .example/net/ghttp/server/request/validation/validation1/validation1.go diff --git a/.example/net/ghttp/server/request/validation/validation2.go b/.example/net/ghttp/server/request/validation/validation2/validation2.go similarity index 96% rename from .example/net/ghttp/server/request/validation/validation2.go rename to .example/net/ghttp/server/request/validation/validation2/validation2.go index ccee1e3f9..2800feab1 100644 --- a/.example/net/ghttp/server/request/validation/validation2.go +++ b/.example/net/ghttp/server/request/validation/validation2/validation2.go @@ -24,7 +24,7 @@ func main() { var req *RegisterReq if err := r.Parse(&req); err != nil { // Validation error. - if v, ok := err.(*gvalid.Error); ok { + if v, ok := err.(gvalid.Error); ok { r.Response.WriteJsonExit(RegisterRes{ Code: 1, Error: v.FirstString(), diff --git a/.example/net/ghttp/server/session/redis/redis_bigint.go b/.example/net/ghttp/server/session/redis/redis_bigint.go new file mode 100644 index 000000000..b8a5b3460 --- /dev/null +++ b/.example/net/ghttp/server/session/redis/redis_bigint.go @@ -0,0 +1,36 @@ +package main + +import ( + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/os/gsession" +) + +func main() { + type User struct { + Id int64 + Name string + } + s := g.Server() + s.SetSessionStorage(gsession.NewStorageRedis(g.Redis())) + s.Group("/", func(group *ghttp.RouterGroup) { + group.GET("/set", func(r *ghttp.Request) { + user := &User{ + Id: 1265476890672672808, + Name: "john", + } + if err := r.Session.Set("user", user); err != nil { + panic(err) + } + r.Response.Write("ok") + }) + group.GET("/get", func(r *ghttp.Request) { + r.Response.WriteJson(r.Session.Get("user")) + }) + group.GET("/clear", func(r *ghttp.Request) { + r.Session.Clear() + }) + }) + s.SetPort(8199) + s.Run() +} diff --git a/.example/os/gproc/signal/signal_handler.go b/.example/os/gproc/signal/signal_handler.go new file mode 100644 index 000000000..a1d495347 --- /dev/null +++ b/.example/os/gproc/signal/signal_handler.go @@ -0,0 +1,54 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" + "time" +) + +func signalHandlerForMQ() { + var ( + sig os.Signal + receivedChan = make(chan os.Signal) + ) + signal.Notify( + receivedChan, + syscall.SIGINT, + syscall.SIGQUIT, + syscall.SIGKILL, + syscall.SIGTERM, + syscall.SIGABRT, + ) + for { + sig = <-receivedChan + fmt.Println("MQ is shutting down due to signal:", sig.String()) + time.Sleep(time.Second) + fmt.Println("MQ is shut down smoothly") + return + } +} + +func main() { + fmt.Println("Process start, pid:", os.Getpid()) + go signalHandlerForMQ() + + var ( + sig os.Signal + receivedChan = make(chan os.Signal) + ) + signal.Notify( + receivedChan, + syscall.SIGINT, + syscall.SIGQUIT, + syscall.SIGKILL, + syscall.SIGTERM, + syscall.SIGABRT, + ) + for { + sig = <-receivedChan + fmt.Println("MainProcess is shutting down due to signal:", sig.String()) + return + } +} diff --git a/.example/os/gproc/signal/signal_handler_gproc.go b/.example/os/gproc/signal/signal_handler_gproc.go new file mode 100644 index 000000000..e8e473484 --- /dev/null +++ b/.example/os/gproc/signal/signal_handler_gproc.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "github.com/gogf/gf/os/gproc" + "os" + "time" +) + +func signalHandlerForMQ(sig os.Signal) { + fmt.Println("MQ is shutting down due to signal:", sig.String()) + time.Sleep(time.Second) + fmt.Println("MQ is shut down smoothly") +} + +func signalHandlerForMain(sig os.Signal) { + fmt.Println("MainProcess is shutting down due to signal:", sig.String()) +} + +func main() { + fmt.Println("Process start, pid:", os.Getpid()) + gproc.AddSigHandlerShutdown( + signalHandlerForMQ, + signalHandlerForMain, + ) + gproc.Listen() +} diff --git a/.example/util/gvalid/config.toml b/.example/util/gvalid/config.toml new file mode 100644 index 000000000..26beac3a7 --- /dev/null +++ b/.example/util/gvalid/config.toml @@ -0,0 +1,14 @@ + + + +# MySQL. +[database] + [database.default] + link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test" + debug = true + + + + + + diff --git a/.example/util/gvalid/gvalid.go b/.example/util/gvalid/gvalid.go index 9975bd443..4d6212876 100644 --- a/.example/util/gvalid/gvalid.go +++ b/.example/util/gvalid/gvalid.go @@ -1,31 +1,32 @@ package main import ( + "context" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gvalid" ) func main() { //rule := "length:6,16" - //if m := gvalid.Check("123456", rule, nil); m != nil { + //if m := gvalid.Check(context.TODO(), "123456", rule, nil); m != nil { // fmt.Println(m) //} - //if m := gvalid.Check("12345", rule, nil); m != nil { + //if m := gvalid.Check(context.TODO(), "12345", rule, nil); m != nil { // fmt.Println(m) // // map[length:字段长度为6到16个字符] //} //rule := "integer|between:6,16" //msgs := "请输入一个整数|参数大小不对啊老铁" - //fmt.Println(gvalid.Check("5.66", rule, msgs)) + //fmt.Println(gvalid.Check(context.TODO(), "5.66", rule, msgs)) //// map[integer:请输入一个整数 between:参数大小不对啊老铁] //// 参数长度至少为6个数字或者6个字母,但是总长度不能超过16个字符 //rule := `regex:\d{6,}|\D{6,}|max-length:16` - //if m := gvalid.Check("123456", rule, nil); m != nil { + //if m := gvalid.Check(context.TODO(), "123456", rule, nil); m != nil { // fmt.Println(m) //} - //if m := gvalid.Check("abcde6", rule, nil); m != nil { + //if m := gvalid.Check(context.TODO(), "abcde6", rule, nil); m != nil { // fmt.Println(m) // // map[regex:字段值不合法] //} @@ -40,18 +41,20 @@ func main() { // "password" : "required|length:6,16|same:password2", // "password2" : "required|length:6,16", //} - //fmt.Println(gvalid.CheckMap(params, rules)) + //fmt.Println(gvalid.CheckMap(context.TODO(), params, rules)) //// map[passport:map[length:字段长度为6到16个字符] password:map[same:字段值不合法]] params := map[string]interface{}{ "passport": "john", "password": "123456", "password2": "1234567", + "name": "gf", } rules := map[string]string{ "passport": "required|length:6,16", "password": "required|length:6,16|same:password2", "password2": "required|length:6,16", + "name": "size:5", } msgs := map[string]interface{}{ "passport": "账号不能为空|账号长度应当在:min到:max之间", @@ -59,8 +62,9 @@ func main() { "required": "密码不能为空", "same": "两次密码输入不相等", }, + "name": "名字长度必须为:size", } - if e := gvalid.CheckMap(params, rules, msgs); e != nil { + if e := gvalid.CheckMap(context.TODO(), params, rules, msgs); e != nil { g.Dump(e.Maps()) } // map[passport:map[length:账号长度应当在6到16之间] password:map[same:两次密码输入不相等]] diff --git a/.example/util/gvalid/gvalid_checkstructwithdata.go b/.example/util/gvalid/gvalid_checkstructwithdata.go new file mode 100644 index 000000000..7e1636f88 --- /dev/null +++ b/.example/util/gvalid/gvalid_checkstructwithdata.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gvalid" +) + +func main() { + type User struct { + Name string `v:"required#请输入用户姓名"` + Type int `v:"required#请选择用户类型"` + } + data := g.Map{ + "name": "john", + } + user := User{} + if err := gconv.Scan(data, &user); err != nil { + panic(err) + } + err := gvalid.CheckStructWithData(context.TODO(), user, data, nil) + // 也可以使用 + // err := g.Validator().Data(data).CheckStruct(user) + if err != nil { + g.Dump(err.Items()) + } +} diff --git a/.example/util/gvalid/gvalid_custom_message.go b/.example/util/gvalid/gvalid_custom_message.go index 816749f10..3c8798693 100644 --- a/.example/util/gvalid/gvalid_custom_message.go +++ b/.example/util/gvalid/gvalid_custom_message.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gvalid" @@ -8,6 +9,6 @@ import ( func main() { g.I18n().SetLanguage("cn") - err := gvalid.Check("", "required", nil) + err := gvalid.Check(context.TODO(), "", "required", nil) fmt.Println(err.String()) } diff --git a/.example/util/gvalid/gvalid_error.go b/.example/util/gvalid/gvalid_error.go index fe5c0a2f2..1f5d350d4 100644 --- a/.example/util/gvalid/gvalid_error.go +++ b/.example/util/gvalid/gvalid_error.go @@ -1,6 +1,7 @@ package main import ( + "context" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gvalid" ) @@ -17,7 +18,7 @@ func main() { ConfiemPassword: "", } - e := gvalid.CheckStruct(user, nil) + e := gvalid.CheckStruct(context.TODO(), user, nil) g.Dump(e.Map()) g.Dump(e.Maps()) g.Dump(e.String()) diff --git a/.example/util/gvalid/gvalid_i18n.go b/.example/util/gvalid/gvalid_i18n.go new file mode 100644 index 000000000..cb2a5e905 --- /dev/null +++ b/.example/util/gvalid/gvalid_i18n.go @@ -0,0 +1,38 @@ +package main + +import ( + "context" + + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/i18n/gi18n" + "github.com/gogf/gf/util/gconv" +) + +func main() { + type User struct { + Name string `v:"required#ReuiredUserName"` + Type int `v:"required#ReuiredUserType"` + Project string `v:"size:10#MustSize"` + } + var ( + data = g.Map{ + "name": "john", + "project": "gf", + } + user = User{} + ctxEn = gi18n.WithLanguage(context.TODO(), "en") + ctxCh = gi18n.WithLanguage(context.TODO(), "zh-CN") + ) + + if err := gconv.Scan(data, &user); err != nil { + panic(err) + } + // 英文 + if err := g.Validator().Ctx(ctxEn).Data(data).CheckStruct(user); err != nil { + g.Dump(err.String()) + } + // 中文 + if err := g.Validator().Ctx(ctxCh).Data(data).CheckStruct(user); err != nil { + g.Dump(err.String()) + } +} diff --git a/.example/util/gvalid/gvalid_result.go b/.example/util/gvalid/gvalid_result.go index 0190f0df4..ff9e01e6d 100644 --- a/.example/util/gvalid/gvalid_result.go +++ b/.example/util/gvalid/gvalid_result.go @@ -1,6 +1,7 @@ package main import ( + "context" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gvalid" ) @@ -18,7 +19,7 @@ func main() { Pass2: "123", } - e := gvalid.CheckStruct(user, nil) + e := gvalid.CheckStruct(context.TODO(), user, nil) g.Dump(e.String()) g.Dump(e.FirstString()) } diff --git a/.example/util/gvalid/gvalid_sequence.go b/.example/util/gvalid/gvalid_sequence.go index 8274cd1c9..0296a4ab5 100644 --- a/.example/util/gvalid/gvalid_sequence.go +++ b/.example/util/gvalid/gvalid_sequence.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "github.com/gogf/gf/util/gvalid" @@ -17,7 +18,7 @@ func main() { "password@required|length:6,16|same:password2#密码不能为空}|两次密码输入不相等", "password2@required|length:6,16#", } - if e := gvalid.CheckMap(params, rules); e != nil { + if e := gvalid.CheckMap(context.TODO(), params, rules); e != nil { fmt.Println(e.Map()) fmt.Println(e.FirstItem()) fmt.Println(e.FirstString()) diff --git a/.example/util/gvalid/gvalid_struct1.go b/.example/util/gvalid/gvalid_struct1.go index d992860ee..6995ec2be 100644 --- a/.example/util/gvalid/gvalid_struct1.go +++ b/.example/util/gvalid/gvalid_struct1.go @@ -1,6 +1,7 @@ package main import ( + "context" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gvalid" ) @@ -20,7 +21,7 @@ func main() { } // 使用结构体定义的校验规则和错误提示进行校验 - g.Dump(gvalid.CheckStruct(user, nil).Map()) + g.Dump(gvalid.CheckStruct(context.TODO(), user, nil).Map()) // 自定义校验规则和错误提示,对定义的特定校验规则和错误提示进行覆盖 rules := map[string]string{ @@ -31,5 +32,5 @@ func main() { "password3": "名称不能为空", }, } - g.Dump(gvalid.CheckStruct(user, rules, msgs).Map()) + g.Dump(gvalid.CheckStruct(context.TODO(), user, rules, msgs).Map()) } diff --git a/.example/util/gvalid/gvalid_struct2.go b/.example/util/gvalid/gvalid_struct2.go index a4c9442fb..8e6c11601 100644 --- a/.example/util/gvalid/gvalid_struct2.go +++ b/.example/util/gvalid/gvalid_struct2.go @@ -1,6 +1,7 @@ package main import ( + "context" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gvalid" ) @@ -13,5 +14,5 @@ func main() { user := &User{} - g.Dump(gvalid.CheckStruct(user, nil)) + g.Dump(gvalid.CheckStruct(context.TODO(), user, nil)) } diff --git a/.example/util/gvalid/gvalid_struct3.go b/.example/util/gvalid/gvalid_struct3.go index de9980ca3..fdd0ea965 100644 --- a/.example/util/gvalid/gvalid_struct3.go +++ b/.example/util/gvalid/gvalid_struct3.go @@ -1,6 +1,7 @@ package main import ( + "context" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gvalid" ) @@ -15,5 +16,5 @@ func main() { Pass: "1", } - g.Dump(gvalid.CheckStruct(user, nil).Maps()) + g.Dump(gvalid.CheckStruct(context.TODO(), user, nil).Maps()) } diff --git a/.example/util/gvalid/gvalid_struct_meta.go b/.example/util/gvalid/gvalid_struct_meta.go new file mode 100644 index 000000000..a08e8d8ca --- /dev/null +++ b/.example/util/gvalid/gvalid_struct_meta.go @@ -0,0 +1,40 @@ +package main + +import ( + "context" + "fmt" + "github.com/gogf/gf/errors/gerror" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/util/gmeta" +) + +type UserCreateReq struct { + gmeta.Meta `v:"UserCreateReq"` + Name string + Pass string +} + +func UserCreateReqChecker(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + user := &UserCreateReq{} + if v, ok := data.(*UserCreateReq); ok { + user = v + } + // SELECT COUNT(*) FROM `user` WHERE `name` = xxx + count, err := g.Model("user").Ctx(ctx).Where("name", user.Name).Count() + if err != nil { + return err + } + if count > 0 { + return gerror.Newf(`The name "%s" is already token`, user.Name) + } + return nil +} + +func main() { + user := &UserCreateReq{ + Name: "john", + Pass: "123456", + } + err := g.Validator().RuleFunc("UserCreateReq", UserCreateReqChecker).CheckStruct(user) + fmt.Println(err) +} diff --git a/.example/util/gvalid/i18n/cn.toml b/.example/util/gvalid/i18n/cn.toml deleted file mode 100644 index 498f60e24..000000000 --- a/.example/util/gvalid/i18n/cn.toml +++ /dev/null @@ -1,14 +0,0 @@ - - -"gf.gvalid.required" = "字段不能为空" - - - - - - - - - - - diff --git a/.example/util/gvalid/i18n/en.toml b/.example/util/gvalid/i18n/en.toml new file mode 100644 index 000000000..7574fd595 --- /dev/null +++ b/.example/util/gvalid/i18n/en.toml @@ -0,0 +1,7 @@ +"gf.gvalid.required" = "字段不能为空" + + +"ReuiredUserName" = "Please input user name" +"ReuiredUserType" = "Please select user type" +"MustSize" = "Size of :attribute must be :size" + diff --git a/.example/util/gvalid/i18n/zh-CN.toml b/.example/util/gvalid/i18n/zh-CN.toml new file mode 100644 index 000000000..3db393e20 --- /dev/null +++ b/.example/util/gvalid/i18n/zh-CN.toml @@ -0,0 +1,16 @@ + + +"gf.gvalid.required" = "字段不能为空" + +"ReuiredUserName" = "请输入用户名称" +"ReuiredUserType" = "请选择用户类型" +"MustSize" = ":attribute长度必须为:size" + + + + + + + + + diff --git a/.gitee/ISSUE_TEMPLATE.MD b/.gitee/ISSUE_TEMPLATE.MD index 1197badda..4d24425c3 100644 --- a/.gitee/ISSUE_TEMPLATE.MD +++ b/.gitee/ISSUE_TEMPLATE.MD @@ -1,4 +1,7 @@ - + + + + ### 1. 您当前使用的`Go`版本,及系统版本、系统架构? diff --git a/.github/ISSUE_TEMPLATE.MD b/.github/ISSUE_TEMPLATE.MD index 09e907f4e..364044d16 100644 --- a/.github/ISSUE_TEMPLATE.MD +++ b/.github/ISSUE_TEMPLATE.MD @@ -1,5 +1,10 @@ + + + + + ### 1. What version of `Go` and system type/arch are you using? -### 3. Can this issue be reproduced with the latest release? +### 3. Can this issue be re-produced with the latest release? diff --git a/README.MD b/README.MD index ad35aafcc..4b7d82f95 100644 --- a/README.MD +++ b/README.MD @@ -50,17 +50,10 @@ The `Web` component performance of `GoFrame`, please refer to third-party projec # Documentation -* 中文官网: https://goframe.org -* GoDoc API: https://godoc.org/github.com/gogf/gf +* 中文官网: [https://goframe.org](https://goframe.org/display/gf) +* GoDoc API: [https://pkg.go.dev/github.com/gogf/gf](https://pkg.go.dev/github.com/gogf/gf) -# Discussion -- QQ Group:[116707870](//shang.qq.com/wpa/qunwpa?idkey=195f91eceeb5d7fa76009b7cd5a4641f70bf4897b7f5a520635eb26ff17adfe7) -- WX Group:Add friend`389961817` in WeChat, commenting `GF` -- Issues:https://github.com/gogf/gf/issues - -> It's recommended learning `GoFrame` through its awesome source codes and API reference. - # License `GF` is licensed under the [MIT License](LICENSE), 100% free and open-source, forever. diff --git a/README_ZH.MD b/README_ZH.MD index 3575a9cea..79b22cfde 100644 --- a/README_ZH.MD +++ b/README_ZH.MD @@ -13,14 +13,16 @@ > 如果您初识`Go`语言,您可以将`GoFrame`类似于`PHP`中的`Laravel`, `Java`中的`SpringBoot`或者`Python`中的`Django`。 # 特点 -* 模块化、松耦合设计; -* 模块丰富、开箱即用; -* 简便易用、易于维护; -* 高代码质量、高单元测试覆盖率; -* 社区活跃,大牛谦逊低调脾气好; -* 详尽的开发文档及示例; -* 完善的本地中文化支持; -* 设计为团队及企业使用; +* 模块化、松耦合 +* 模块丰富、开箱即用 +* 简洁易用、快速接入 +* 文档详尽、易于维护 +* 自顶向下、体系化设计 +* 统一框架、统一组件、降低选择成本 +* 开发规范、设计模式、代码分层模型 +* 强大便捷的开发工具链 +* 完善的本地中文化支持 +* 设计为团队及企业使用 # 地址 - **主库**:https://github.com/gogf/gf @@ -63,16 +65,10 @@ golang版本 >= 1.11 # 文档 -开发文档:https://goframe.org +官方站点:[https://goframe.org](https://goframe.org/display/gf) -接口文档:https://godoc.org/github.com/gogf/gf +接口文档:[https://pkg.go.dev/github.com/gogf/gf](https://pkg.go.dev/github.com/gogf/gf) -# 帮助 -- QQ交流群:[116707870](//shang.qq.com/wpa/qunwpa?idkey=195f91eceeb5d7fa76009b7cd5a4641f70bf4897b7f5a520635eb26ff17adfe7) -- WX交流群:微信添加`389961817`备注`GF` -- 主库ISSUE:https://github.com/gogf/gf/issues - -> 建议通过阅读`GoFrame`的源码以及API文档深度学习`GoFrame`,了解更多的精妙设计。 # 协议 diff --git a/container/garray/garray_normal_any.go b/container/garray/garray_normal_any.go index 3e0324a10..985c02fc7 100644 --- a/container/garray/garray_normal_any.go +++ b/container/garray/garray_normal_any.go @@ -30,7 +30,7 @@ type Array struct { } // New creates and returns an empty array. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func New(safe ...bool) *Array { return NewArraySize(0, 0, safe...) @@ -42,7 +42,7 @@ func NewArray(safe ...bool) *Array { } // NewArraySize create and returns an array with given size and cap. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewArraySize(size int, cap int, safe ...bool) *Array { return &Array{ @@ -51,8 +51,8 @@ func NewArraySize(size int, cap int, safe ...bool) *Array { } } -// NewArrayRange creates and returns a array by a range from to -// with step value . +// NewArrayRange creates and returns a array by a range from `start` to `end` +// with step value `step`. func NewArrayRange(start, end, step int, safe ...bool) *Array { if step == 0 { panic(fmt.Sprintf(`invalid step value: %d`, step)) @@ -66,18 +66,20 @@ func NewArrayRange(start, end, step int, safe ...bool) *Array { return NewArrayFrom(slice, safe...) } +// NewFrom is alias of NewArrayFrom. // See NewArrayFrom. func NewFrom(array []interface{}, safe ...bool) *Array { return NewArrayFrom(array, safe...) } +// NewFromCopy is alias of NewArrayFromCopy. // See NewArrayFromCopy. func NewFromCopy(array []interface{}, safe ...bool) *Array { return NewArrayFromCopy(array, safe...) } -// NewArrayFrom creates and returns an array with given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewArrayFrom creates and returns an array with given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewArrayFrom(array []interface{}, safe ...bool) *Array { return &Array{ @@ -86,8 +88,8 @@ func NewArrayFrom(array []interface{}, safe ...bool) *Array { } } -// NewArrayFromCopy creates and returns an array from a copy of given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewArrayFromCopy creates and returns an array from a copy of given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewArrayFromCopy(array []interface{}, safe ...bool) *Array { newArray := make([]interface{}, len(array)) @@ -98,8 +100,15 @@ func NewArrayFromCopy(array []interface{}, safe ...bool) *Array { } } +// At returns the value by the specified index. +// If the given `index` is out of range of the array, it returns `nil`. +func (a *Array) At(index int) (value interface{}) { + value, _ = a.Get(index) + return +} + // Get returns the value by the specified index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *Array) Get(index int) (value interface{}, found bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -120,7 +129,7 @@ func (a *Array) Set(index int, value interface{}) error { return nil } -// SetArray sets the underlying slice array with the given . +// SetArray sets the underlying slice array with the given `array`. func (a *Array) SetArray(array []interface{}) *Array { a.mu.Lock() defer a.mu.Unlock() @@ -128,7 +137,7 @@ func (a *Array) SetArray(array []interface{}) *Array { return a } -// Replace replaces the array items by given from the beginning of array. +// Replace replaces the array items by given `array` from the beginning of array. func (a *Array) Replace(array []interface{}) *Array { a.mu.Lock() defer a.mu.Unlock() @@ -152,7 +161,7 @@ func (a *Array) Sum() (sum int) { return } -// SortFunc sorts the array by custom function . +// SortFunc sorts the array by custom function `less`. func (a *Array) SortFunc(less func(v1, v2 interface{}) bool) *Array { a.mu.Lock() defer a.mu.Unlock() @@ -162,7 +171,7 @@ func (a *Array) SortFunc(less func(v1, v2 interface{}) bool) *Array { return a } -// InsertBefore inserts the to the front of . +// InsertBefore inserts the `value` to the front of `index`. func (a *Array) InsertBefore(index int, value interface{}) error { a.mu.Lock() defer a.mu.Unlock() @@ -175,7 +184,7 @@ func (a *Array) InsertBefore(index int, value interface{}) error { return nil } -// InsertAfter inserts the to the back of . +// InsertAfter inserts the `value` to the back of `index`. func (a *Array) InsertAfter(index int, value interface{}) error { a.mu.Lock() defer a.mu.Unlock() @@ -189,7 +198,7 @@ func (a *Array) InsertAfter(index int, value interface{}) error { } // Remove removes an item by index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *Array) Remove(index int) (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -247,14 +256,14 @@ func (a *Array) PushRight(value ...interface{}) *Array { } // PopRand randomly pops and return an item out of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *Array) PopRand() (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() return a.doRemoveWithoutLock(grand.Intn(len(a.array))) } -// PopRands randomly pops and returns items out of array. +// PopRands randomly pops and returns `size` items out of array. func (a *Array) PopRands(size int) []interface{} { a.mu.Lock() defer a.mu.Unlock() @@ -272,7 +281,7 @@ func (a *Array) PopRands(size int) []interface{} { } // PopLeft pops and returns an item from the beginning of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *Array) PopLeft() (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -285,7 +294,7 @@ func (a *Array) PopLeft() (value interface{}, found bool) { } // PopRight pops and returns an item from the end of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *Array) PopRight() (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -298,7 +307,7 @@ func (a *Array) PopRight() (value interface{}, found bool) { return value, true } -// PopLefts pops and returns items from the beginning of array. +// PopLefts pops and returns `size` items from the beginning of array. func (a *Array) PopLefts(size int) []interface{} { a.mu.Lock() defer a.mu.Unlock() @@ -315,7 +324,7 @@ func (a *Array) PopLefts(size int) []interface{} { return value } -// PopRights pops and returns items from the end of array. +// PopRights pops and returns `size` items from the end of array. func (a *Array) PopRights(size int) []interface{} { a.mu.Lock() defer a.mu.Unlock() @@ -337,8 +346,8 @@ func (a *Array) PopRights(size int) []interface{} { // Notice, if in concurrent-safe usage, it returns a copy of slice; // else a pointer to the underlying data. // -// If is negative, then the offset will start from the end of array. -// If is omitted, then the sequence will have everything from start up +// If `end` is negative, then the offset will start from the end of array. +// If `end` is omitted, then the sequence will have everything from start up // until the end of the array. func (a *Array) Range(start int, end ...int) []interface{} { a.mu.RLock() @@ -364,7 +373,7 @@ func (a *Array) Range(start int, end ...int) []interface{} { } // SubSlice returns a slice of elements from the array as specified -// by the and parameters. +// by the `offset` and `size` parameters. // If in concurrent safe usage, it returns a copy of the slice; else a pointer. // // If offset is non-negative, the sequence will start at that offset in the array. @@ -471,7 +480,7 @@ func (a *Array) Contains(value interface{}) bool { return a.Search(value) != -1 } -// Search searches array by , returns the index of , +// Search searches array by `value`, returns the index of `value`, // or returns -1 if not exists. func (a *Array) Search(value interface{}) int { a.mu.RLock() @@ -506,7 +515,7 @@ func (a *Array) Unique() *Array { return a } -// LockFunc locks writing by callback function . +// LockFunc locks writing by callback function `f`. func (a *Array) LockFunc(f func(array []interface{})) *Array { a.mu.Lock() defer a.mu.Unlock() @@ -514,7 +523,7 @@ func (a *Array) LockFunc(f func(array []interface{})) *Array { return a } -// RLockFunc locks reading by callback function . +// RLockFunc locks reading by callback function `f`. func (a *Array) RLockFunc(f func(array []interface{})) *Array { a.mu.RLock() defer a.mu.RUnlock() @@ -522,16 +531,16 @@ func (a *Array) RLockFunc(f func(array []interface{})) *Array { return a } -// Merge merges into current array. -// The parameter can be any garray or slice type. +// Merge merges `array` into current array. +// The parameter `array` can be any garray or slice type. // The difference between Merge and Append is Append supports only specified slice type, // but Merge supports more parameter types. func (a *Array) Merge(array interface{}) *Array { return a.Append(gconv.Interfaces(array)...) } -// Fill fills an array with num entries of the value , -// keys starting at the parameter. +// Fill fills an array with num entries of the value `value`, +// keys starting at the `startIndex` parameter. func (a *Array) Fill(startIndex int, num int, value interface{}) error { a.mu.Lock() defer a.mu.Unlock() @@ -549,7 +558,7 @@ func (a *Array) Fill(startIndex int, num int, value interface{}) error { } // Chunk splits an array into multiple arrays, -// the size of each array is determined by . +// the size of each array is determined by `size`. // The last chunk may contain less than size elements. func (a *Array) Chunk(size int) [][]interface{} { if size < 1 { @@ -571,9 +580,9 @@ func (a *Array) Chunk(size int) [][]interface{} { return n } -// Pad pads array to the specified length with . +// Pad pads array to the specified length with `value`. // If size is positive then the array is padded on the right, or negative on the left. -// If the absolute value of is less than or equal to the length of the array +// If the absolute value of `size` is less than or equal to the length of the array // then no padding takes place. func (a *Array) Pad(size int, val interface{}) *Array { a.mu.Lock() @@ -608,7 +617,7 @@ func (a *Array) Rand() (value interface{}, found bool) { return a.array[grand.Intn(len(a.array))], true } -// Rands randomly returns items from array(no deleting). +// Rands randomly returns `size` items from array(no deleting). func (a *Array) Rands(size int) []interface{} { a.mu.RLock() defer a.mu.RUnlock() @@ -642,7 +651,7 @@ func (a *Array) Reverse() *Array { return a } -// Join joins array elements with a string . +// Join joins array elements with a string `glue`. func (a *Array) Join(glue string) string { a.mu.RLock() defer a.mu.RUnlock() @@ -675,8 +684,8 @@ func (a *Array) Iterator(f func(k int, v interface{}) bool) { a.IteratorAsc(f) } -// IteratorAsc iterates the array readonly in ascending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorAsc iterates the array readonly in ascending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *Array) IteratorAsc(f func(k int, v interface{}) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -687,8 +696,8 @@ func (a *Array) IteratorAsc(f func(k int, v interface{}) bool) { } } -// IteratorDesc iterates the array readonly in descending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorDesc iterates the array readonly in descending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *Array) IteratorDesc(f func(k int, v interface{}) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -736,7 +745,7 @@ func (a *Array) UnmarshalJSON(b []byte) error { } a.mu.Lock() defer a.mu.Unlock() - if err := json.Unmarshal(b, &a.array); err != nil { + if err := json.UnmarshalUseNumber(b, &a.array); err != nil { return err } return nil @@ -748,7 +757,7 @@ func (a *Array) UnmarshalValue(value interface{}) error { defer a.mu.Unlock() switch value.(type) { case string, []byte: - return json.Unmarshal(gconv.Bytes(value), &a.array) + return json.UnmarshalUseNumber(gconv.Bytes(value), &a.array) default: a.array = gconv.SliceAny(value) } @@ -784,7 +793,7 @@ func (a *Array) FilterEmpty() *Array { return a } -// Walk applies a user supplied function to every item of array. +// Walk applies a user supplied function `f` to every item of array. func (a *Array) Walk(f func(value interface{}) interface{}) *Array { a.mu.Lock() defer a.mu.Unlock() diff --git a/container/garray/garray_normal_int.go b/container/garray/garray_normal_int.go index 10b64529d..05a10f0fe 100644 --- a/container/garray/garray_normal_int.go +++ b/container/garray/garray_normal_int.go @@ -28,14 +28,14 @@ type IntArray struct { } // NewIntArray creates and returns an empty array. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewIntArray(safe ...bool) *IntArray { return NewIntArraySize(0, 0, safe...) } // NewIntArraySize create and returns an array with given size and cap. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewIntArraySize(size int, cap int, safe ...bool) *IntArray { return &IntArray{ @@ -44,8 +44,8 @@ func NewIntArraySize(size int, cap int, safe ...bool) *IntArray { } } -// NewIntArrayRange creates and returns a array by a range from to -// with step value . +// NewIntArrayRange creates and returns a array by a range from `start` to `end` +// with step value `step`. func NewIntArrayRange(start, end, step int, safe ...bool) *IntArray { if step == 0 { panic(fmt.Sprintf(`invalid step value: %d`, step)) @@ -59,8 +59,8 @@ func NewIntArrayRange(start, end, step int, safe ...bool) *IntArray { return NewIntArrayFrom(slice, safe...) } -// NewIntArrayFrom creates and returns an array with given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewIntArrayFrom creates and returns an array with given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewIntArrayFrom(array []int, safe ...bool) *IntArray { return &IntArray{ @@ -69,8 +69,8 @@ func NewIntArrayFrom(array []int, safe ...bool) *IntArray { } } -// NewIntArrayFromCopy creates and returns an array from a copy of given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewIntArrayFromCopy creates and returns an array from a copy of given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewIntArrayFromCopy(array []int, safe ...bool) *IntArray { newArray := make([]int, len(array)) @@ -81,8 +81,15 @@ func NewIntArrayFromCopy(array []int, safe ...bool) *IntArray { } } +// At returns the value by the specified index. +// If the given `index` is out of range of the array, it returns `0`. +func (a *IntArray) At(index int) (value int) { + value, _ = a.Get(index) + return +} + // Get returns the value by the specified index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *IntArray) Get(index int) (value int, found bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -103,7 +110,7 @@ func (a *IntArray) Set(index int, value int) error { return nil } -// SetArray sets the underlying slice array with the given . +// SetArray sets the underlying slice array with the given `array`. func (a *IntArray) SetArray(array []int) *IntArray { a.mu.Lock() defer a.mu.Unlock() @@ -111,7 +118,7 @@ func (a *IntArray) SetArray(array []int) *IntArray { return a } -// Replace replaces the array items by given from the beginning of array. +// Replace replaces the array items by given `array` from the beginning of array. func (a *IntArray) Replace(array []int) *IntArray { a.mu.Lock() defer a.mu.Unlock() @@ -136,7 +143,7 @@ func (a *IntArray) Sum() (sum int) { } // Sort sorts the array in increasing order. -// The parameter controls whether sort in increasing order(default) or decreasing order. +// The parameter `reverse` controls whether sort in increasing order(default) or decreasing order. func (a *IntArray) Sort(reverse ...bool) *IntArray { a.mu.Lock() defer a.mu.Unlock() @@ -153,7 +160,7 @@ func (a *IntArray) Sort(reverse ...bool) *IntArray { return a } -// SortFunc sorts the array by custom function . +// SortFunc sorts the array by custom function `less`. func (a *IntArray) SortFunc(less func(v1, v2 int) bool) *IntArray { a.mu.Lock() defer a.mu.Unlock() @@ -163,7 +170,7 @@ func (a *IntArray) SortFunc(less func(v1, v2 int) bool) *IntArray { return a } -// InsertBefore inserts the to the front of . +// InsertBefore inserts the `value` to the front of `index`. func (a *IntArray) InsertBefore(index int, value int) error { a.mu.Lock() defer a.mu.Unlock() @@ -176,7 +183,7 @@ func (a *IntArray) InsertBefore(index int, value int) error { return nil } -// InsertAfter inserts the to the back of . +// InsertAfter inserts the `value` to the back of `index`. func (a *IntArray) InsertAfter(index int, value int) error { a.mu.Lock() defer a.mu.Unlock() @@ -190,7 +197,7 @@ func (a *IntArray) InsertAfter(index int, value int) error { } // Remove removes an item by index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *IntArray) Remove(index int) (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -248,7 +255,7 @@ func (a *IntArray) PushRight(value ...int) *IntArray { } // PopLeft pops and returns an item from the beginning of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *IntArray) PopLeft() (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -261,7 +268,7 @@ func (a *IntArray) PopLeft() (value int, found bool) { } // PopRight pops and returns an item from the end of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *IntArray) PopRight() (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -275,16 +282,16 @@ func (a *IntArray) PopRight() (value int, found bool) { } // PopRand randomly pops and return an item out of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *IntArray) PopRand() (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() return a.doRemoveWithoutLock(grand.Intn(len(a.array))) } -// PopRands randomly pops and returns items out of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRands randomly pops and returns `size` items out of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *IntArray) PopRands(size int) []int { a.mu.Lock() defer a.mu.Unlock() @@ -301,9 +308,9 @@ func (a *IntArray) PopRands(size int) []int { return array } -// PopLefts pops and returns items from the beginning of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopLefts pops and returns `size` items from the beginning of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *IntArray) PopLefts(size int) []int { a.mu.Lock() defer a.mu.Unlock() @@ -320,9 +327,9 @@ func (a *IntArray) PopLefts(size int) []int { return value } -// PopRights pops and returns items from the end of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRights pops and returns `size` items from the end of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *IntArray) PopRights(size int) []int { a.mu.Lock() defer a.mu.Unlock() @@ -344,8 +351,8 @@ func (a *IntArray) PopRights(size int) []int { // Notice, if in concurrent-safe usage, it returns a copy of slice; // else a pointer to the underlying data. // -// If is negative, then the offset will start from the end of array. -// If is omitted, then the sequence will have everything from start up +// If `end` is negative, then the offset will start from the end of array. +// If `end` is omitted, then the sequence will have everything from start up // until the end of the array. func (a *IntArray) Range(start int, end ...int) []int { a.mu.RLock() @@ -371,7 +378,7 @@ func (a *IntArray) Range(start int, end ...int) []int { } // SubSlice returns a slice of elements from the array as specified -// by the and parameters. +// by the `offset` and `size` parameters. // If in concurrent safe usage, it returns a copy of the slice; else a pointer. // // If offset is non-negative, the sequence will start at that offset in the array. @@ -487,7 +494,7 @@ func (a *IntArray) Contains(value int) bool { return a.Search(value) != -1 } -// Search searches array by , returns the index of , +// Search searches array by `value`, returns the index of `value`, // or returns -1 if not exists. func (a *IntArray) Search(value int) int { a.mu.RLock() @@ -522,7 +529,7 @@ func (a *IntArray) Unique() *IntArray { return a } -// LockFunc locks writing by callback function . +// LockFunc locks writing by callback function `f`. func (a *IntArray) LockFunc(f func(array []int)) *IntArray { a.mu.Lock() defer a.mu.Unlock() @@ -530,7 +537,7 @@ func (a *IntArray) LockFunc(f func(array []int)) *IntArray { return a } -// RLockFunc locks reading by callback function . +// RLockFunc locks reading by callback function `f`. func (a *IntArray) RLockFunc(f func(array []int)) *IntArray { a.mu.RLock() defer a.mu.RUnlock() @@ -538,16 +545,16 @@ func (a *IntArray) RLockFunc(f func(array []int)) *IntArray { return a } -// Merge merges into current array. -// The parameter can be any garray or slice type. +// Merge merges `array` into current array. +// The parameter `array` can be any garray or slice type. // The difference between Merge and Append is Append supports only specified slice type, // but Merge supports more parameter types. func (a *IntArray) Merge(array interface{}) *IntArray { return a.Append(gconv.Ints(array)...) } -// Fill fills an array with num entries of the value , -// keys starting at the parameter. +// Fill fills an array with num entries of the value `value`, +// keys starting at the `startIndex` parameter. func (a *IntArray) Fill(startIndex int, num int, value int) error { a.mu.Lock() defer a.mu.Unlock() @@ -565,7 +572,7 @@ func (a *IntArray) Fill(startIndex int, num int, value int) error { } // Chunk splits an array into multiple arrays, -// the size of each array is determined by . +// the size of each array is determined by `size`. // The last chunk may contain less than size elements. func (a *IntArray) Chunk(size int) [][]int { if size < 1 { @@ -587,9 +594,9 @@ func (a *IntArray) Chunk(size int) [][]int { return n } -// Pad pads array to the specified length with . +// Pad pads array to the specified length with `value`. // If size is positive then the array is padded on the right, or negative on the left. -// If the absolute value of is less than or equal to the length of the array +// If the absolute value of `size` is less than or equal to the length of the array // then no padding takes place. func (a *IntArray) Pad(size int, value int) *IntArray { a.mu.Lock() @@ -624,7 +631,7 @@ func (a *IntArray) Rand() (value int, found bool) { return a.array[grand.Intn(len(a.array))], true } -// Rands randomly returns items from array(no deleting). +// Rands randomly returns `size` items from array(no deleting). func (a *IntArray) Rands(size int) []int { a.mu.RLock() defer a.mu.RUnlock() @@ -658,7 +665,7 @@ func (a *IntArray) Reverse() *IntArray { return a } -// Join joins array elements with a string . +// Join joins array elements with a string `glue`. func (a *IntArray) Join(glue string) string { a.mu.RLock() defer a.mu.RUnlock() @@ -691,8 +698,8 @@ func (a *IntArray) Iterator(f func(k int, v int) bool) { a.IteratorAsc(f) } -// IteratorAsc iterates the array readonly in ascending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorAsc iterates the array readonly in ascending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *IntArray) IteratorAsc(f func(k int, v int) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -703,8 +710,8 @@ func (a *IntArray) IteratorAsc(f func(k int, v int) bool) { } } -// IteratorDesc iterates the array readonly in descending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorDesc iterates the array readonly in descending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *IntArray) IteratorDesc(f func(k int, v int) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -735,7 +742,7 @@ func (a *IntArray) UnmarshalJSON(b []byte) error { } a.mu.Lock() defer a.mu.Unlock() - if err := json.Unmarshal(b, &a.array); err != nil { + if err := json.UnmarshalUseNumber(b, &a.array); err != nil { return err } return nil @@ -747,7 +754,7 @@ func (a *IntArray) UnmarshalValue(value interface{}) error { defer a.mu.Unlock() switch value.(type) { case string, []byte: - return json.Unmarshal(gconv.Bytes(value), &a.array) + return json.UnmarshalUseNumber(gconv.Bytes(value), &a.array) default: a.array = gconv.SliceInt(value) } @@ -768,7 +775,7 @@ func (a *IntArray) FilterEmpty() *IntArray { return a } -// Walk applies a user supplied function to every item of array. +// Walk applies a user supplied function `f` to every item of array. func (a *IntArray) Walk(f func(value int) int) *IntArray { a.mu.Lock() defer a.mu.Unlock() diff --git a/container/garray/garray_normal_str.go b/container/garray/garray_normal_str.go index 477156a8a..f1754e80d 100644 --- a/container/garray/garray_normal_str.go +++ b/container/garray/garray_normal_str.go @@ -30,14 +30,14 @@ type StrArray struct { } // NewStrArray creates and returns an empty array. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewStrArray(safe ...bool) *StrArray { return NewStrArraySize(0, 0, safe...) } // NewStrArraySize create and returns an array with given size and cap. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewStrArraySize(size int, cap int, safe ...bool) *StrArray { return &StrArray{ @@ -46,8 +46,8 @@ func NewStrArraySize(size int, cap int, safe ...bool) *StrArray { } } -// NewStrArrayFrom creates and returns an array with given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewStrArrayFrom creates and returns an array with given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewStrArrayFrom(array []string, safe ...bool) *StrArray { return &StrArray{ @@ -56,8 +56,8 @@ func NewStrArrayFrom(array []string, safe ...bool) *StrArray { } } -// NewStrArrayFromCopy creates and returns an array from a copy of given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewStrArrayFromCopy creates and returns an array from a copy of given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewStrArrayFromCopy(array []string, safe ...bool) *StrArray { newArray := make([]string, len(array)) @@ -68,8 +68,15 @@ func NewStrArrayFromCopy(array []string, safe ...bool) *StrArray { } } +// At returns the value by the specified index. +// If the given `index` is out of range of the array, it returns an empty string. +func (a *StrArray) At(index int) (value string) { + value, _ = a.Get(index) + return +} + // Get returns the value by the specified index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *StrArray) Get(index int) (value string, found bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -90,7 +97,7 @@ func (a *StrArray) Set(index int, value string) error { return nil } -// SetArray sets the underlying slice array with the given . +// SetArray sets the underlying slice array with the given `array`. func (a *StrArray) SetArray(array []string) *StrArray { a.mu.Lock() defer a.mu.Unlock() @@ -98,7 +105,7 @@ func (a *StrArray) SetArray(array []string) *StrArray { return a } -// Replace replaces the array items by given from the beginning of array. +// Replace replaces the array items by given `array` from the beginning of array. func (a *StrArray) Replace(array []string) *StrArray { a.mu.Lock() defer a.mu.Unlock() @@ -123,7 +130,7 @@ func (a *StrArray) Sum() (sum int) { } // Sort sorts the array in increasing order. -// The parameter controls whether sort +// The parameter `reverse` controls whether sort // in increasing order(default) or decreasing order func (a *StrArray) Sort(reverse ...bool) *StrArray { a.mu.Lock() @@ -141,7 +148,7 @@ func (a *StrArray) Sort(reverse ...bool) *StrArray { return a } -// SortFunc sorts the array by custom function . +// SortFunc sorts the array by custom function `less`. func (a *StrArray) SortFunc(less func(v1, v2 string) bool) *StrArray { a.mu.Lock() defer a.mu.Unlock() @@ -151,7 +158,7 @@ func (a *StrArray) SortFunc(less func(v1, v2 string) bool) *StrArray { return a } -// InsertBefore inserts the to the front of . +// InsertBefore inserts the `value` to the front of `index`. func (a *StrArray) InsertBefore(index int, value string) error { a.mu.Lock() defer a.mu.Unlock() @@ -164,7 +171,7 @@ func (a *StrArray) InsertBefore(index int, value string) error { return nil } -// InsertAfter inserts the to the back of . +// InsertAfter inserts the `value` to the back of `index`. func (a *StrArray) InsertAfter(index int, value string) error { a.mu.Lock() defer a.mu.Unlock() @@ -178,7 +185,7 @@ func (a *StrArray) InsertAfter(index int, value string) error { } // Remove removes an item by index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *StrArray) Remove(index int) (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -236,7 +243,7 @@ func (a *StrArray) PushRight(value ...string) *StrArray { } // PopLeft pops and returns an item from the beginning of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *StrArray) PopLeft() (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -249,7 +256,7 @@ func (a *StrArray) PopLeft() (value string, found bool) { } // PopRight pops and returns an item from the end of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *StrArray) PopRight() (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -263,16 +270,16 @@ func (a *StrArray) PopRight() (value string, found bool) { } // PopRand randomly pops and return an item out of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *StrArray) PopRand() (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() return a.doRemoveWithoutLock(grand.Intn(len(a.array))) } -// PopRands randomly pops and returns items out of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRands randomly pops and returns `size` items out of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *StrArray) PopRands(size int) []string { a.mu.Lock() defer a.mu.Unlock() @@ -289,9 +296,9 @@ func (a *StrArray) PopRands(size int) []string { return array } -// PopLefts pops and returns items from the beginning of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopLefts pops and returns `size` items from the beginning of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *StrArray) PopLefts(size int) []string { a.mu.Lock() defer a.mu.Unlock() @@ -308,9 +315,9 @@ func (a *StrArray) PopLefts(size int) []string { return value } -// PopRights pops and returns items from the end of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRights pops and returns `size` items from the end of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *StrArray) PopRights(size int) []string { a.mu.Lock() defer a.mu.Unlock() @@ -332,8 +339,8 @@ func (a *StrArray) PopRights(size int) []string { // Notice, if in concurrent-safe usage, it returns a copy of slice; // else a pointer to the underlying data. // -// If is negative, then the offset will start from the end of array. -// If is omitted, then the sequence will have everything from start up +// If `end` is negative, then the offset will start from the end of array. +// If `end` is omitted, then the sequence will have everything from start up // until the end of the array. func (a *StrArray) Range(start int, end ...int) []string { a.mu.RLock() @@ -359,7 +366,7 @@ func (a *StrArray) Range(start int, end ...int) []string { } // SubSlice returns a slice of elements from the array as specified -// by the and parameters. +// by the `offset` and `size` parameters. // If in concurrent safe usage, it returns a copy of the slice; else a pointer. // // If offset is non-negative, the sequence will start at that offset in the array. @@ -491,7 +498,7 @@ func (a *StrArray) ContainsI(value string) bool { return false } -// Search searches array by , returns the index of , +// Search searches array by `value`, returns the index of `value`, // or returns -1 if not exists. func (a *StrArray) Search(value string) int { a.mu.RLock() @@ -526,7 +533,7 @@ func (a *StrArray) Unique() *StrArray { return a } -// LockFunc locks writing by callback function . +// LockFunc locks writing by callback function `f`. func (a *StrArray) LockFunc(f func(array []string)) *StrArray { a.mu.Lock() defer a.mu.Unlock() @@ -534,7 +541,7 @@ func (a *StrArray) LockFunc(f func(array []string)) *StrArray { return a } -// RLockFunc locks reading by callback function . +// RLockFunc locks reading by callback function `f`. func (a *StrArray) RLockFunc(f func(array []string)) *StrArray { a.mu.RLock() defer a.mu.RUnlock() @@ -542,16 +549,16 @@ func (a *StrArray) RLockFunc(f func(array []string)) *StrArray { return a } -// Merge merges into current array. -// The parameter can be any garray or slice type. +// Merge merges `array` into current array. +// The parameter `array` can be any garray or slice type. // The difference between Merge and Append is Append supports only specified slice type, // but Merge supports more parameter types. func (a *StrArray) Merge(array interface{}) *StrArray { return a.Append(gconv.Strings(array)...) } -// Fill fills an array with num entries of the value , -// keys starting at the parameter. +// Fill fills an array with num entries of the value `value`, +// keys starting at the `startIndex` parameter. func (a *StrArray) Fill(startIndex int, num int, value string) error { a.mu.Lock() defer a.mu.Unlock() @@ -569,7 +576,7 @@ func (a *StrArray) Fill(startIndex int, num int, value string) error { } // Chunk splits an array into multiple arrays, -// the size of each array is determined by . +// the size of each array is determined by `size`. // The last chunk may contain less than size elements. func (a *StrArray) Chunk(size int) [][]string { if size < 1 { @@ -591,9 +598,9 @@ func (a *StrArray) Chunk(size int) [][]string { return n } -// Pad pads array to the specified length with . +// Pad pads array to the specified length with `value`. // If size is positive then the array is padded on the right, or negative on the left. -// If the absolute value of is less than or equal to the length of the array +// If the absolute value of `size` is less than or equal to the length of the array // then no padding takes place. func (a *StrArray) Pad(size int, value string) *StrArray { a.mu.Lock() @@ -628,7 +635,7 @@ func (a *StrArray) Rand() (value string, found bool) { return a.array[grand.Intn(len(a.array))], true } -// Rands randomly returns items from array(no deleting). +// Rands randomly returns `size` items from array(no deleting). func (a *StrArray) Rands(size int) []string { a.mu.RLock() defer a.mu.RUnlock() @@ -662,7 +669,7 @@ func (a *StrArray) Reverse() *StrArray { return a } -// Join joins array elements with a string . +// Join joins array elements with a string `glue`. func (a *StrArray) Join(glue string) string { a.mu.RLock() defer a.mu.RUnlock() @@ -695,8 +702,8 @@ func (a *StrArray) Iterator(f func(k int, v string) bool) { a.IteratorAsc(f) } -// IteratorAsc iterates the array readonly in ascending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorAsc iterates the array readonly in ascending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *StrArray) IteratorAsc(f func(k int, v string) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -707,8 +714,8 @@ func (a *StrArray) IteratorAsc(f func(k int, v string) bool) { } } -// IteratorDesc iterates the array readonly in descending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorDesc iterates the array readonly in descending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *StrArray) IteratorDesc(f func(k int, v string) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -750,7 +757,7 @@ func (a *StrArray) UnmarshalJSON(b []byte) error { } a.mu.Lock() defer a.mu.Unlock() - if err := json.Unmarshal(b, &a.array); err != nil { + if err := json.UnmarshalUseNumber(b, &a.array); err != nil { return err } return nil @@ -762,7 +769,7 @@ func (a *StrArray) UnmarshalValue(value interface{}) error { defer a.mu.Unlock() switch value.(type) { case string, []byte: - return json.Unmarshal(gconv.Bytes(value), &a.array) + return json.UnmarshalUseNumber(gconv.Bytes(value), &a.array) default: a.array = gconv.SliceStr(value) } @@ -783,7 +790,7 @@ func (a *StrArray) FilterEmpty() *StrArray { return a } -// Walk applies a user supplied function to every item of array. +// Walk applies a user supplied function `f` to every item of array. func (a *StrArray) Walk(f func(value string) string) *StrArray { a.mu.Lock() defer a.mu.Unlock() diff --git a/container/garray/garray_sorted_any.go b/container/garray/garray_sorted_any.go index 497ea9e67..cd14d6252 100644 --- a/container/garray/garray_sorted_any.go +++ b/container/garray/garray_sorted_any.go @@ -34,8 +34,8 @@ type SortedArray struct { } // NewSortedArray creates and returns an empty sorted array. -// The parameter is used to specify whether using array in concurrent-safety, which is false in default. -// The parameter used to compare values to sort in array, +// The parameter `safe` is used to specify whether using array in concurrent-safety, which is false in default. +// The parameter `comparator` used to compare values to sort in array, // if it returns value < 0, means v1 < v2; the v1 will be inserted before v2; // if it returns value = 0, means v1 = v2; the v1 will be replaced by v2; // if it returns value > 0, means v1 > v2; the v1 will be inserted after v2; @@ -44,7 +44,7 @@ func NewSortedArray(comparator func(a, b interface{}) int, safe ...bool) *Sorted } // NewSortedArraySize create and returns an sorted array with given size and cap. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedArraySize(cap int, comparator func(a, b interface{}) int, safe ...bool) *SortedArray { return &SortedArray{ @@ -54,8 +54,8 @@ func NewSortedArraySize(cap int, comparator func(a, b interface{}) int, safe ... } } -// NewSortedArrayRange creates and returns a array by a range from to -// with step value . +// NewSortedArrayRange creates and returns a array by a range from `start` to `end` +// with step value `step`. func NewSortedArrayRange(start, end, step int, comparator func(a, b interface{}) int, safe ...bool) *SortedArray { if step == 0 { panic(fmt.Sprintf(`invalid step value: %d`, step)) @@ -69,8 +69,8 @@ func NewSortedArrayRange(start, end, step int, comparator func(a, b interface{}) return NewSortedArrayFrom(slice, comparator, safe...) } -// NewSortedArrayFrom creates and returns an sorted array with given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewSortedArrayFrom creates and returns an sorted array with given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedArrayFrom(array []interface{}, comparator func(a, b interface{}) int, safe ...bool) *SortedArray { a := NewSortedArraySize(0, comparator, safe...) @@ -81,8 +81,8 @@ func NewSortedArrayFrom(array []interface{}, comparator func(a, b interface{}) i return a } -// NewSortedArrayFromCopy creates and returns an sorted array from a copy of given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewSortedArrayFromCopy creates and returns an sorted array from a copy of given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedArrayFromCopy(array []interface{}, comparator func(a, b interface{}) int, safe ...bool) *SortedArray { newArray := make([]interface{}, len(array)) @@ -90,7 +90,14 @@ func NewSortedArrayFromCopy(array []interface{}, comparator func(a, b interface{ return NewSortedArrayFrom(newArray, comparator, safe...) } -// SetArray sets the underlying slice array with the given . +// At returns the value by the specified index. +// If the given `index` is out of range of the array, it returns `nil`. +func (a *SortedArray) At(index int) (value interface{}) { + value, _ = a.Get(index) + return +} + +// SetArray sets the underlying slice array with the given `array`. func (a *SortedArray) SetArray(array []interface{}) *SortedArray { a.mu.Lock() defer a.mu.Unlock() @@ -113,7 +120,7 @@ func (a *SortedArray) SetComparator(comparator func(a, b interface{}) int) { } // Sort sorts the array in increasing order. -// The parameter controls whether sort +// The parameter `reverse` controls whether sort // in increasing order(default) or decreasing order func (a *SortedArray) Sort() *SortedArray { a.mu.Lock() @@ -157,7 +164,7 @@ func (a *SortedArray) Append(values ...interface{}) *SortedArray { } // Get returns the value by the specified index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *SortedArray) Get(index int) (value interface{}, found bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -168,7 +175,7 @@ func (a *SortedArray) Get(index int) (value interface{}, found bool) { } // Remove removes an item by index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *SortedArray) Remove(index int) (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -209,7 +216,7 @@ func (a *SortedArray) RemoveValue(value interface{}) bool { } // PopLeft pops and returns an item from the beginning of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedArray) PopLeft() (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -222,7 +229,7 @@ func (a *SortedArray) PopLeft() (value interface{}, found bool) { } // PopRight pops and returns an item from the end of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedArray) PopRight() (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -236,14 +243,14 @@ func (a *SortedArray) PopRight() (value interface{}, found bool) { } // PopRand randomly pops and return an item out of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedArray) PopRand() (value interface{}, found bool) { a.mu.Lock() defer a.mu.Unlock() return a.doRemoveWithoutLock(grand.Intn(len(a.array))) } -// PopRands randomly pops and returns items out of array. +// PopRands randomly pops and returns `size` items out of array. func (a *SortedArray) PopRands(size int) []interface{} { a.mu.Lock() defer a.mu.Unlock() @@ -260,7 +267,7 @@ func (a *SortedArray) PopRands(size int) []interface{} { return array } -// PopLefts pops and returns items from the beginning of array. +// PopLefts pops and returns `size` items from the beginning of array. func (a *SortedArray) PopLefts(size int) []interface{} { a.mu.Lock() defer a.mu.Unlock() @@ -277,7 +284,7 @@ func (a *SortedArray) PopLefts(size int) []interface{} { return value } -// PopRights pops and returns items from the end of array. +// PopRights pops and returns `size` items from the end of array. func (a *SortedArray) PopRights(size int) []interface{} { a.mu.Lock() defer a.mu.Unlock() @@ -299,8 +306,8 @@ func (a *SortedArray) PopRights(size int) []interface{} { // Notice, if in concurrent-safe usage, it returns a copy of slice; // else a pointer to the underlying data. // -// If is negative, then the offset will start from the end of array. -// If is omitted, then the sequence will have everything from start up +// If `end` is negative, then the offset will start from the end of array. +// If `end` is omitted, then the sequence will have everything from start up // until the end of the array. func (a *SortedArray) Range(start int, end ...int) []interface{} { a.mu.RLock() @@ -326,7 +333,7 @@ func (a *SortedArray) Range(start int, end ...int) []interface{} { } // SubSlice returns a slice of elements from the array as specified -// by the and parameters. +// by the `offset` and `size` parameters. // If in concurrent safe usage, it returns a copy of the slice; else a pointer. // // If offset is non-negative, the sequence will start at that offset in the array. @@ -419,7 +426,7 @@ func (a *SortedArray) Contains(value interface{}) bool { return a.Search(value) != -1 } -// Search searches array by , returns the index of , +// Search searches array by `value`, returns the index of `value`, // or returns -1 if not exists. func (a *SortedArray) Search(value interface{}) (index int) { if i, r := a.binSearch(value, true); r == 0 { @@ -430,9 +437,9 @@ func (a *SortedArray) Search(value interface{}) (index int) { // Binary search. // It returns the last compared index and the result. -// If equals to 0, it means the value at is equals to . -// If lesser than 0, it means the value at is lesser than . -// If greater than 0, it means the value at is greater than . +// If `result` equals to 0, it means the value at `index` is equals to `value`. +// If `result` lesser than 0, it means the value at `index` is lesser than `value`. +// If `result` greater than 0, it means the value at `index` is greater than `value`. func (a *SortedArray) binSearch(value interface{}, lock bool) (index int, result int) { if lock { a.mu.RLock() @@ -512,7 +519,7 @@ func (a *SortedArray) Clear() *SortedArray { return a } -// LockFunc locks writing by callback function . +// LockFunc locks writing by callback function `f`. func (a *SortedArray) LockFunc(f func(array []interface{})) *SortedArray { a.mu.Lock() defer a.mu.Unlock() @@ -526,7 +533,7 @@ func (a *SortedArray) LockFunc(f func(array []interface{})) *SortedArray { return a } -// RLockFunc locks reading by callback function . +// RLockFunc locks reading by callback function `f`. func (a *SortedArray) RLockFunc(f func(array []interface{})) *SortedArray { a.mu.RLock() defer a.mu.RUnlock() @@ -534,8 +541,8 @@ func (a *SortedArray) RLockFunc(f func(array []interface{})) *SortedArray { return a } -// Merge merges into current array. -// The parameter can be any garray or slice type. +// Merge merges `array` into current array. +// The parameter `array` can be any garray or slice type. // The difference between Merge and Append is Append supports only specified slice type, // but Merge supports more parameter types. func (a *SortedArray) Merge(array interface{}) *SortedArray { @@ -543,7 +550,7 @@ func (a *SortedArray) Merge(array interface{}) *SortedArray { } // Chunk splits an array into multiple arrays, -// the size of each array is determined by . +// the size of each array is determined by `size`. // The last chunk may contain less than size elements. func (a *SortedArray) Chunk(size int) [][]interface{} { if size < 1 { @@ -575,7 +582,7 @@ func (a *SortedArray) Rand() (value interface{}, found bool) { return a.array[grand.Intn(len(a.array))], true } -// Rands randomly returns items from array(no deleting). +// Rands randomly returns `size` items from array(no deleting). func (a *SortedArray) Rands(size int) []interface{} { a.mu.RLock() defer a.mu.RUnlock() @@ -589,7 +596,7 @@ func (a *SortedArray) Rands(size int) []interface{} { return array } -// Join joins array elements with a string . +// Join joins array elements with a string `glue`. func (a *SortedArray) Join(glue string) string { a.mu.RLock() defer a.mu.RUnlock() @@ -622,8 +629,8 @@ func (a *SortedArray) Iterator(f func(k int, v interface{}) bool) { a.IteratorAsc(f) } -// IteratorAsc iterates the array readonly in ascending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorAsc iterates the array readonly in ascending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *SortedArray) IteratorAsc(f func(k int, v interface{}) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -634,8 +641,8 @@ func (a *SortedArray) IteratorAsc(f func(k int, v interface{}) bool) { } } -// IteratorDesc iterates the array readonly in descending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorDesc iterates the array readonly in descending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *SortedArray) IteratorDesc(f func(k int, v interface{}) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -685,7 +692,7 @@ func (a *SortedArray) UnmarshalJSON(b []byte) error { } a.mu.Lock() defer a.mu.Unlock() - if err := json.Unmarshal(b, &a.array); err != nil { + if err := json.UnmarshalUseNumber(b, &a.array); err != nil { return err } if a.comparator != nil && a.array != nil { @@ -706,7 +713,7 @@ func (a *SortedArray) UnmarshalValue(value interface{}) (err error) { defer a.mu.Unlock() switch value.(type) { case string, []byte: - err = json.Unmarshal(gconv.Bytes(value), &a.array) + err = json.UnmarshalUseNumber(gconv.Bytes(value), &a.array) default: a.array = gconv.SliceAny(value) } @@ -761,7 +768,7 @@ func (a *SortedArray) FilterEmpty() *SortedArray { return a } -// Walk applies a user supplied function to every item of array. +// Walk applies a user supplied function `f` to every item of array. func (a *SortedArray) Walk(f func(value interface{}) interface{}) *SortedArray { a.mu.Lock() defer a.mu.Unlock() diff --git a/container/garray/garray_sorted_int.go b/container/garray/garray_sorted_int.go index ab926a63a..f250d989c 100644 --- a/container/garray/garray_sorted_int.go +++ b/container/garray/garray_sorted_int.go @@ -31,14 +31,14 @@ type SortedIntArray struct { } // NewSortedIntArray creates and returns an empty sorted array. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedIntArray(safe ...bool) *SortedIntArray { return NewSortedIntArraySize(0, safe...) } // NewSortedIntArrayComparator creates and returns an empty sorted array with specified comparator. -// The parameter is used to specify whether using array in concurrent-safety which is false in default. +// The parameter `safe` is used to specify whether using array in concurrent-safety which is false in default. func NewSortedIntArrayComparator(comparator func(a, b int) int, safe ...bool) *SortedIntArray { array := NewSortedIntArray(safe...) array.comparator = comparator @@ -46,7 +46,7 @@ func NewSortedIntArrayComparator(comparator func(a, b int) int, safe ...bool) *S } // NewSortedIntArraySize create and returns an sorted array with given size and cap. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedIntArraySize(cap int, safe ...bool) *SortedIntArray { return &SortedIntArray{ @@ -56,8 +56,8 @@ func NewSortedIntArraySize(cap int, safe ...bool) *SortedIntArray { } } -// NewSortedIntArrayRange creates and returns a array by a range from to -// with step value . +// NewSortedIntArrayRange creates and returns a array by a range from `start` to `end` +// with step value `step`. func NewSortedIntArrayRange(start, end, step int, safe ...bool) *SortedIntArray { if step == 0 { panic(fmt.Sprintf(`invalid step value: %d`, step)) @@ -71,8 +71,8 @@ func NewSortedIntArrayRange(start, end, step int, safe ...bool) *SortedIntArray return NewSortedIntArrayFrom(slice, safe...) } -// NewIntArrayFrom creates and returns an sorted array with given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewSortedIntArrayFrom creates and returns an sorted array with given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedIntArrayFrom(array []int, safe ...bool) *SortedIntArray { a := NewSortedIntArraySize(0, safe...) @@ -81,8 +81,8 @@ func NewSortedIntArrayFrom(array []int, safe ...bool) *SortedIntArray { return a } -// NewSortedIntArrayFromCopy creates and returns an sorted array from a copy of given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewSortedIntArrayFromCopy creates and returns an sorted array from a copy of given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedIntArrayFromCopy(array []int, safe ...bool) *SortedIntArray { newArray := make([]int, len(array)) @@ -90,7 +90,14 @@ func NewSortedIntArrayFromCopy(array []int, safe ...bool) *SortedIntArray { return NewSortedIntArrayFrom(newArray, safe...) } -// SetArray sets the underlying slice array with the given . +// At returns the value by the specified index. +// If the given `index` is out of range of the array, it returns `0`. +func (a *SortedIntArray) At(index int) (value int) { + value, _ = a.Get(index) + return +} + +// SetArray sets the underlying slice array with the given `array`. func (a *SortedIntArray) SetArray(array []int) *SortedIntArray { a.mu.Lock() defer a.mu.Unlock() @@ -100,7 +107,7 @@ func (a *SortedIntArray) SetArray(array []int) *SortedIntArray { } // Sort sorts the array in increasing order. -// The parameter controls whether sort +// The parameter `reverse` controls whether sort // in increasing order(default) or decreasing order. func (a *SortedIntArray) Sort() *SortedIntArray { a.mu.Lock() @@ -142,7 +149,7 @@ func (a *SortedIntArray) Append(values ...int) *SortedIntArray { } // Get returns the value by the specified index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *SortedIntArray) Get(index int) (value int, found bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -153,7 +160,7 @@ func (a *SortedIntArray) Get(index int) (value int, found bool) { } // Remove removes an item by index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *SortedIntArray) Remove(index int) (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -194,7 +201,7 @@ func (a *SortedIntArray) RemoveValue(value int) bool { } // PopLeft pops and returns an item from the beginning of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedIntArray) PopLeft() (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -207,7 +214,7 @@ func (a *SortedIntArray) PopLeft() (value int, found bool) { } // PopRight pops and returns an item from the end of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedIntArray) PopRight() (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -221,16 +228,16 @@ func (a *SortedIntArray) PopRight() (value int, found bool) { } // PopRand randomly pops and return an item out of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedIntArray) PopRand() (value int, found bool) { a.mu.Lock() defer a.mu.Unlock() return a.doRemoveWithoutLock(grand.Intn(len(a.array))) } -// PopRands randomly pops and returns items out of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRands randomly pops and returns `size` items out of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *SortedIntArray) PopRands(size int) []int { a.mu.Lock() defer a.mu.Unlock() @@ -247,9 +254,9 @@ func (a *SortedIntArray) PopRands(size int) []int { return array } -// PopLefts pops and returns items from the beginning of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopLefts pops and returns `size` items from the beginning of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *SortedIntArray) PopLefts(size int) []int { a.mu.Lock() defer a.mu.Unlock() @@ -266,9 +273,9 @@ func (a *SortedIntArray) PopLefts(size int) []int { return value } -// PopRights pops and returns items from the end of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRights pops and returns `size` items from the end of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *SortedIntArray) PopRights(size int) []int { a.mu.Lock() defer a.mu.Unlock() @@ -290,8 +297,8 @@ func (a *SortedIntArray) PopRights(size int) []int { // Notice, if in concurrent-safe usage, it returns a copy of slice; // else a pointer to the underlying data. // -// If is negative, then the offset will start from the end of array. -// If is omitted, then the sequence will have everything from start up +// If `end` is negative, then the offset will start from the end of array. +// If `end` is omitted, then the sequence will have everything from start up // until the end of the array. func (a *SortedIntArray) Range(start int, end ...int) []int { a.mu.RLock() @@ -317,7 +324,7 @@ func (a *SortedIntArray) Range(start int, end ...int) []int { } // SubSlice returns a slice of elements from the array as specified -// by the and parameters. +// by the `offset` and `size` parameters. // If in concurrent safe usage, it returns a copy of the slice; else a pointer. // // If offset is non-negative, the sequence will start at that offset in the array. @@ -416,7 +423,7 @@ func (a *SortedIntArray) Contains(value int) bool { return a.Search(value) != -1 } -// Search searches array by , returns the index of , +// Search searches array by `value`, returns the index of `value`, // or returns -1 if not exists. func (a *SortedIntArray) Search(value int) (index int) { if i, r := a.binSearch(value, true); r == 0 { @@ -427,9 +434,9 @@ func (a *SortedIntArray) Search(value int) (index int) { // Binary search. // It returns the last compared index and the result. -// If equals to 0, it means the value at is equals to . -// If lesser than 0, it means the value at is lesser than . -// If greater than 0, it means the value at is greater than . +// If `result` equals to 0, it means the value at `index` is equals to `value`. +// If `result` lesser than 0, it means the value at `index` is lesser than `value`. +// If `result` greater than 0, it means the value at `index` is greater than `value`. func (a *SortedIntArray) binSearch(value int, lock bool) (index int, result int) { if lock { a.mu.RLock() @@ -509,7 +516,7 @@ func (a *SortedIntArray) Clear() *SortedIntArray { return a } -// LockFunc locks writing by callback function . +// LockFunc locks writing by callback function `f`. func (a *SortedIntArray) LockFunc(f func(array []int)) *SortedIntArray { a.mu.Lock() defer a.mu.Unlock() @@ -517,7 +524,7 @@ func (a *SortedIntArray) LockFunc(f func(array []int)) *SortedIntArray { return a } -// RLockFunc locks reading by callback function . +// RLockFunc locks reading by callback function `f`. func (a *SortedIntArray) RLockFunc(f func(array []int)) *SortedIntArray { a.mu.RLock() defer a.mu.RUnlock() @@ -525,8 +532,8 @@ func (a *SortedIntArray) RLockFunc(f func(array []int)) *SortedIntArray { return a } -// Merge merges into current array. -// The parameter can be any garray or slice type. +// Merge merges `array` into current array. +// The parameter `array` can be any garray or slice type. // The difference between Merge and Append is Append supports only specified slice type, // but Merge supports more parameter types. func (a *SortedIntArray) Merge(array interface{}) *SortedIntArray { @@ -534,7 +541,7 @@ func (a *SortedIntArray) Merge(array interface{}) *SortedIntArray { } // Chunk splits an array into multiple arrays, -// the size of each array is determined by . +// the size of each array is determined by `size`. // The last chunk may contain less than size elements. func (a *SortedIntArray) Chunk(size int) [][]int { if size < 1 { @@ -566,7 +573,7 @@ func (a *SortedIntArray) Rand() (value int, found bool) { return a.array[grand.Intn(len(a.array))], true } -// Rands randomly returns items from array(no deleting). +// Rands randomly returns `size` items from array(no deleting). func (a *SortedIntArray) Rands(size int) []int { a.mu.RLock() defer a.mu.RUnlock() @@ -580,7 +587,7 @@ func (a *SortedIntArray) Rands(size int) []int { return array } -// Join joins array elements with a string . +// Join joins array elements with a string `glue`. func (a *SortedIntArray) Join(glue string) string { a.mu.RLock() defer a.mu.RUnlock() @@ -613,8 +620,8 @@ func (a *SortedIntArray) Iterator(f func(k int, v int) bool) { a.IteratorAsc(f) } -// IteratorAsc iterates the array readonly in ascending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorAsc iterates the array readonly in ascending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *SortedIntArray) IteratorAsc(f func(k int, v int) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -625,8 +632,8 @@ func (a *SortedIntArray) IteratorAsc(f func(k int, v int) bool) { } } -// IteratorDesc iterates the array readonly in descending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorDesc iterates the array readonly in descending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *SortedIntArray) IteratorDesc(f func(k int, v int) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -658,7 +665,7 @@ func (a *SortedIntArray) UnmarshalJSON(b []byte) error { } a.mu.Lock() defer a.mu.Unlock() - if err := json.Unmarshal(b, &a.array); err != nil { + if err := json.UnmarshalUseNumber(b, &a.array); err != nil { return err } if a.array != nil { @@ -676,7 +683,7 @@ func (a *SortedIntArray) UnmarshalValue(value interface{}) (err error) { defer a.mu.Unlock() switch value.(type) { case string, []byte: - err = json.Unmarshal(gconv.Bytes(value), &a.array) + err = json.UnmarshalUseNumber(gconv.Bytes(value), &a.array) default: a.array = gconv.SliceInt(value) } @@ -707,7 +714,7 @@ func (a *SortedIntArray) FilterEmpty() *SortedIntArray { return a } -// Walk applies a user supplied function to every item of array. +// Walk applies a user supplied function `f` to every item of array. func (a *SortedIntArray) Walk(f func(value int) int) *SortedIntArray { a.mu.Lock() defer a.mu.Unlock() diff --git a/container/garray/garray_sorted_str.go b/container/garray/garray_sorted_str.go index 5450f5252..896030e65 100644 --- a/container/garray/garray_sorted_str.go +++ b/container/garray/garray_sorted_str.go @@ -32,14 +32,14 @@ type SortedStrArray struct { } // NewSortedStrArray creates and returns an empty sorted array. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedStrArray(safe ...bool) *SortedStrArray { return NewSortedStrArraySize(0, safe...) } // NewSortedStrArrayComparator creates and returns an empty sorted array with specified comparator. -// The parameter is used to specify whether using array in concurrent-safety which is false in default. +// The parameter `safe` is used to specify whether using array in concurrent-safety which is false in default. func NewSortedStrArrayComparator(comparator func(a, b string) int, safe ...bool) *SortedStrArray { array := NewSortedStrArray(safe...) array.comparator = comparator @@ -47,7 +47,7 @@ func NewSortedStrArrayComparator(comparator func(a, b string) int, safe ...bool) } // NewSortedStrArraySize create and returns an sorted array with given size and cap. -// The parameter is used to specify whether using array in concurrent-safety, +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedStrArraySize(cap int, safe ...bool) *SortedStrArray { return &SortedStrArray{ @@ -57,8 +57,8 @@ func NewSortedStrArraySize(cap int, safe ...bool) *SortedStrArray { } } -// NewSortedStrArrayFrom creates and returns an sorted array with given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewSortedStrArrayFrom creates and returns an sorted array with given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedStrArrayFrom(array []string, safe ...bool) *SortedStrArray { a := NewSortedStrArraySize(0, safe...) @@ -67,8 +67,8 @@ func NewSortedStrArrayFrom(array []string, safe ...bool) *SortedStrArray { return a } -// NewSortedStrArrayFromCopy creates and returns an sorted array from a copy of given slice . -// The parameter is used to specify whether using array in concurrent-safety, +// NewSortedStrArrayFromCopy creates and returns an sorted array from a copy of given slice `array`. +// The parameter `safe` is used to specify whether using array in concurrent-safety, // which is false in default. func NewSortedStrArrayFromCopy(array []string, safe ...bool) *SortedStrArray { newArray := make([]string, len(array)) @@ -76,7 +76,7 @@ func NewSortedStrArrayFromCopy(array []string, safe ...bool) *SortedStrArray { return NewSortedStrArrayFrom(newArray, safe...) } -// SetArray sets the underlying slice array with the given . +// SetArray sets the underlying slice array with the given `array`. func (a *SortedStrArray) SetArray(array []string) *SortedStrArray { a.mu.Lock() defer a.mu.Unlock() @@ -85,8 +85,15 @@ func (a *SortedStrArray) SetArray(array []string) *SortedStrArray { return a } +// At returns the value by the specified index. +// If the given `index` is out of range of the array, it returns an empty string. +func (a *SortedStrArray) At(index int) (value string) { + value, _ = a.Get(index) + return +} + // Sort sorts the array in increasing order. -// The parameter controls whether sort +// The parameter `reverse` controls whether sort // in increasing order(default) or decreasing order. func (a *SortedStrArray) Sort() *SortedStrArray { a.mu.Lock() @@ -128,7 +135,7 @@ func (a *SortedStrArray) Append(values ...string) *SortedStrArray { } // Get returns the value by the specified index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *SortedStrArray) Get(index int) (value string, found bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -139,7 +146,7 @@ func (a *SortedStrArray) Get(index int) (value string, found bool) { } // Remove removes an item by index. -// If the given is out of range of the array, the is false. +// If the given `index` is out of range of the array, the `found` is false. func (a *SortedStrArray) Remove(index int) (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -180,7 +187,7 @@ func (a *SortedStrArray) RemoveValue(value string) bool { } // PopLeft pops and returns an item from the beginning of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedStrArray) PopLeft() (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -193,7 +200,7 @@ func (a *SortedStrArray) PopLeft() (value string, found bool) { } // PopRight pops and returns an item from the end of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedStrArray) PopRight() (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() @@ -207,16 +214,16 @@ func (a *SortedStrArray) PopRight() (value string, found bool) { } // PopRand randomly pops and return an item out of array. -// Note that if the array is empty, the is false. +// Note that if the array is empty, the `found` is false. func (a *SortedStrArray) PopRand() (value string, found bool) { a.mu.Lock() defer a.mu.Unlock() return a.doRemoveWithoutLock(grand.Intn(len(a.array))) } -// PopRands randomly pops and returns items out of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRands randomly pops and returns `size` items out of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *SortedStrArray) PopRands(size int) []string { a.mu.Lock() defer a.mu.Unlock() @@ -233,9 +240,9 @@ func (a *SortedStrArray) PopRands(size int) []string { return array } -// PopLefts pops and returns items from the beginning of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopLefts pops and returns `size` items from the beginning of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *SortedStrArray) PopLefts(size int) []string { a.mu.Lock() defer a.mu.Unlock() @@ -252,9 +259,9 @@ func (a *SortedStrArray) PopLefts(size int) []string { return value } -// PopRights pops and returns items from the end of array. -// If the given is greater than size of the array, it returns all elements of the array. -// Note that if given <= 0 or the array is empty, it returns nil. +// PopRights pops and returns `size` items from the end of array. +// If the given `size` is greater than size of the array, it returns all elements of the array. +// Note that if given `size` <= 0 or the array is empty, it returns nil. func (a *SortedStrArray) PopRights(size int) []string { a.mu.Lock() defer a.mu.Unlock() @@ -276,8 +283,8 @@ func (a *SortedStrArray) PopRights(size int) []string { // Notice, if in concurrent-safe usage, it returns a copy of slice; // else a pointer to the underlying data. // -// If is negative, then the offset will start from the end of array. -// If is omitted, then the sequence will have everything from start up +// If `end` is negative, then the offset will start from the end of array. +// If `end` is omitted, then the sequence will have everything from start up // until the end of the array. func (a *SortedStrArray) Range(start int, end ...int) []string { a.mu.RLock() @@ -303,7 +310,7 @@ func (a *SortedStrArray) Range(start int, end ...int) []string { } // SubSlice returns a slice of elements from the array as specified -// by the and parameters. +// by the `offset` and `size` parameters. // If in concurrent safe usage, it returns a copy of the slice; else a pointer. // // If offset is non-negative, the sequence will start at that offset in the array. @@ -418,7 +425,7 @@ func (a *SortedStrArray) ContainsI(value string) bool { return false } -// Search searches array by , returns the index of , +// Search searches array by `value`, returns the index of `value`, // or returns -1 if not exists. func (a *SortedStrArray) Search(value string) (index int) { if i, r := a.binSearch(value, true); r == 0 { @@ -429,9 +436,9 @@ func (a *SortedStrArray) Search(value string) (index int) { // Binary search. // It returns the last compared index and the result. -// If equals to 0, it means the value at is equals to . -// If lesser than 0, it means the value at is lesser than . -// If greater than 0, it means the value at is greater than . +// If `result` equals to 0, it means the value at `index` is equals to `value`. +// If `result` lesser than 0, it means the value at `index` is lesser than `value`. +// If `result` greater than 0, it means the value at `index` is greater than `value`. func (a *SortedStrArray) binSearch(value string, lock bool) (index int, result int) { if lock { a.mu.RLock() @@ -511,7 +518,7 @@ func (a *SortedStrArray) Clear() *SortedStrArray { return a } -// LockFunc locks writing by callback function . +// LockFunc locks writing by callback function `f`. func (a *SortedStrArray) LockFunc(f func(array []string)) *SortedStrArray { a.mu.Lock() defer a.mu.Unlock() @@ -519,7 +526,7 @@ func (a *SortedStrArray) LockFunc(f func(array []string)) *SortedStrArray { return a } -// RLockFunc locks reading by callback function . +// RLockFunc locks reading by callback function `f`. func (a *SortedStrArray) RLockFunc(f func(array []string)) *SortedStrArray { a.mu.RLock() defer a.mu.RUnlock() @@ -527,8 +534,8 @@ func (a *SortedStrArray) RLockFunc(f func(array []string)) *SortedStrArray { return a } -// Merge merges into current array. -// The parameter can be any garray or slice type. +// Merge merges `array` into current array. +// The parameter `array` can be any garray or slice type. // The difference between Merge and Append is Append supports only specified slice type, // but Merge supports more parameter types. func (a *SortedStrArray) Merge(array interface{}) *SortedStrArray { @@ -536,7 +543,7 @@ func (a *SortedStrArray) Merge(array interface{}) *SortedStrArray { } // Chunk splits an array into multiple arrays, -// the size of each array is determined by . +// the size of each array is determined by `size`. // The last chunk may contain less than size elements. func (a *SortedStrArray) Chunk(size int) [][]string { if size < 1 { @@ -568,7 +575,7 @@ func (a *SortedStrArray) Rand() (value string, found bool) { return a.array[grand.Intn(len(a.array))], true } -// Rands randomly returns items from array(no deleting). +// Rands randomly returns `size` items from array(no deleting). func (a *SortedStrArray) Rands(size int) []string { a.mu.RLock() defer a.mu.RUnlock() @@ -582,7 +589,7 @@ func (a *SortedStrArray) Rands(size int) []string { return array } -// Join joins array elements with a string . +// Join joins array elements with a string `glue`. func (a *SortedStrArray) Join(glue string) string { a.mu.RLock() defer a.mu.RUnlock() @@ -615,8 +622,8 @@ func (a *SortedStrArray) Iterator(f func(k int, v string) bool) { a.IteratorAsc(f) } -// IteratorAsc iterates the array readonly in ascending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorAsc iterates the array readonly in ascending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *SortedStrArray) IteratorAsc(f func(k int, v string) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -627,8 +634,8 @@ func (a *SortedStrArray) IteratorAsc(f func(k int, v string) bool) { } } -// IteratorDesc iterates the array readonly in descending order with given callback function . -// If returns true, then it continues iterating; or false to stop. +// IteratorDesc iterates the array readonly in descending order with given callback function `f`. +// If `f` returns true, then it continues iterating; or false to stop. func (a *SortedStrArray) IteratorDesc(f func(k int, v string) bool) { a.mu.RLock() defer a.mu.RUnlock() @@ -671,7 +678,7 @@ func (a *SortedStrArray) UnmarshalJSON(b []byte) error { } a.mu.Lock() defer a.mu.Unlock() - if err := json.Unmarshal(b, &a.array); err != nil { + if err := json.UnmarshalUseNumber(b, &a.array); err != nil { return err } if a.array != nil { @@ -689,7 +696,7 @@ func (a *SortedStrArray) UnmarshalValue(value interface{}) (err error) { defer a.mu.Unlock() switch value.(type) { case string, []byte: - err = json.Unmarshal(gconv.Bytes(value), &a.array) + err = json.UnmarshalUseNumber(gconv.Bytes(value), &a.array) default: a.array = gconv.SliceStr(value) } @@ -720,7 +727,7 @@ func (a *SortedStrArray) FilterEmpty() *SortedStrArray { return a } -// Walk applies a user supplied function to every item of array. +// Walk applies a user supplied function `f` to every item of array. func (a *SortedStrArray) Walk(f func(value string) string) *SortedStrArray { a.mu.Lock() defer a.mu.Unlock() diff --git a/container/garray/garray_z_example_any_test.go b/container/garray/garray_z_example_any_test.go index f79fe1adc..fccf99716 100644 --- a/container/garray/garray_z_example_any_test.go +++ b/container/garray/garray_z_example_any_test.go @@ -75,14 +75,14 @@ func ExampleNew() { func ExampleArray_Iterator() { array := garray.NewArrayFrom(g.Slice{"a", "b", "c"}) // Iterator is alias of IteratorAsc, which iterates the array readonly in ascending order - // with given callback function . - // If returns true, then it continues iterating; or false to stop. + // with given callback function `f`. + // If `f` returns true, then it continues iterating; or false to stop. array.Iterator(func(k int, v interface{}) bool { fmt.Println(k, v) return true }) - // IteratorDesc iterates the array readonly in descending order with given callback function . - // If returns true, then it continues iterating; or false to stop. + // IteratorDesc iterates the array readonly in descending order with given callback function `f`. + // If `f` returns true, then it continues iterating; or false to stop. array.IteratorDesc(func(k int, v interface{}) bool { fmt.Println(k, v) return true @@ -150,7 +150,7 @@ func ExampleArray_Chunk() { array := garray.NewFrom(g.Slice{1, 2, 3, 4, 5, 6, 7, 8, 9}) // Chunk splits an array into multiple arrays, - // the size of each array is determined by . + // the size of each array is determined by `size`. // The last chunk may contain less than size elements. fmt.Println(array.Chunk(2)) diff --git a/container/garray/garray_z_unit_normal_any_array_test.go b/container/garray/garray_z_unit_normal_any_array_test.go index b74fc7656..004e9bfd3 100644 --- a/container/garray/garray_z_unit_normal_any_array_test.go +++ b/container/garray/garray_z_unit_normal_any_array_test.go @@ -570,12 +570,12 @@ func TestArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.New() - err2 = json.Unmarshal(b2, &a2) + err2 = json.UnmarshalUseNumber(b2, &a2) t.Assert(err2, nil) t.Assert(a2.Slice(), s1) var a3 garray.Array - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -589,12 +589,12 @@ func TestArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.New() - err2 = json.Unmarshal(b2, &a2) + err2 = json.UnmarshalUseNumber(b2, &a2) t.Assert(err2, nil) t.Assert(a2.Slice(), s1) var a3 garray.Array - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -612,7 +612,7 @@ func TestArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, data["Scores"]) @@ -631,7 +631,7 @@ func TestArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, data["Scores"]) diff --git a/container/garray/garray_z_unit_normal_int_array_test.go b/container/garray/garray_z_unit_normal_int_array_test.go index 3d6b0b27b..ef1e763ef 100644 --- a/container/garray/garray_z_unit_normal_int_array_test.go +++ b/container/garray/garray_z_unit_normal_int_array_test.go @@ -615,11 +615,11 @@ func TestIntArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewIntArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s1) var a3 garray.IntArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -633,11 +633,11 @@ func TestIntArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewIntArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s1) var a3 garray.IntArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -655,7 +655,7 @@ func TestIntArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, data["Scores"]) @@ -674,7 +674,7 @@ func TestIntArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, data["Scores"]) diff --git a/container/garray/garray_z_unit_normal_str_array_test.go b/container/garray/garray_z_unit_normal_str_array_test.go index f4a02744c..be165863b 100644 --- a/container/garray/garray_z_unit_normal_str_array_test.go +++ b/container/garray/garray_z_unit_normal_str_array_test.go @@ -614,11 +614,11 @@ func TestStrArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewStrArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s1) var a3 garray.StrArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -632,11 +632,11 @@ func TestStrArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewStrArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s1) var a3 garray.StrArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -654,7 +654,7 @@ func TestStrArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, data["Scores"]) @@ -673,7 +673,7 @@ func TestStrArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, data["Scores"]) diff --git a/container/garray/garray_z_unit_sorted_any_array_test.go b/container/garray/garray_z_unit_sorted_any_array_test.go index f3ce498ea..baecec1b5 100644 --- a/container/garray/garray_z_unit_sorted_any_array_test.go +++ b/container/garray/garray_z_unit_sorted_any_array_test.go @@ -656,11 +656,11 @@ func TestSortedArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewSortedArray(gutil.ComparatorString) - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s2) var a3 garray.SortedArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) t.Assert(a3.Interfaces(), s1) @@ -676,11 +676,11 @@ func TestSortedArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewSortedArray(gutil.ComparatorString) - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s2) var a3 garray.SortedArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) t.Assert(a3.Interfaces(), s1) @@ -699,7 +699,7 @@ func TestSortedArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.AssertNE(user.Scores, nil) @@ -735,7 +735,7 @@ func TestSortedArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.AssertNE(user.Scores, nil) diff --git a/container/garray/garray_z_unit_sorted_int_array_test.go b/container/garray/garray_z_unit_sorted_int_array_test.go index c4e2e04b7..56b873f47 100644 --- a/container/garray/garray_z_unit_sorted_int_array_test.go +++ b/container/garray/garray_z_unit_sorted_int_array_test.go @@ -557,11 +557,11 @@ func TestSortedIntArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewSortedIntArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s2) var a3 garray.SortedIntArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -576,11 +576,11 @@ func TestSortedIntArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewSortedIntArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s2) var a3 garray.SortedIntArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) }) @@ -598,7 +598,7 @@ func TestSortedIntArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, []int{98, 99, 100}) @@ -617,7 +617,7 @@ func TestSortedIntArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, []int{98, 99, 100}) diff --git a/container/garray/garray_z_unit_sorted_str_array_test.go b/container/garray/garray_z_unit_sorted_str_array_test.go index 5be967e54..bab5c539e 100644 --- a/container/garray/garray_z_unit_sorted_str_array_test.go +++ b/container/garray/garray_z_unit_sorted_str_array_test.go @@ -577,12 +577,12 @@ func TestSortedStrArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewSortedStrArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s2) t.Assert(a2.Interfaces(), s2) var a3 garray.SortedStrArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) t.Assert(a3.Interfaces(), s1) @@ -598,12 +598,12 @@ func TestSortedStrArray_Json(t *testing.T) { t.Assert(err1, err2) a2 := garray.NewSortedStrArray() - err1 = json.Unmarshal(b2, &a2) + err1 = json.UnmarshalUseNumber(b2, &a2) t.Assert(a2.Slice(), s2) t.Assert(a2.Interfaces(), s2) var a3 garray.SortedStrArray - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Slice(), s1) t.Assert(a3.Interfaces(), s1) @@ -622,7 +622,7 @@ func TestSortedStrArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, []string{"A", "A", "A+"}) @@ -641,7 +641,7 @@ func TestSortedStrArray_Json(t *testing.T) { t.Assert(err, nil) user := new(User) - err = json.Unmarshal(b, user) + err = json.UnmarshalUseNumber(b, user) t.Assert(err, nil) t.Assert(user.Name, data["Name"]) t.Assert(user.Scores, []string{"A", "A", "A+"}) diff --git a/container/glist/glist.go b/container/glist/glist.go index 1a3fd22bb..94319e50e 100644 --- a/container/glist/glist.go +++ b/container/glist/glist.go @@ -519,7 +519,7 @@ func (l *List) UnmarshalJSON(b []byte) error { l.list = list.New() } var array []interface{} - if err := json.Unmarshal(b, &array); err != nil { + if err := json.UnmarshalUseNumber(b, &array); err != nil { return err } l.PushBacks(array) @@ -536,7 +536,7 @@ func (l *List) UnmarshalValue(value interface{}) (err error) { var array []interface{} switch value.(type) { case string, []byte: - err = json.Unmarshal(gconv.Bytes(value), &array) + err = json.UnmarshalUseNumber(gconv.Bytes(value), &array) default: array = gconv.SliceAny(value) } diff --git a/container/glist/glist_z_unit_test.go b/container/glist/glist_z_unit_test.go index d1dd57c3e..5e1da47c8 100644 --- a/container/glist/glist_z_unit_test.go +++ b/container/glist/glist_z_unit_test.go @@ -711,7 +711,7 @@ func TestList_Json(t *testing.T) { b, err := json.Marshal(a) t.Assert(err, nil) - err = json.Unmarshal(b, l) + err = json.UnmarshalUseNumber(b, l) t.Assert(err, nil) t.Assert(l.FrontAll(), a) }) @@ -721,7 +721,7 @@ func TestList_Json(t *testing.T) { b, err := json.Marshal(a) t.Assert(err, nil) - err = json.Unmarshal(b, &l) + err = json.UnmarshalUseNumber(b, &l) t.Assert(err, nil) t.Assert(l.FrontAll(), a) }) diff --git a/container/gmap/gmap_hash_any_any_map.go b/container/gmap/gmap_hash_any_any_map.go index acc10da60..a42cf26ea 100644 --- a/container/gmap/gmap_hash_any_any_map.go +++ b/container/gmap/gmap_hash_any_any_map.go @@ -476,7 +476,7 @@ func (m *AnyAnyMap) UnmarshalJSON(b []byte) error { m.data = make(map[interface{}]interface{}) } var data map[string]interface{} - if err := json.Unmarshal(b, &data); err != nil { + if err := json.UnmarshalUseNumber(b, &data); err != nil { return err } for k, v := range data { diff --git a/container/gmap/gmap_hash_int_any_map.go b/container/gmap/gmap_hash_int_any_map.go index d04c49a09..f99fd77d4 100644 --- a/container/gmap/gmap_hash_int_any_map.go +++ b/container/gmap/gmap_hash_int_any_map.go @@ -475,7 +475,7 @@ func (m *IntAnyMap) UnmarshalJSON(b []byte) error { if m.data == nil { m.data = make(map[int]interface{}) } - if err := json.Unmarshal(b, &m.data); err != nil { + if err := json.UnmarshalUseNumber(b, &m.data); err != nil { return err } return nil @@ -490,7 +490,7 @@ func (m *IntAnyMap) UnmarshalValue(value interface{}) (err error) { } switch value.(type) { case string, []byte: - return json.Unmarshal(gconv.Bytes(value), &m.data) + return json.UnmarshalUseNumber(gconv.Bytes(value), &m.data) default: for k, v := range gconv.Map(value) { m.data[gconv.Int(k)] = v diff --git a/container/gmap/gmap_hash_int_int_map.go b/container/gmap/gmap_hash_int_int_map.go index 4372246e3..3f8b8d411 100644 --- a/container/gmap/gmap_hash_int_int_map.go +++ b/container/gmap/gmap_hash_int_int_map.go @@ -446,7 +446,7 @@ func (m *IntIntMap) UnmarshalJSON(b []byte) error { if m.data == nil { m.data = make(map[int]int) } - if err := json.Unmarshal(b, &m.data); err != nil { + if err := json.UnmarshalUseNumber(b, &m.data); err != nil { return err } return nil @@ -461,7 +461,7 @@ func (m *IntIntMap) UnmarshalValue(value interface{}) (err error) { } switch value.(type) { case string, []byte: - return json.Unmarshal(gconv.Bytes(value), &m.data) + return json.UnmarshalUseNumber(gconv.Bytes(value), &m.data) default: for k, v := range gconv.Map(value) { m.data[gconv.Int(k)] = gconv.Int(v) diff --git a/container/gmap/gmap_hash_int_str_map.go b/container/gmap/gmap_hash_int_str_map.go index e92f8023a..c5f5ac323 100644 --- a/container/gmap/gmap_hash_int_str_map.go +++ b/container/gmap/gmap_hash_int_str_map.go @@ -446,7 +446,7 @@ func (m *IntStrMap) UnmarshalJSON(b []byte) error { if m.data == nil { m.data = make(map[int]string) } - if err := json.Unmarshal(b, &m.data); err != nil { + if err := json.UnmarshalUseNumber(b, &m.data); err != nil { return err } return nil @@ -461,7 +461,7 @@ func (m *IntStrMap) UnmarshalValue(value interface{}) (err error) { } switch value.(type) { case string, []byte: - return json.Unmarshal(gconv.Bytes(value), &m.data) + return json.UnmarshalUseNumber(gconv.Bytes(value), &m.data) default: for k, v := range gconv.Map(value) { m.data[gconv.Int(k)] = gconv.String(v) diff --git a/container/gmap/gmap_hash_str_any_map.go b/container/gmap/gmap_hash_str_any_map.go index 1ee97612e..04d70f664 100644 --- a/container/gmap/gmap_hash_str_any_map.go +++ b/container/gmap/gmap_hash_str_any_map.go @@ -471,7 +471,7 @@ func (m *StrAnyMap) UnmarshalJSON(b []byte) error { if m.data == nil { m.data = make(map[string]interface{}) } - if err := json.Unmarshal(b, &m.data); err != nil { + if err := json.UnmarshalUseNumber(b, &m.data); err != nil { return err } return nil diff --git a/container/gmap/gmap_hash_str_int_map.go b/container/gmap/gmap_hash_str_int_map.go index 7a51f6ad9..7bc03cd34 100644 --- a/container/gmap/gmap_hash_str_int_map.go +++ b/container/gmap/gmap_hash_str_int_map.go @@ -449,7 +449,7 @@ func (m *StrIntMap) UnmarshalJSON(b []byte) error { if m.data == nil { m.data = make(map[string]int) } - if err := json.Unmarshal(b, &m.data); err != nil { + if err := json.UnmarshalUseNumber(b, &m.data); err != nil { return err } return nil @@ -464,7 +464,7 @@ func (m *StrIntMap) UnmarshalValue(value interface{}) (err error) { } switch value.(type) { case string, []byte: - return json.Unmarshal(gconv.Bytes(value), &m.data) + return json.UnmarshalUseNumber(gconv.Bytes(value), &m.data) default: for k, v := range gconv.Map(value) { m.data[k] = gconv.Int(v) diff --git a/container/gmap/gmap_hash_str_str_map.go b/container/gmap/gmap_hash_str_str_map.go index 04f6c0698..ae68640c8 100644 --- a/container/gmap/gmap_hash_str_str_map.go +++ b/container/gmap/gmap_hash_str_str_map.go @@ -449,7 +449,7 @@ func (m *StrStrMap) UnmarshalJSON(b []byte) error { if m.data == nil { m.data = make(map[string]string) } - if err := json.Unmarshal(b, &m.data); err != nil { + if err := json.UnmarshalUseNumber(b, &m.data); err != nil { return err } return nil diff --git a/container/gmap/gmap_list_map.go b/container/gmap/gmap_list_map.go index 224cb9f63..790998da7 100644 --- a/container/gmap/gmap_list_map.go +++ b/container/gmap/gmap_list_map.go @@ -530,7 +530,7 @@ func (m *ListMap) UnmarshalJSON(b []byte) error { m.list = glist.New() } var data map[string]interface{} - if err := json.Unmarshal(b, &data); err != nil { + if err := json.UnmarshalUseNumber(b, &data); err != nil { return err } for key, value := range data { diff --git a/container/gmap/gmap_z_unit_any_any_test.go b/container/gmap/gmap_z_unit_any_any_test.go index 4a06b4e04..3a5c78592 100644 --- a/container/gmap/gmap_z_unit_any_any_test.go +++ b/container/gmap/gmap_z_unit_any_any_test.go @@ -255,7 +255,7 @@ func Test_AnyAnyMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.New() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) @@ -269,7 +269,7 @@ func Test_AnyAnyMap_Json(t *testing.T) { t.Assert(err, nil) var m gmap.Map - err = json.Unmarshal(b, &m) + err = json.UnmarshalUseNumber(b, &m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) diff --git a/container/gmap/gmap_z_unit_int_any_test.go b/container/gmap/gmap_z_unit_int_any_test.go index c6ac46486..2c6bc25ea 100644 --- a/container/gmap/gmap_z_unit_int_any_test.go +++ b/container/gmap/gmap_z_unit_int_any_test.go @@ -245,7 +245,7 @@ func Test_IntAnyMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewIntAnyMap() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get(1), data[1]) t.Assert(m.Get(2), data[2]) diff --git a/container/gmap/gmap_z_unit_int_int_test.go b/container/gmap/gmap_z_unit_int_int_test.go index 37b940152..762ba223f 100644 --- a/container/gmap/gmap_z_unit_int_int_test.go +++ b/container/gmap/gmap_z_unit_int_int_test.go @@ -251,7 +251,7 @@ func Test_IntIntMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewIntIntMap() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get(1), data[1]) t.Assert(m.Get(2), data[2]) diff --git a/container/gmap/gmap_z_unit_int_str_test.go b/container/gmap/gmap_z_unit_int_str_test.go index 2b4292ef1..6a91b00f6 100644 --- a/container/gmap/gmap_z_unit_int_str_test.go +++ b/container/gmap/gmap_z_unit_int_str_test.go @@ -249,7 +249,7 @@ func Test_IntStrMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewIntStrMap() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get(1), data[1]) t.Assert(m.Get(2), data[2]) diff --git a/container/gmap/gmap_z_unit_list_map_test.go b/container/gmap/gmap_z_unit_list_map_test.go index 045d673a7..4a226cfca 100644 --- a/container/gmap/gmap_z_unit_list_map_test.go +++ b/container/gmap/gmap_z_unit_list_map_test.go @@ -204,7 +204,7 @@ func Test_ListMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewListMap() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) @@ -219,7 +219,7 @@ func Test_ListMap_Json(t *testing.T) { t.Assert(err, nil) var m gmap.ListMap - err = json.Unmarshal(b, &m) + err = json.UnmarshalUseNumber(b, &m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) diff --git a/container/gmap/gmap_z_unit_str_any_test.go b/container/gmap/gmap_z_unit_str_any_test.go index 22180c901..1cee92de2 100644 --- a/container/gmap/gmap_z_unit_str_any_test.go +++ b/container/gmap/gmap_z_unit_str_any_test.go @@ -243,7 +243,7 @@ func Test_StrAnyMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewStrAnyMap() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) @@ -257,7 +257,7 @@ func Test_StrAnyMap_Json(t *testing.T) { t.Assert(err, nil) var m gmap.StrAnyMap - err = json.Unmarshal(b, &m) + err = json.UnmarshalUseNumber(b, &m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) diff --git a/container/gmap/gmap_z_unit_str_int_test.go b/container/gmap/gmap_z_unit_str_int_test.go index 56ba1141a..eb80c7760 100644 --- a/container/gmap/gmap_z_unit_str_int_test.go +++ b/container/gmap/gmap_z_unit_str_int_test.go @@ -247,7 +247,7 @@ func Test_StrIntMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewStrIntMap() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) @@ -261,7 +261,7 @@ func Test_StrIntMap_Json(t *testing.T) { t.Assert(err, nil) var m gmap.StrIntMap - err = json.Unmarshal(b, &m) + err = json.UnmarshalUseNumber(b, &m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) diff --git a/container/gmap/gmap_z_unit_str_str_test.go b/container/gmap/gmap_z_unit_str_str_test.go index 0dfffd3a5..e64a36af1 100644 --- a/container/gmap/gmap_z_unit_str_str_test.go +++ b/container/gmap/gmap_z_unit_str_str_test.go @@ -244,7 +244,7 @@ func Test_StrStrMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewStrStrMap() - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) @@ -258,7 +258,7 @@ func Test_StrStrMap_Json(t *testing.T) { t.Assert(err, nil) var m gmap.StrStrMap - err = json.Unmarshal(b, &m) + err = json.UnmarshalUseNumber(b, &m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) diff --git a/container/gmap/gmap_z_unit_tree_map_test.go b/container/gmap/gmap_z_unit_tree_map_test.go index e008d9986..470cf40f0 100644 --- a/container/gmap/gmap_z_unit_tree_map_test.go +++ b/container/gmap/gmap_z_unit_tree_map_test.go @@ -188,7 +188,7 @@ func Test_TreeMap_Json(t *testing.T) { t.Assert(err, nil) m := gmap.NewTreeMap(gutil.ComparatorString) - err = json.Unmarshal(b, m) + err = json.UnmarshalUseNumber(b, m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) @@ -202,7 +202,7 @@ func Test_TreeMap_Json(t *testing.T) { t.Assert(err, nil) var m gmap.TreeMap - err = json.Unmarshal(b, &m) + err = json.UnmarshalUseNumber(b, &m) t.Assert(err, nil) t.Assert(m.Get("k1"), data["k1"]) t.Assert(m.Get("k2"), data["k2"]) diff --git a/container/gqueue/gqueue.go b/container/gqueue/gqueue.go index fe9bde541..b8a2dfbaa 100644 --- a/container/gqueue/gqueue.go +++ b/container/gqueue/gqueue.go @@ -35,10 +35,8 @@ type Queue struct { } const ( - // Size for queue buffer. - gDEFAULT_QUEUE_SIZE = 10000 - // Max batch size per-fetching from list. - gDEFAULT_MAX_BATCH_SIZE = 10 + defaultQueueSize = 10000 // Size for queue buffer. + defaultBatchSize = 10 // Max batch size per-fetching from list. ) // New returns an empty queue object. @@ -54,7 +52,7 @@ func New(limit ...int) *Queue { } else { q.list = glist.New(true) q.events = make(chan struct{}, math.MaxInt32) - q.C = make(chan interface{}, gDEFAULT_QUEUE_SIZE) + q.C = make(chan interface{}, defaultQueueSize) go q.asyncLoopFromListToChannel() } return q @@ -72,8 +70,8 @@ func (q *Queue) asyncLoopFromListToChannel() { <-q.events for !q.closed.Val() { if length := q.list.Len(); length > 0 { - if length > gDEFAULT_MAX_BATCH_SIZE { - length = gDEFAULT_MAX_BATCH_SIZE + if length > defaultBatchSize { + length = defaultBatchSize } for _, v := range q.list.PopFronts(length) { // When q.C is closed, it will panic here, especially q.C is being blocked for writing. @@ -101,7 +99,7 @@ func (q *Queue) Push(v interface{}) { q.C <- v } else { q.list.PushBack(v) - if len(q.events) < gDEFAULT_QUEUE_SIZE { + if len(q.events) < defaultQueueSize { q.events <- struct{}{} } } @@ -124,14 +122,14 @@ func (q *Queue) Close() { if q.limit > 0 { close(q.C) } - for i := 0; i < gDEFAULT_MAX_BATCH_SIZE; i++ { + 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 -// asynchronize channel reading the list constantly. +// asynchronous channel reading the list constantly. func (q *Queue) Len() (length int) { if q.list != nil { length += q.list.Len() diff --git a/container/gqueue/gqueue_bench_test.go b/container/gqueue/gqueue_z_bench_test.go similarity index 100% rename from container/gqueue/gqueue_bench_test.go rename to container/gqueue/gqueue_z_bench_test.go diff --git a/container/gqueue/gqueue_unit_test.go b/container/gqueue/gqueue_z_unit_test.go similarity index 100% rename from container/gqueue/gqueue_unit_test.go rename to container/gqueue/gqueue_z_unit_test.go diff --git a/container/gset/gset_any_set.go b/container/gset/gset_any_set.go index f6ceccfa6..9db79a47a 100644 --- a/container/gset/gset_any_set.go +++ b/container/gset/gset_any_set.go @@ -477,7 +477,7 @@ func (set *Set) UnmarshalJSON(b []byte) error { set.data = make(map[interface{}]struct{}) } var array []interface{} - if err := json.Unmarshal(b, &array); err != nil { + if err := json.UnmarshalUseNumber(b, &array); err != nil { return err } for _, v := range array { @@ -496,7 +496,7 @@ func (set *Set) UnmarshalValue(value interface{}) (err error) { var array []interface{} switch value.(type) { case string, []byte: - err = json.Unmarshal(gconv.Bytes(value), &array) + err = json.UnmarshalUseNumber(gconv.Bytes(value), &array) default: array = gconv.SliceAny(value) } diff --git a/container/gset/gset_int_set.go b/container/gset/gset_int_set.go index 18d6ca9e2..39a98d9f3 100644 --- a/container/gset/gset_int_set.go +++ b/container/gset/gset_int_set.go @@ -437,7 +437,7 @@ func (set *IntSet) UnmarshalJSON(b []byte) error { set.data = make(map[int]struct{}) } var array []int - if err := json.Unmarshal(b, &array); err != nil { + if err := json.UnmarshalUseNumber(b, &array); err != nil { return err } for _, v := range array { @@ -456,7 +456,7 @@ func (set *IntSet) UnmarshalValue(value interface{}) (err error) { var array []int switch value.(type) { case string, []byte: - err = json.Unmarshal(gconv.Bytes(value), &array) + err = json.UnmarshalUseNumber(gconv.Bytes(value), &array) default: array = gconv.SliceInt(value) } diff --git a/container/gset/gset_str_set.go b/container/gset/gset_str_set.go index d800af22e..68b4ec6bc 100644 --- a/container/gset/gset_str_set.go +++ b/container/gset/gset_str_set.go @@ -465,7 +465,7 @@ func (set *StrSet) UnmarshalJSON(b []byte) error { set.data = make(map[string]struct{}) } var array []string - if err := json.Unmarshal(b, &array); err != nil { + if err := json.UnmarshalUseNumber(b, &array); err != nil { return err } for _, v := range array { @@ -484,7 +484,7 @@ func (set *StrSet) UnmarshalValue(value interface{}) (err error) { var array []string switch value.(type) { case string, []byte: - err = json.Unmarshal(gconv.Bytes(value), &array) + err = json.UnmarshalUseNumber(gconv.Bytes(value), &array) default: array = gconv.SliceStr(value) } diff --git a/container/gset/gset_z_unit_any_test.go b/container/gset/gset_z_unit_any_test.go index d9d6e6479..bef113955 100644 --- a/container/gset/gset_z_unit_any_test.go +++ b/container/gset/gset_z_unit_any_test.go @@ -327,7 +327,7 @@ func TestSet_Json(t *testing.T) { t.Assert(err1, err2) a2 := gset.New() - err2 = json.Unmarshal(b2, &a2) + err2 = json.UnmarshalUseNumber(b2, &a2) t.Assert(err2, nil) t.Assert(a2.Contains("a"), true) t.Assert(a2.Contains("b"), true) @@ -336,7 +336,7 @@ func TestSet_Json(t *testing.T) { t.Assert(a2.Contains("e"), false) var a3 gset.Set - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Contains("a"), true) t.Assert(a3.Contains("b"), true) diff --git a/container/gset/gset_z_unit_int_test.go b/container/gset/gset_z_unit_int_test.go index e45fa21d4..618200691 100644 --- a/container/gset/gset_z_unit_int_test.go +++ b/container/gset/gset_z_unit_int_test.go @@ -358,7 +358,7 @@ func TestIntSet_Json(t *testing.T) { t.Assert(err1, err2) a2 := gset.NewIntSet() - err2 = json.Unmarshal(b2, &a2) + err2 = json.UnmarshalUseNumber(b2, &a2) t.Assert(err2, nil) t.Assert(a2.Contains(1), true) t.Assert(a2.Contains(2), true) @@ -367,7 +367,7 @@ func TestIntSet_Json(t *testing.T) { t.Assert(a2.Contains(5), false) var a3 gset.IntSet - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a2.Contains(1), true) t.Assert(a2.Contains(2), true) diff --git a/container/gset/gset_z_unit_str_test.go b/container/gset/gset_z_unit_str_test.go index 33586cd8e..ab57cf6ca 100644 --- a/container/gset/gset_z_unit_str_test.go +++ b/container/gset/gset_z_unit_str_test.go @@ -404,7 +404,7 @@ func TestStrSet_Json(t *testing.T) { t.Assert(err1, err2) a2 := gset.NewStrSet() - err2 = json.Unmarshal(b2, &a2) + err2 = json.UnmarshalUseNumber(b2, &a2) t.Assert(err2, nil) t.Assert(a2.Contains("a"), true) t.Assert(a2.Contains("b"), true) @@ -413,7 +413,7 @@ func TestStrSet_Json(t *testing.T) { t.Assert(a2.Contains("e"), false) var a3 gset.StrSet - err := json.Unmarshal(b2, &a3) + err := json.UnmarshalUseNumber(b2, &a3) t.Assert(err, nil) t.Assert(a3.Contains("a"), true) t.Assert(a3.Contains("b"), true) diff --git a/container/gtree/gtree_redblacktree.go b/container/gtree/gtree_redblacktree.go index 33a8d45dd..7ccf0fd1c 100644 --- a/container/gtree/gtree_redblacktree.go +++ b/container/gtree/gtree_redblacktree.go @@ -937,7 +937,7 @@ func (tree *RedBlackTree) UnmarshalJSON(b []byte) error { tree.comparator = gutil.ComparatorString } var data map[string]interface{} - if err := json.Unmarshal(b, &data); err != nil { + if err := json.UnmarshalUseNumber(b, &data); err != nil { return err } for k, v := range data { diff --git a/container/gtype/interface.go b/container/gtype/interface.go index acb26f46c..8cd554dde 100644 --- a/container/gtype/interface.go +++ b/container/gtype/interface.go @@ -58,7 +58,7 @@ func (v *Interface) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the interface UnmarshalJSON for json.Unmarshal. func (v *Interface) UnmarshalJSON(b []byte) error { var i interface{} - err := json.Unmarshal(b, &i) + err := json.UnmarshalUseNumber(b, &i) if err != nil { return err } diff --git a/container/gtype/z_unit_bool_test.go b/container/gtype/z_unit_bool_test.go index 297f5de85..b30738a84 100644 --- a/container/gtype/z_unit_bool_test.go +++ b/container/gtype/z_unit_bool_test.go @@ -55,16 +55,16 @@ func Test_Bool_JSON(t *testing.T) { gtest.C(t, func(t *gtest.T) { var err error i := gtype.NewBool() - err = json.Unmarshal([]byte("true"), &i) + err = json.UnmarshalUseNumber([]byte("true"), &i) t.Assert(err, nil) t.Assert(i.Val(), true) - err = json.Unmarshal([]byte("false"), &i) + err = json.UnmarshalUseNumber([]byte("false"), &i) t.Assert(err, nil) t.Assert(i.Val(), false) - err = json.Unmarshal([]byte("1"), &i) + err = json.UnmarshalUseNumber([]byte("1"), &i) t.Assert(err, nil) t.Assert(i.Val(), true) - err = json.Unmarshal([]byte("0"), &i) + err = json.UnmarshalUseNumber([]byte("0"), &i) t.Assert(err, nil) t.Assert(i.Val(), false) }) @@ -78,7 +78,7 @@ func Test_Bool_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewBool() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), i.Val()) }) @@ -91,7 +91,7 @@ func Test_Bool_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewBool() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), i.Val()) }) diff --git a/container/gtype/z_unit_byte_test.go b/container/gtype/z_unit_byte_test.go index 0a6c77df4..86ba6cfb9 100644 --- a/container/gtype/z_unit_byte_test.go +++ b/container/gtype/z_unit_byte_test.go @@ -53,7 +53,7 @@ func Test_Byte_JSON(t *testing.T) { gtest.C(t, func(t *gtest.T) { var err error i := gtype.NewByte() - err = json.Unmarshal([]byte("49"), &i) + err = json.UnmarshalUseNumber([]byte("49"), &i) t.Assert(err, nil) t.Assert(i.Val(), "49") }) diff --git a/container/gtype/z_unit_bytes_test.go b/container/gtype/z_unit_bytes_test.go index 234307ae8..49145d290 100644 --- a/container/gtype/z_unit_bytes_test.go +++ b/container/gtype/z_unit_bytes_test.go @@ -39,7 +39,7 @@ func Test_Bytes_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewBytes() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), b) }) diff --git a/container/gtype/z_unit_float32_test.go b/container/gtype/z_unit_float32_test.go index 14914ed27..7aff7f79c 100644 --- a/container/gtype/z_unit_float32_test.go +++ b/container/gtype/z_unit_float32_test.go @@ -40,7 +40,7 @@ func Test_Float32_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewFloat32() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), v) }) diff --git a/container/gtype/z_unit_float64_test.go b/container/gtype/z_unit_float64_test.go index 717e0500a..1ba6cfa1e 100644 --- a/container/gtype/z_unit_float64_test.go +++ b/container/gtype/z_unit_float64_test.go @@ -38,7 +38,7 @@ func Test_Float64_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewFloat64() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), v) }) diff --git a/container/gtype/z_unit_int32_test.go b/container/gtype/z_unit_int32_test.go index c2b7d5690..cf2250ae5 100644 --- a/container/gtype/z_unit_int32_test.go +++ b/container/gtype/z_unit_int32_test.go @@ -51,7 +51,7 @@ func Test_Int32_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewInt32() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), v) }) diff --git a/container/gtype/z_unit_int64_test.go b/container/gtype/z_unit_int64_test.go index 85a0bbea8..8b5c6a299 100644 --- a/container/gtype/z_unit_int64_test.go +++ b/container/gtype/z_unit_int64_test.go @@ -50,7 +50,7 @@ func Test_Int64_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewInt64() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), i) }) diff --git a/container/gtype/z_unit_int_test.go b/container/gtype/z_unit_int_test.go index 49e44b2f2..939ef81e9 100644 --- a/container/gtype/z_unit_int_test.go +++ b/container/gtype/z_unit_int_test.go @@ -50,7 +50,7 @@ func Test_Int_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewInt() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), v) }) diff --git a/container/gtype/z_unit_interface_test.go b/container/gtype/z_unit_interface_test.go index 990951b53..130102f63 100644 --- a/container/gtype/z_unit_interface_test.go +++ b/container/gtype/z_unit_interface_test.go @@ -40,7 +40,7 @@ func Test_Interface_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.New() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), s) }) diff --git a/container/gtype/z_unit_string_test.go b/container/gtype/z_unit_string_test.go index 16132474d..3b42cca41 100644 --- a/container/gtype/z_unit_string_test.go +++ b/container/gtype/z_unit_string_test.go @@ -38,7 +38,7 @@ func Test_String_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewString() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), s) }) diff --git a/container/gtype/z_unit_uint32_test.go b/container/gtype/z_unit_uint32_test.go index 1e9d0a508..abe188dad 100644 --- a/container/gtype/z_unit_uint32_test.go +++ b/container/gtype/z_unit_uint32_test.go @@ -50,7 +50,7 @@ func Test_Uint32_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewUint32() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), i) }) diff --git a/container/gtype/z_unit_uint64_test.go b/container/gtype/z_unit_uint64_test.go index 1c97b53c5..ab05217ff 100644 --- a/container/gtype/z_unit_uint64_test.go +++ b/container/gtype/z_unit_uint64_test.go @@ -55,7 +55,7 @@ func Test_Uint64_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewUint64() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), i) }) diff --git a/container/gtype/z_unit_uint_test.go b/container/gtype/z_unit_uint_test.go index d9237940a..4f314856e 100644 --- a/container/gtype/z_unit_uint_test.go +++ b/container/gtype/z_unit_uint_test.go @@ -49,7 +49,7 @@ func Test_Uint_JSON(t *testing.T) { t.Assert(b1, b2) i2 := gtype.NewUint() - err := json.Unmarshal(b2, &i2) + err := json.UnmarshalUseNumber(b2, &i2) t.Assert(err, nil) t.Assert(i2.Val(), i) }) diff --git a/container/gvar/gvar.go b/container/gvar/gvar.go index 1babba781..36480d71a 100644 --- a/container/gvar/gvar.go +++ b/container/gvar/gvar.go @@ -189,7 +189,7 @@ func (v *Var) MarshalJSON() ([]byte, error) { // UnmarshalJSON implements the interface UnmarshalJSON for json.Unmarshal. func (v *Var) UnmarshalJSON(b []byte) error { var i interface{} - err := json.Unmarshal(b, &i) + err := json.UnmarshalUseNumber(b, &i) if err != nil { return err } diff --git a/container/gvar/gvar_z_unit_json_test.go b/container/gvar/gvar_z_unit_json_test.go index 53dc856f3..dde33a732 100644 --- a/container/gvar/gvar_z_unit_json_test.go +++ b/container/gvar/gvar_z_unit_json_test.go @@ -41,7 +41,7 @@ func TestVar_Json(t *testing.T) { b, err := json.Marshal(s) t.Assert(err, nil) - err = json.Unmarshal(b, v) + err = json.UnmarshalUseNumber(b, v) t.Assert(err, nil) t.Assert(v.String(), s) }) @@ -52,7 +52,7 @@ func TestVar_Json(t *testing.T) { b, err := json.Marshal(s) t.Assert(err, nil) - err = json.Unmarshal(b, &v) + err = json.UnmarshalUseNumber(b, &v) t.Assert(err, nil) t.Assert(v.String(), s) }) diff --git a/database/gdb/gdb.go b/database/gdb/gdb.go index 10672ca3a..ab415021a 100644 --- a/database/gdb/gdb.go +++ b/database/gdb/gdb.go @@ -11,9 +11,10 @@ import ( "context" "database/sql" "fmt" + "time" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/os/gcmd" - "time" "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/internal/intlog" @@ -32,11 +33,12 @@ type DB interface { // Model creation. // =========================================================================== + // Table function is deprecated, use Model instead. // The DB interface is designed not only for // relational databases but also for NoSQL databases in the future. The name // "Table" is not proper for that purpose any more. - // Deprecated, use Model instead. - Table(table ...string) *Model + // Also see Core.Table. + Table(tableNameOrStruct ...interface{}) *Model // Model creates and returns a new ORM model from given schema. // The parameter `table` can be more than one table names, and also alias name, like: @@ -46,150 +48,144 @@ type DB interface { // Model("user, user_detail") // Model("user u, user_detail ud") // 2. Model name with alias: Model("user", "u") - Model(table ...string) *Model + // Also see Core.Model. + Model(tableNameOrStruct ...interface{}) *Model // Schema creates and returns a schema. + // Also see Core.Schema. Schema(schema string) *Schema // With creates and returns an ORM model based on meta data of given object. - With(object interface{}) *Model + // Also see Core.With. + With(objects ...interface{}) *Model // Open creates a raw connection object for database with given node configuration. // Note that it is not recommended using the this function manually. + // Also see DriverMysql.Open. Open(config *ConfigNode) (*sql.DB, error) // Ctx is a chaining function, which creates and returns a new DB that is a shallow copy // of current DB object and with given context in it. // Note that this returned DB object can be used only once, so do not assign it to // a global or package variable for long using. + // Also see Core.Ctx. Ctx(ctx context.Context) DB // =========================================================================== // Query APIs. // =========================================================================== - Query(sql string, args ...interface{}) (*sql.Rows, error) - Exec(sql string, args ...interface{}) (sql.Result, error) - Prepare(sql string, execOnMaster ...bool) (*Stmt, error) + Query(sql string, args ...interface{}) (*sql.Rows, error) // See Core.Query. + Exec(sql string, args ...interface{}) (sql.Result, error) // See Core.Exec. + Prepare(sql string, execOnMaster ...bool) (*Stmt, error) // See Core.Prepare. // =========================================================================== // Common APIs for CURD. // =========================================================================== - Insert(table string, data interface{}, batch ...int) (sql.Result, error) - InsertIgnore(table string, data interface{}, batch ...int) (sql.Result, error) - Replace(table string, data interface{}, batch ...int) (sql.Result, error) - Save(table string, data interface{}, batch ...int) (sql.Result, error) + Insert(table string, data interface{}, batch ...int) (sql.Result, error) // See Core.Insert. + InsertIgnore(table string, data interface{}, batch ...int) (sql.Result, error) // See Core.InsertIgnore. + InsertAndGetId(table string, data interface{}, batch ...int) (int64, error) // See Core.InsertAndGetId. + Replace(table string, data interface{}, batch ...int) (sql.Result, error) // See Core.Replace. + Save(table string, data interface{}, batch ...int) (sql.Result, error) // See Core.Save. - BatchInsert(table string, list interface{}, batch ...int) (sql.Result, error) - BatchReplace(table string, list interface{}, batch ...int) (sql.Result, error) - BatchSave(table string, list interface{}, batch ...int) (sql.Result, error) + BatchInsert(table string, list interface{}, batch ...int) (sql.Result, error) // See Core.BatchInsert. + BatchReplace(table string, list interface{}, batch ...int) (sql.Result, error) // See Core.BatchReplace. + BatchSave(table string, list interface{}, batch ...int) (sql.Result, error) // See Core.BatchSave. - Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) - Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) + Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) // See Core.Update. + Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) // See Core.Delete. // =========================================================================== // Internal APIs for CURD, which can be overwrote for custom CURD implements. // =========================================================================== - DoQuery(link Link, sql string, args ...interface{}) (rows *sql.Rows, err error) - DoGetAll(link Link, sql string, args ...interface{}) (result Result, err error) - DoExec(link Link, sql string, args ...interface{}) (result sql.Result, err error) - DoPrepare(link Link, sql string) (*Stmt, error) - DoInsert(link Link, table string, data interface{}, option int, batch ...int) (result sql.Result, err error) - DoBatchInsert(link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) - DoUpdate(link Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) - DoDelete(link Link, table string, condition string, args ...interface{}) (result sql.Result, err error) + DoQuery(ctx context.Context, link Link, sql string, args ...interface{}) (rows *sql.Rows, err error) // See Core.DoQuery. + DoExec(ctx context.Context, link Link, sql string, args ...interface{}) (result sql.Result, err error) // See Core.DoExec. + DoPrepare(ctx context.Context, link Link, sql string) (*Stmt, error) // See Core.DoPrepare. + DoGetAll(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) // See Core.DoGetAll. + DoInsert(ctx context.Context, link Link, table string, data interface{}, option int, batch ...int) (result sql.Result, err error) // See Core.DoInsert. + DoBatchInsert(ctx context.Context, link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) // See Core.DoBatchInsert. + DoUpdate(ctx context.Context, link Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) // See Core.DoUpdate. + DoDelete(ctx context.Context, link Link, table string, condition string, args ...interface{}) (result sql.Result, err error) // See Core.DoDelete. // =========================================================================== // Query APIs for convenience purpose. // =========================================================================== - GetAll(sql string, args ...interface{}) (Result, error) - GetOne(sql string, args ...interface{}) (Record, error) - GetValue(sql string, args ...interface{}) (Value, error) - GetArray(sql string, args ...interface{}) ([]Value, error) - GetCount(sql string, args ...interface{}) (int, error) - GetStruct(objPointer interface{}, sql string, args ...interface{}) error - GetStructs(objPointerSlice interface{}, sql string, args ...interface{}) error - GetScan(objPointer interface{}, sql string, args ...interface{}) error + GetAll(sql string, args ...interface{}) (Result, error) // See Core.GetAll. + GetOne(sql string, args ...interface{}) (Record, error) // See Core.GetOne. + GetValue(sql string, args ...interface{}) (Value, error) // See Core.GetValue. + GetArray(sql string, args ...interface{}) ([]Value, error) // See Core.GetArray. + GetCount(sql string, args ...interface{}) (int, error) // See Core.GetCount. + GetStruct(objPointer interface{}, sql string, args ...interface{}) error // See Core.GetStruct. + GetStructs(objPointerSlice interface{}, sql string, args ...interface{}) error // See Core.GetStructs. + GetScan(objPointer interface{}, sql string, args ...interface{}) error // See Core.GetScan. // =========================================================================== // Master/Slave specification support. // =========================================================================== - Master() (*sql.DB, error) - Slave() (*sql.DB, error) + Master(schema ...string) (*sql.DB, error) // See Core.Master. + Slave(schema ...string) (*sql.DB, error) // See Core.Slave. // =========================================================================== // Ping-Pong. // =========================================================================== - PingMaster() error - PingSlave() error + PingMaster() error // See Core.PingMaster. + PingSlave() error // See Core.PingSlave. // =========================================================================== // Transaction. // =========================================================================== - Begin() (*TX, error) - Transaction(f func(tx *TX) error) (err error) + Begin() (*TX, error) // See Core.Begin. + Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) error // See Core.Transaction. // =========================================================================== // Configuration methods. // =========================================================================== - GetCache() *gcache.Cache - SetDebug(debug bool) - GetDebug() bool - SetSchema(schema string) - GetSchema() string - GetPrefix() string - GetGroup() string - SetDryRun(dryrun bool) - GetDryRun() bool - SetLogger(logger *glog.Logger) - GetLogger() *glog.Logger - GetConfig() *ConfigNode - SetMaxIdleConnCount(n int) - SetMaxOpenConnCount(n int) - SetMaxConnLifetime(d time.Duration) + GetCache() *gcache.Cache // See Core.GetCache. + SetDebug(debug bool) // See Core.SetDebug. + GetDebug() bool // See Core.GetDebug. + SetSchema(schema string) // See Core.SetSchema. + GetSchema() string // See Core.GetSchema. + GetPrefix() string // See Core.GetPrefix. + GetGroup() string // See Core.GetGroup. + SetDryRun(enabled bool) // See Core.SetDryRun. + GetDryRun() bool // See Core.GetDryRun. + SetLogger(logger *glog.Logger) // See Core.SetLogger. + GetLogger() *glog.Logger // See Core.GetLogger. + GetConfig() *ConfigNode // See Core.GetConfig. + SetMaxIdleConnCount(n int) // See Core.SetMaxIdleConnCount. + SetMaxOpenConnCount(n int) // See Core.SetMaxOpenConnCount. + SetMaxConnLifeTime(d time.Duration) // See Core.SetMaxConnLifeTime. // =========================================================================== // Utility methods. // =========================================================================== - GetCtx() context.Context - GetChars() (charLeft string, charRight string) - GetMaster(schema ...string) (*sql.DB, error) - GetSlave(schema ...string) (*sql.DB, error) - QuoteWord(s string) string - QuoteString(s string) string - QuotePrefixTableName(table string) string - Tables(schema ...string) (tables []string, err error) - TableFields(table string, schema ...string) (map[string]*TableField, error) - HasTable(name string) (bool, error) - FilteredLinkInfo() string + GetCtx() context.Context // See Core.GetCtx. + GetCore() *Core // See Core.GetCore + GetChars() (charLeft string, charRight string) // See Core.GetChars. + Tables(ctx context.Context, schema ...string) (tables []string, err error) // See Core.Tables. + TableFields(ctx context.Context, table string, schema ...string) (map[string]*TableField, error) // See Core.TableFields. + FilteredLinkInfo() string // See Core.FilteredLinkInfo. // HandleSqlBeforeCommit is a hook function, which deals with the sql string before // it's committed to underlying driver. The parameter `link` specifies the current // database connection operation object. You can modify the sql string `sql` and its // arguments `args` as you wish before they're committed to driver. - HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{}) - - // =========================================================================== - // Internal methods, for internal usage purpose, you do not need consider it. - // =========================================================================== - - mappingAndFilterData(schema, table string, data map[string]interface{}, filter bool) (map[string]interface{}, error) - convertFieldValueToLocalValue(fieldValue interface{}, fieldType string) interface{} - convertRowsToResult(rows *sql.Rows) (Result, error) + // Also see Core.HandleSqlBeforeCommit. + HandleSqlBeforeCommit(ctx context.Context, link Link, sql string, args []interface{}) (string, []interface{}) } // Core is the base struct for database management. type Core struct { db DB // DB interface object. - ctx context.Context // Context for chaining operation only. + ctx context.Context // Context for chaining operation only. Do not set a default value in Core initialization. group string // Configuration group name. debug *gtype.Bool // Enable debug mode for the database, which can be changed in runtime. cache *gcache.Cache // Cache manager, SQL result cache only. @@ -204,16 +200,28 @@ type Driver interface { New(core *Core, node *ConfigNode) (DB, error) } +// Link is a common database function wrapper interface. +type Link interface { + Query(sql string, args ...interface{}) (*sql.Rows, error) + Exec(sql string, args ...interface{}) (sql.Result, error) + Prepare(sql string) (*sql.Stmt, error) + QueryContext(ctx context.Context, sql string, args ...interface{}) (*sql.Rows, error) + ExecContext(ctx context.Context, sql string, args ...interface{}) (sql.Result, error) + PrepareContext(ctx context.Context, sql string) (*sql.Stmt, error) + IsTransaction() bool +} + // Sql is the sql recording struct. type Sql struct { - Sql string // SQL string(may contain reserved char '?'). - Type string // SQL operation type. - Args []interface{} // Arguments for this sql. - Format string // Formatted sql which contains arguments in the sql. - Error error // Execution result. - Start int64 // Start execution timestamp in milliseconds. - End int64 // End execution timestamp in milliseconds. - Group string // Group is the group name of the configuration that the sql is executed from. + Sql string // SQL string(may contain reserved char '?'). + Type string // SQL operation type. + Args []interface{} // Arguments for this sql. + Format string // Formatted sql which contains arguments in the sql. + Error error // Execution result. + Start int64 // Start execution timestamp in milliseconds. + End int64 // End execution timestamp in milliseconds. + Group string // Group is the group name of the configuration that the sql is executed from. + IsTransaction bool // IsTransaction marks whether this sql is executed in transaction. } // TableField is the struct for table field. @@ -228,16 +236,6 @@ type TableField struct { Comment string // Comment. } -// Link is a common database function wrapper interface. -type Link interface { - Query(sql string, args ...interface{}) (*sql.Rows, error) - Exec(sql string, args ...interface{}) (sql.Result, error) - Prepare(sql string) (*sql.Stmt, error) - QueryContext(ctx context.Context, sql string, args ...interface{}) (*sql.Rows, error) - ExecContext(ctx context.Context, sql string, args ...interface{}) (sql.Result, error) - PrepareContext(ctx context.Context, sql string) (*sql.Stmt, error) -} - // Counter is the type for update count. type Counter struct { Field string @@ -289,16 +287,19 @@ var ( // regularFieldNameRegPattern is the regular expression pattern for a string // which is a regular field name of table. - regularFieldNameRegPattern = `^[\w\.\-\_]+$` + regularFieldNameRegPattern = `^[\w\.\-]+$` // regularFieldNameWithoutDotRegPattern is similar to regularFieldNameRegPattern but not allows '.'. // Note that, although some databases allow char '.' in the field name, but it here does not allow '.' // in the field name as it conflicts with "db.table.field" pattern in SOME situations. - regularFieldNameWithoutDotRegPattern = `^[\w\-\_]+$` + regularFieldNameWithoutDotRegPattern = `^[\w\-]+$` // internalCache is the memory cache for internal usage. internalCache = gcache.New() + // tableFieldsMap caches the table information retrived from database. + tableFieldsMap = gmap.New(true) + // allDryRun sets dry-run feature for all database connections. // It is commonly used for command options for convenience. allDryRun = false @@ -327,7 +328,7 @@ func New(group ...string) (db DB, err error) { defer configs.RUnlock() if len(configs.config) < 1 { - return nil, gerror.New("empty database configuration") + return nil, gerror.New("database configuration is empty, please set the database configuration before using") } if _, ok := configs.config[groupName]; ok { if node, err := getConfigNodeByGroup(groupName, true); err == nil { @@ -346,13 +347,19 @@ func New(group ...string) (db DB, err error) { } return c.db, nil } else { - return nil, gerror.New(fmt.Sprintf(`unsupported database type "%s"`, node.Type)) + return nil, gerror.Newf( + `cannot find database driver for specified database type "%s", did you misspell type name "%s" or forget importing the database driver?`, + node.Type, node.Type, + ) } } else { return nil, err } } else { - return nil, gerror.New(fmt.Sprintf(`database configuration node "%s" is not found`, groupName)) + return nil, gerror.Newf( + `database configuration node "%s" is not found, did you misspell group name "%s" or miss the database configuration?`, + groupName, groupName, + ) } } @@ -471,15 +478,26 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error } // Cache the underlying connection pool object by node. v, _ := internalCache.GetOrSetFuncLock(node.String(), func() (interface{}, error) { - sqlDb, err = c.db.Open(node) - if err != nil { - intlog.Printf(`db open failed: %v, %+v`, err, node) - return nil, err - } intlog.Printf( - `open new connection, master:%v, config:%+v, node:%+v`, + `open new connection, master:%#v, config:%#v, node:%#v`, master, c.config, node, ) + defer func() { + if err != nil { + intlog.Printf(`open new connection failed: %v, %#v`, err, node) + } else { + intlog.Printf( + `open new connection success, master:%#v, config:%#v, node:%#v`, + master, c.config, node, + ) + } + }() + + sqlDb, err = c.db.Open(node) + if err != nil { + return nil, err + } + if c.config.MaxIdleConnCount > 0 { sqlDb.SetMaxIdleConns(c.config.MaxIdleConnCount) } else { @@ -490,13 +508,13 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error } else { sqlDb.SetMaxOpenConns(defaultMaxOpenConnCount) } - if c.config.MaxConnLifetime > 0 { + if c.config.MaxConnLifeTime > 0 { // Automatically checks whether MaxConnLifetime is configured using string like: "30s", "60s", etc. // Or else it is configured just using number, which means value in seconds. - if c.config.MaxConnLifetime > time.Second { - sqlDb.SetConnMaxLifetime(c.config.MaxConnLifetime) + if c.config.MaxConnLifeTime > time.Second { + sqlDb.SetConnMaxLifetime(c.config.MaxConnLifeTime) } else { - sqlDb.SetConnMaxLifetime(c.config.MaxConnLifetime * time.Second) + sqlDb.SetConnMaxLifetime(c.config.MaxConnLifeTime * time.Second) } } else { sqlDb.SetConnMaxLifetime(defaultMaxConnLifeTime) diff --git a/database/gdb/gdb_core.go b/database/gdb/gdb_core.go index 7f2a6a1b5..84d50bbd9 100644 --- a/database/gdb/gdb_core.go +++ b/database/gdb/gdb_core.go @@ -11,19 +11,23 @@ import ( "context" "database/sql" "fmt" - "github.com/gogf/gf/errors/gerror" - "github.com/gogf/gf/text/gstr" "reflect" "strings" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/utils" + "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/container/gvar" - "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/util/gconv" ) +// GetCore returns the underlying *Core object. +func (c *Core) GetCore() *Core { + return c +} + // Ctx is a chaining function, which creates and returns a new DB that is a shallow copy // of current DB object and with given context in it. // Note that this returned DB object can be used only once, so do not assign it to @@ -32,6 +36,11 @@ func (c *Core) Ctx(ctx context.Context) DB { if ctx == nil { return c.db } + // It is already set context in previous chaining operation. + if c.ctx != nil { + return c.db + } + // It makes a shallow copy of current db and changes its context for next chaining operation. var ( err error newCore = &Core{} @@ -39,10 +48,12 @@ func (c *Core) Ctx(ctx context.Context) DB { ) *newCore = *c newCore.ctx = ctx + // It creates a new DB object, which is commonly a wrapper for object `Core`. newCore.db, err = driverMap[configNode.Type].New(newCore, configNode) - // Seldom error, just log it. if err != nil { - c.db.GetLogger().Ctx(ctx).Error(err) + // It is really a serious error here. + // Do not let it continue. + panic(err) } return newCore.db } @@ -53,13 +64,13 @@ func (c *Core) GetCtx() context.Context { if c.ctx != nil { return c.ctx } - return context.Background() + return context.TODO() } // GetCtxTimeout returns the context and cancel function for specified timeout type. func (c *Core) GetCtxTimeout(timeoutType int, ctx context.Context) (context.Context, context.CancelFunc) { if ctx == nil { - ctx = c.db.GetCtx() + ctx = c.GetCtx() } else { ctx = context.WithValue(ctx, "WrappedByGetCtxTimeout", nil) } @@ -84,184 +95,41 @@ func (c *Core) GetCtxTimeout(timeoutType int, ctx context.Context) (context.Cont // Master creates and returns a connection from master node if master-slave configured. // It returns the default connection if master-slave not configured. -func (c *Core) Master() (*sql.DB, error) { - return c.getSqlDb(true, c.schema.Val()) +func (c *Core) Master(schema ...string) (*sql.DB, error) { + useSchema := "" + if len(schema) > 0 && schema[0] != "" { + useSchema = schema[0] + } else { + useSchema = c.schema.Val() + } + return c.getSqlDb(true, useSchema) } // Slave creates and returns a connection from slave node if master-slave configured. // It returns the default connection if master-slave not configured. -func (c *Core) Slave() (*sql.DB, error) { - return c.getSqlDb(false, c.schema.Val()) -} - -// Query commits one query SQL to underlying driver and returns the execution result. -// It is most commonly used for data querying. -func (c *Core) Query(sql string, args ...interface{}) (rows *sql.Rows, err error) { - link, err := c.db.Slave() - if err != nil { - return nil, err - } - return c.db.DoQuery(link, sql, args...) -} - -// DoQuery commits the sql string and its arguments to underlying driver -// through given link object and returns the execution result. -func (c *Core) DoQuery(link Link, sql string, args ...interface{}) (rows *sql.Rows, err error) { - sql, args = formatSql(sql, args) - sql, args = c.db.HandleSqlBeforeCommit(link, sql, args) - ctx := c.db.GetCtx() - if c.GetConfig().QueryTimeout > 0 { - ctx, _ = context.WithTimeout(ctx, c.GetConfig().QueryTimeout) - } - - mTime1 := gtime.TimestampMilli() - rows, err = link.QueryContext(ctx, sql, args...) - mTime2 := gtime.TimestampMilli() - sqlObj := &Sql{ - Sql: sql, - Type: "DB.QueryContext", - Args: args, - Format: FormatSqlWithArgs(sql, args), - Error: err, - Start: mTime1, - End: mTime2, - Group: c.db.GetGroup(), - } - c.addSqlToTracing(ctx, sqlObj) - if c.db.GetDebug() { - c.writeSqlToLogger(sqlObj) - } - if err == nil { - return rows, nil +func (c *Core) Slave(schema ...string) (*sql.DB, error) { + useSchema := "" + if len(schema) > 0 && schema[0] != "" { + useSchema = schema[0] } else { - err = formatError(err, sql, args...) + useSchema = c.schema.Val() } - return nil, err -} - -// Exec commits one query SQL to underlying driver and returns the execution result. -// It is most commonly used for data inserting and updating. -func (c *Core) Exec(sql string, args ...interface{}) (result sql.Result, err error) { - link, err := c.db.Master() - if err != nil { - return nil, err - } - return c.db.DoExec(link, sql, args...) -} - -// DoExec commits the sql string and its arguments to underlying driver -// through given link object and returns the execution result. -func (c *Core) DoExec(link Link, sql string, args ...interface{}) (result sql.Result, err error) { - sql, args = formatSql(sql, args) - sql, args = c.db.HandleSqlBeforeCommit(link, sql, args) - ctx := c.db.GetCtx() - if c.GetConfig().ExecTimeout > 0 { - var cancelFunc context.CancelFunc - ctx, cancelFunc = context.WithTimeout(ctx, c.GetConfig().ExecTimeout) - defer cancelFunc() - } - - mTime1 := gtime.TimestampMilli() - if !c.db.GetDryRun() { - result, err = link.ExecContext(ctx, sql, args...) - } else { - result = new(SqlResult) - } - mTime2 := gtime.TimestampMilli() - sqlObj := &Sql{ - Sql: sql, - Type: "DB.ExecContext", - Args: args, - Format: FormatSqlWithArgs(sql, args), - Error: err, - Start: mTime1, - End: mTime2, - Group: c.db.GetGroup(), - } - c.addSqlToTracing(ctx, sqlObj) - if c.db.GetDebug() { - c.writeSqlToLogger(sqlObj) - } - return result, formatError(err, sql, args...) -} - -// Prepare creates a prepared statement for later queries or executions. -// Multiple queries or executions may be run concurrently from the -// returned statement. -// The caller must call the statement's Close method -// when the statement is no longer needed. -// -// The parameter `execOnMaster` specifies whether executing the sql on master node, -// or else it executes the sql on slave node if master-slave configured. -func (c *Core) Prepare(sql string, execOnMaster ...bool) (*Stmt, error) { - var ( - err error - link Link - ) - if len(execOnMaster) > 0 && execOnMaster[0] { - if link, err = c.db.Master(); err != nil { - return nil, err - } - } else { - if link, err = c.db.Slave(); err != nil { - return nil, err - } - } - return c.db.DoPrepare(link, sql) -} - -// doPrepare calls prepare function on given link object and returns the statement object. -func (c *Core) DoPrepare(link Link, sql string) (*Stmt, error) { - ctx := c.db.GetCtx() - if c.GetConfig().PrepareTimeout > 0 { - // DO NOT USE cancel function in prepare statement. - ctx, _ = context.WithTimeout(ctx, c.GetConfig().PrepareTimeout) - } - var ( - mTime1 = gtime.TimestampMilli() - stmt, err = link.PrepareContext(ctx, sql) - mTime2 = gtime.TimestampMilli() - sqlObj = &Sql{ - Sql: sql, - Type: "DB.PrepareContext", - Args: nil, - Format: FormatSqlWithArgs(sql, nil), - Error: err, - Start: mTime1, - End: mTime2, - Group: c.db.GetGroup(), - } - ) - c.addSqlToTracing(ctx, sqlObj) - if c.db.GetDebug() { - c.writeSqlToLogger(sqlObj) - } - return &Stmt{ - Stmt: stmt, - core: c, - sql: sql, - }, err + return c.getSqlDb(false, useSchema) } // GetAll queries and returns data records from database. func (c *Core) GetAll(sql string, args ...interface{}) (Result, error) { - return c.db.DoGetAll(nil, sql, args...) + return c.db.DoGetAll(c.GetCtx(), nil, sql, args...) } // DoGetAll queries and returns data records from database. -func (c *Core) DoGetAll(link Link, sql string, args ...interface{}) (result Result, err error) { - if link == nil { - link, err = c.db.Slave() - if err != nil { - return nil, err - } - } - rows, err := c.db.DoQuery(link, sql, args...) +func (c *Core) DoGetAll(ctx context.Context, link Link, sql string, args ...interface{}) (result Result, err error) { + rows, err := c.db.DoQuery(ctx, link, sql, args...) if err != nil || rows == nil { return nil, err } defer rows.Close() - return c.db.convertRowsToResult(rows) + return c.convertRowsToResult(rows) } // GetOne queries and returns one record from database. @@ -277,9 +145,9 @@ func (c *Core) GetOne(sql string, args ...interface{}) (Record, error) { } // GetArray queries and returns data values as slice from database. -// Note that if there're multiple columns in the result, it returns just one column values randomly. +// Note that if there are multiple columns in the result, it returns just one column values randomly. func (c *Core) GetArray(sql string, args ...interface{}) ([]Value, error) { - all, err := c.db.DoGetAll(nil, sql, args...) + all, err := c.db.DoGetAll(c.GetCtx(), nil, sql, args...) if err != nil { return nil, err } @@ -374,65 +242,6 @@ func (c *Core) PingSlave() error { } } -// Begin starts and returns the transaction object. -// You should call Commit or Rollback functions of the transaction object -// if you no longer use the transaction. Commit or Rollback functions will also -// close the transaction automatically. -func (c *Core) Begin() (*TX, error) { - if master, err := c.db.Master(); err != nil { - return nil, err - } else { - ctx := c.db.GetCtx() - if c.GetConfig().TranTimeout > 0 { - var cancelFunc context.CancelFunc - ctx, cancelFunc = context.WithTimeout(ctx, c.GetConfig().TranTimeout) - defer cancelFunc() - } - if tx, err := master.BeginTx(ctx, nil); err == nil { - return &TX{ - db: c.db, - tx: tx, - master: master, - }, nil - } else { - return nil, err - } - } -} - -// Transaction wraps the transaction logic using function `f`. -// It rollbacks the transaction and returns the error from function `f` if -// it returns non-nil error. It commits the transaction and returns nil if -// function `f` returns nil. -// -// Note that, you should not Commit or Rollback the transaction in function `f` -// as it is automatically handled by this function. -func (c *Core) Transaction(f func(tx *TX) error) (err error) { - var tx *TX - tx, err = c.db.Begin() - if err != nil { - return err - } - defer func() { - if err == nil { - if e := recover(); e != nil { - err = fmt.Errorf("%v", e) - } - } - if err != nil { - if e := tx.Rollback(); e != nil { - err = e - } - } else { - if e := tx.Commit(); e != nil { - err = e - } - } - }() - err = f(tx) - return -} - // Insert does "INSERT INTO ..." statement for the table. // If there's already one unique record of the data in the table, it returns error. // @@ -465,6 +274,14 @@ func (c *Core) InsertIgnore(table string, data interface{}, batch ...int) (sql.R return c.Model(table).Data(data).InsertIgnore() } +// InsertAndGetId performs action Insert and returns the last insert id that automatically generated. +func (c *Core) InsertAndGetId(table string, data interface{}, batch ...int) (int64, error) { + if len(batch) > 0 { + return c.Model(table).Data(data).Batch(batch[0]).InsertAndGetId() + } + return c.Model(table).Data(data).InsertAndGetId() +} + // Replace does "REPLACE INTO ..." statement for the table. // If there's already one unique record of the data in the table, it deletes the record // and inserts a new one. @@ -502,7 +319,7 @@ func (c *Core) Save(table string, data interface{}, batch ...int) (sql.Result, e return c.Model(table).Data(data).Save() } -// doInsert inserts or updates data for given table. +// DoInsert inserts or updates data for given table. // This function is usually used for custom interface definition, you do not need call it manually. // The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. // Eg: @@ -514,8 +331,8 @@ func (c *Core) Save(table string, data interface{}, batch ...int) (sql.Result, e // 1: replace: if there's unique/primary key in the data, it deletes it from table and inserts a new one; // 2: save: if there's unique/primary key in the data, it updates it or else inserts a new one; // 3: ignore: if there's unique/primary key in the data, it ignores the inserting; -func (c *Core) DoInsert(link Link, table string, data interface{}, option int, batch ...int) (result sql.Result, err error) { - table = c.db.QuotePrefixTableName(table) +func (c *Core) DoInsert(ctx context.Context, link Link, table string, data interface{}, option int, batch ...int) (result sql.Result, err error) { + table = c.QuotePrefixTableName(table) var ( fields []string values []string @@ -530,10 +347,10 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b } switch reflectKind { case reflect.Slice, reflect.Array: - return c.db.DoBatchInsert(link, table, data, option, batch...) + return c.db.DoBatchInsert(ctx, link, table, data, option, batch...) case reflect.Struct: if _, ok := data.(apiInterfaces); ok { - return c.db.DoBatchInsert(link, table, data, option, batch...) + return c.db.DoBatchInsert(ctx, link, table, data, option, batch...) } else { dataMap = ConvertDataForTableRecord(data) } @@ -578,19 +395,15 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b updateStr = fmt.Sprintf("ON DUPLICATE KEY UPDATE %s", updateStr) } if link == nil { - if link, err = c.db.Master(); err != nil { + if link, err = c.MasterLink(); err != nil { return nil, err } } - return c.db.DoExec( - link, - fmt.Sprintf( - "%s INTO %s(%s) VALUES(%s) %s", - operation, table, strings.Join(fields, ","), - strings.Join(values, ","), updateStr, - ), - params..., - ) + return c.db.DoExec(ctx, link, fmt.Sprintf( + "%s INTO %s(%s) VALUES(%s) %s", + operation, table, strings.Join(fields, ","), + strings.Join(values, ","), updateStr, + ), params...) } // BatchInsert batch inserts data. @@ -631,8 +444,8 @@ func (c *Core) BatchSave(table string, list interface{}, batch ...int) (sql.Resu // DoBatchInsert batch inserts/replaces/saves data. // This function is usually used for custom interface definition, you do not need call it manually. -func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) { - table = c.db.QuotePrefixTableName(table) +func (c *Core) DoBatchInsert(ctx context.Context, link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) { + table = c.QuotePrefixTableName(table) var ( keys []string // Field names. values []string // Value holder string array, like: (?,?,?) @@ -687,7 +500,7 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i return result, gerror.New("data list cannot be empty") } if link == nil { - if link, err = c.db.Master(); err != nil { + if link, err = c.MasterLink(); err != nil { return } } @@ -743,16 +556,12 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i } valueHolder = append(valueHolder, "("+gstr.Join(values, ",")+")") if len(valueHolder) == batchNum || (i == listMapLen-1 && len(valueHolder) > 0) { - r, err := c.db.DoExec( - link, - fmt.Sprintf( - "%s INTO %s(%s) VALUES%s %s", - operation, table, keysStr, - gstr.Join(valueHolder, ","), - updateStr, - ), - params..., - ) + r, err := c.db.DoExec(ctx, link, fmt.Sprintf( + "%s INTO %s(%s) VALUES%s %s", + operation, table, keysStr, + gstr.Join(valueHolder, ","), + updateStr, + ), params...) if err != nil { return r, err } @@ -787,10 +596,10 @@ func (c *Core) Update(table string, data interface{}, condition interface{}, arg return c.Model(table).Data(data).Where(condition, args...).Update() } -// doUpdate does "UPDATE ... " statement for the table. +// DoUpdate does "UPDATE ... " statement for the table. // This function is usually used for custom interface definition, you do not need call it manually. -func (c *Core) DoUpdate(link Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) { - table = c.db.QuotePrefixTableName(table) +func (c *Core) DoUpdate(ctx context.Context, link Link, table string, data interface{}, condition string, args ...interface{}) (result sql.Result, err error) { + table = c.QuotePrefixTableName(table) var ( rv = reflect.ValueOf(data) kind = rv.Kind() @@ -813,24 +622,29 @@ func (c *Core) DoUpdate(link Link, table string, data interface{}, condition str switch value := v.(type) { case *Counter: if value.Value != 0 { - column := c.db.QuoteWord(value.Field) + column := k + if value.Field != "" { + column = c.QuoteWord(value.Field) + } fields = append(fields, fmt.Sprintf("%s=%s+?", column, column)) params = append(params, value.Value) } case Counter: if value.Value != 0 { - column := c.db.QuoteWord(value.Field) + column := k + if value.Field != "" { + column = c.QuoteWord(value.Field) + } fields = append(fields, fmt.Sprintf("%s=%s+?", column, column)) params = append(params, value.Value) } default: if s, ok := v.(Raw); ok { - fields = append(fields, c.db.QuoteWord(k)+"="+gconv.String(s)) + fields = append(fields, c.QuoteWord(k)+"="+gconv.String(s)) } else { - fields = append(fields, c.db.QuoteWord(k)+"=?") + fields = append(fields, c.QuoteWord(k)+"=?") params = append(params, v) } - } } updates = strings.Join(fields, ",") @@ -845,15 +659,11 @@ func (c *Core) DoUpdate(link Link, table string, data interface{}, condition str } // If no link passed, it then uses the master link. if link == nil { - if link, err = c.db.Master(); err != nil { + if link, err = c.MasterLink(); err != nil { return nil, err } } - return c.db.DoExec( - link, - fmt.Sprintf("UPDATE %s SET %s%s", table, updates, condition), - args..., - ) + return c.db.DoExec(ctx, link, fmt.Sprintf("UPDATE %s SET %s%s", table, updates, condition), args...) } // Delete does "DELETE FROM ... " statement for the table. @@ -873,14 +683,14 @@ func (c *Core) Delete(table string, condition interface{}, args ...interface{}) // DoDelete does "DELETE FROM ... " statement for the table. // This function is usually used for custom interface definition, you do not need call it manually. -func (c *Core) DoDelete(link Link, table string, condition string, args ...interface{}) (result sql.Result, err error) { +func (c *Core) DoDelete(ctx context.Context, link Link, table string, condition string, args ...interface{}) (result sql.Result, err error) { if link == nil { - if link, err = c.db.Master(); err != nil { + if link, err = c.MasterLink(); err != nil { return nil, err } } - table = c.db.QuotePrefixTableName(table) - return c.db.DoExec(link, fmt.Sprintf("DELETE FROM %s%s", table, condition), args...) + table = c.QuotePrefixTableName(table) + return c.db.DoExec(ctx, link, fmt.Sprintf("DELETE FROM %s%s", table, condition), args...) } // convertRowsToResult converts underlying data record type sql.Rows to Result type. @@ -916,7 +726,7 @@ func (c *Core) convertRowsToResult(rows *sql.Rows) (Result, error) { if value == nil { row[columnNames[i]] = gvar.New(nil) } else { - row[columnNames[i]] = gvar.New(c.db.convertFieldValueToLocalValue(value, columnTypes[i])) + row[columnNames[i]] = gvar.New(c.convertFieldValueToLocalValue(value, columnTypes[i])) } } records = append(records, row) @@ -938,19 +748,25 @@ func (c *Core) MarshalJSON() ([]byte, error) { // writeSqlToLogger outputs the sql object to logger. // It is enabled only if configuration "debug" is true. -func (c *Core) writeSqlToLogger(v *Sql) { - s := fmt.Sprintf("[%3d ms] [%s] %s", v.End-v.Start, v.Group, v.Format) - if v.Error != nil { - s += "\nError: " + v.Error.Error() - c.logger.Ctx(c.db.GetCtx()).Error(s) +func (c *Core) writeSqlToLogger(ctx context.Context, sql *Sql) { + var transactionIdStr string + if sql.IsTransaction { + if v := ctx.Value(transactionIdForLoggerCtx); v != nil { + transactionIdStr = fmt.Sprintf(`[%d] `, v.(uint64)) + } + } + s := fmt.Sprintf("[%3d ms] [%s] %s%s", sql.End-sql.Start, sql.Group, transactionIdStr, sql.Format) + if sql.Error != nil { + s += "\nError: " + sql.Error.Error() + c.logger.Ctx(ctx).Error(s) } else { - c.logger.Ctx(c.db.GetCtx()).Debug(s) + c.logger.Ctx(ctx).Debug(s) } } // HasTable determine whether the table name exists in the database. func (c *Core) HasTable(name string) (bool, error) { - tableList, err := c.db.Tables() + tableList, err := c.db.Tables(c.GetCtx()) if err != nil { return false, err } diff --git a/database/gdb/gdb_core_config.go b/database/gdb/gdb_core_config.go index ab222cf44..8ce85dc8e 100644 --- a/database/gdb/gdb_core_config.go +++ b/database/gdb/gdb_core_config.go @@ -8,15 +8,12 @@ package gdb import ( "fmt" - "github.com/gogf/gf/os/gcache" "sync" "time" - "github.com/gogf/gf/os/glog" -) + "github.com/gogf/gf/os/gcache" -const ( - DefaultGroupName = "default" // Default group name. + "github.com/gogf/gf/os/glog" ) // Config is the configuration management object. @@ -42,7 +39,7 @@ type ConfigNode struct { LinkInfo string `json:"link"` // (Optional) Custom link information, when it is used, configuration Host/Port/User/Pass/Name are ignored. MaxIdleConnCount int `json:"maxIdle"` // (Optional) Max idle connection configuration for underlying connection pool. MaxOpenConnCount int `json:"maxOpen"` // (Optional) Max open connection configuration for underlying connection pool. - MaxConnLifetime time.Duration `json:"maxLifetime"` // (Optional) Max connection TTL configuration for underlying connection pool. + MaxConnLifeTime time.Duration `json:"maxLifeTime"` // (Optional) Max amount of time a connection may be idle before being closed. QueryTimeout time.Duration `json:"queryTimeout"` // (Optional) Max query time for per dql. ExecTimeout time.Duration `json:"execTimeout"` // (Optional) Max exec time for dml. TranTimeout time.Duration `json:"tranTimeout"` // (Optional) Max exec time time for a transaction. @@ -53,6 +50,10 @@ type ConfigNode struct { TimeMaintainDisabled bool `json:"timeMaintainDisabled"` // (Optional) Disable the automatic time maintaining feature. } +const ( + DefaultGroupName = "default" // Default group name. +) + // configs is internal used configuration object. var configs struct { sync.RWMutex @@ -141,20 +142,39 @@ func (c *Core) GetLogger() *glog.Logger { return c.logger } -// SetMaxIdleConnCount sets the max idle connection count for underlying connection pool. +// SetMaxIdleConnCount sets the maximum number of connections in the idle +// connection pool. +// +// If MaxOpenConns is greater than 0 but less than the new MaxIdleConns, +// then the new MaxIdleConns will be reduced to match the MaxOpenConns limit. +// +// If n <= 0, no idle connections are retained. +// +// The default max idle connections is currently 2. This may change in +// a future release. func (c *Core) SetMaxIdleConnCount(n int) { c.config.MaxIdleConnCount = n } -// SetMaxOpenConnCount sets the max open connection count for underlying connection pool. +// SetMaxOpenConnCount sets the maximum number of open connections to the database. +// +// If MaxIdleConns is greater than 0 and the new MaxOpenConns is less than +// MaxIdleConns, then MaxIdleConns will be reduced to match the new +// MaxOpenConns limit. +// +// If n <= 0, then there is no limit on the number of open connections. +// The default is 0 (unlimited). func (c *Core) SetMaxOpenConnCount(n int) { c.config.MaxOpenConnCount = n } -// SetMaxConnLifetime sets the connection TTL for underlying connection pool. -// If parameter `d` <= 0, it means the connection never expires. -func (c *Core) SetMaxConnLifetime(d time.Duration) { - c.config.MaxConnLifetime = d +// SetMaxConnLifeTime sets the maximum amount of time a connection may be reused. +// +// Expired connections may be closed lazily before reuse. +// +// If d <= 0, connections are not closed due to a connection's age. +func (c *Core) SetMaxConnLifeTime(d time.Duration) { + c.config.MaxConnLifeTime = d } // String returns the node as string. @@ -165,7 +185,7 @@ func (node *ConfigNode) String() string { node.Name, node.Type, node.Role, node.Charset, node.Debug, node.MaxIdleConnCount, node.MaxOpenConnCount, - node.MaxConnLifetime, + node.MaxConnLifeTime, node.LinkInfo, ) } diff --git a/database/gdb/gdb_core_link.go b/database/gdb/gdb_core_link.go new file mode 100644 index 000000000..06ea3190b --- /dev/null +++ b/database/gdb/gdb_core_link.go @@ -0,0 +1,31 @@ +// 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 gdb + +import ( + "database/sql" +) + +// dbLink is used to implement interface Link for DB. +type dbLink struct { + *sql.DB +} + +// txLink is used to implement interface Link for TX. +type txLink struct { + *sql.Tx +} + +// IsTransaction returns if current Link is a transaction. +func (*dbLink) IsTransaction() bool { + return false +} + +// IsTransaction returns if current Link is a transaction. +func (*txLink) IsTransaction() bool { + return true +} diff --git a/database/gdb/gdb_core_structure.go b/database/gdb/gdb_core_structure.go index de5b9e770..e7aa76b67 100644 --- a/database/gdb/gdb_core_structure.go +++ b/database/gdb/gdb_core_structure.go @@ -7,10 +7,11 @@ package gdb import ( - "github.com/gogf/gf/util/gutil" "strings" "time" + "github.com/gogf/gf/util/gutil" + "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/os/gtime" @@ -99,7 +100,8 @@ func (c *Core) convertFieldValueToLocalValue(fieldValue interface{}, fieldType s case "datetime", - "timestamp": + "timestamp", + "timestamptz": if t, ok := fieldValue.(time.Time); ok { return gtime.NewFromTime(t) } @@ -149,7 +151,7 @@ func (c *Core) convertFieldValueToLocalValue(fieldValue interface{}, fieldType s // mappingAndFilterData automatically mappings the map key to table field and removes // all key-value pairs that are not the field of given table. func (c *Core) mappingAndFilterData(schema, table string, data map[string]interface{}, filter bool) (map[string]interface{}, error) { - if fieldsMap, err := c.db.TableFields(table, schema); err == nil { + if fieldsMap, err := c.db.TableFields(c.GetCtx(), table, schema); err == nil { fieldsKeyMap := make(map[string]interface{}, len(fieldsMap)) for k, _ := range fieldsMap { fieldsKeyMap[k] = nil diff --git a/database/gdb/gdb_core_transaction.go b/database/gdb/gdb_core_transaction.go new file mode 100644 index 000000000..d9e575ce8 --- /dev/null +++ b/database/gdb/gdb_core_transaction.go @@ -0,0 +1,573 @@ +// 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 gdb + +import ( + "context" + "database/sql" + "fmt" + "reflect" + + "github.com/gogf/gf/container/gtype" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/guid" + + "github.com/gogf/gf/text/gregex" +) + +// TX is the struct for transaction management. +type TX struct { + db DB // db is the current gdb database manager. + tx *sql.Tx // tx is the raw and underlying transaction manager. + ctx context.Context // ctx is the context for this transaction only. + master *sql.DB // master is the raw and underlying database manager. + transactionId string // transactionId is an unique id generated by this object for this transaction. + transactionCount int // transactionCount marks the times that Begins. +} + +const ( + transactionPointerPrefix = "transaction" + contextTransactionKeyPrefix = "TransactionObjectForGroup_" + transactionIdForLoggerCtx = "TransactionId" +) + +var ( + transactionIdGenerator = gtype.NewUint64() +) + +// Begin starts and returns the transaction object. +// You should call Commit or Rollback functions of the transaction object +// if you no longer use the transaction. Commit or Rollback functions will also +// close the transaction automatically. +func (c *Core) Begin() (tx *TX, err error) { + return c.doBeginCtx(c.GetCtx()) +} + +func (c *Core) doBeginCtx(ctx context.Context) (*TX, error) { + if master, err := c.db.Master(); err != nil { + return nil, err + } else { + var ( + tx *TX + sqlStr = "BEGIN" + mTime1 = gtime.TimestampMilli() + rawTx, err = master.Begin() + mTime2 = gtime.TimestampMilli() + sqlObj = &Sql{ + Sql: sqlStr, + Type: "DB.Begin", + Args: nil, + Format: sqlStr, + Error: err, + Start: mTime1, + End: mTime2, + Group: c.db.GetGroup(), + IsTransaction: true, + } + ) + if err == nil { + tx = &TX{ + db: c.db, + tx: rawTx, + ctx: context.WithValue(ctx, transactionIdForLoggerCtx, transactionIdGenerator.Add(1)), + master: master, + transactionId: guid.S(), + } + ctx = tx.ctx + } + // Tracing and logging. + c.addSqlToTracing(ctx, sqlObj) + if c.db.GetDebug() { + c.writeSqlToLogger(ctx, sqlObj) + } + return tx, err + } +} + +// Transaction wraps the transaction logic using function `f`. +// It rollbacks the transaction and returns the error from function `f` if +// it returns non-nil error. It commits the transaction and returns nil if +// function `f` returns nil. +// +// Note that, you should not Commit or Rollback the transaction in function `f` +// as it is automatically handled by this function. +func (c *Core) Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) (err error) { + var ( + tx *TX + ) + if ctx == nil { + ctx = c.GetCtx() + } + // Check transaction object from context. + tx = TXFromCtx(ctx, c.db.GetGroup()) + if tx != nil { + return tx.Transaction(ctx, f) + } + tx, err = c.doBeginCtx(ctx) + if err != nil { + return err + } + // Inject transaction object into context. + tx.ctx = WithTX(tx.ctx, tx) + defer func() { + if err == nil { + if e := recover(); e != nil { + err = fmt.Errorf("%v", e) + } + } + if err != nil { + if e := tx.Rollback(); e != nil { + err = e + } + } else { + if e := tx.Commit(); e != nil { + err = e + } + } + }() + err = f(tx.ctx, tx) + return +} + +// WithTX injects given transaction object into context and returns a new context. +func WithTX(ctx context.Context, tx *TX) context.Context { + if tx == nil { + return ctx + } + // Check repeat injection from given. + group := tx.db.GetGroup() + if tx := TXFromCtx(ctx, group); tx != nil && tx.db.GetGroup() == group { + return ctx + } + dbCtx := tx.db.GetCtx() + if tx := TXFromCtx(dbCtx, group); tx != nil && tx.db.GetGroup() == group { + return dbCtx + } + // Inject transaction object and id into context. + ctx = context.WithValue(ctx, transactionKeyForContext(group), tx) + return ctx +} + +// TXFromCtx retrieves and returns transaction object from context. +// It is usually used in nested transaction feature, and it returns nil if it is not set previously. +func TXFromCtx(ctx context.Context, group string) *TX { + if ctx == nil { + return nil + } + v := ctx.Value(transactionKeyForContext(group)) + if v != nil { + tx := v.(*TX) + tx.ctx = ctx + return tx + } + return nil +} + +// transactionKeyForContext forms and returns a string for storing transaction object of certain database group into context. +func transactionKeyForContext(group string) string { + return contextTransactionKeyPrefix + group +} + +// transactionKeyForNestedPoint forms and returns the transaction key at current save point. +func (tx *TX) transactionKeyForNestedPoint() string { + return tx.db.GetCore().QuoteWord(transactionPointerPrefix + gconv.String(tx.transactionCount)) +} + +// Ctx sets the context for current transaction. +func (tx *TX) Ctx(ctx context.Context) *TX { + tx.ctx = ctx + return tx +} + +// Commit commits current transaction. +// Note that it releases previous saved transaction point if it's in a nested transaction procedure, +// or else it commits the hole transaction. +func (tx *TX) Commit() error { + if tx.transactionCount > 0 { + tx.transactionCount-- + _, err := tx.Exec("RELEASE SAVEPOINT " + tx.transactionKeyForNestedPoint()) + return err + } + var ( + sqlStr = "COMMIT" + mTime1 = gtime.TimestampMilli() + err = tx.tx.Commit() + mTime2 = gtime.TimestampMilli() + sqlObj = &Sql{ + Sql: sqlStr, + Type: "TX.Commit", + Args: nil, + Format: sqlStr, + Error: err, + Start: mTime1, + End: mTime2, + Group: tx.db.GetGroup(), + IsTransaction: true, + } + ) + tx.db.GetCore().addSqlToTracing(tx.ctx, sqlObj) + if tx.db.GetDebug() { + tx.db.GetCore().writeSqlToLogger(tx.ctx, sqlObj) + } + return err +} + +// Rollback aborts current transaction. +// Note that it aborts current transaction if it's in a nested transaction procedure, +// or else it aborts the hole transaction. +func (tx *TX) Rollback() error { + if tx.transactionCount > 0 { + tx.transactionCount-- + _, err := tx.Exec("ROLLBACK TO SAVEPOINT " + tx.transactionKeyForNestedPoint()) + return err + } + var ( + sqlStr = "ROLLBACK" + mTime1 = gtime.TimestampMilli() + err = tx.tx.Rollback() + mTime2 = gtime.TimestampMilli() + sqlObj = &Sql{ + Sql: sqlStr, + Type: "TX.Rollback", + Args: nil, + Format: sqlStr, + Error: err, + Start: mTime1, + End: mTime2, + Group: tx.db.GetGroup(), + IsTransaction: true, + } + ) + tx.db.GetCore().addSqlToTracing(tx.ctx, sqlObj) + if tx.db.GetDebug() { + tx.db.GetCore().writeSqlToLogger(tx.ctx, sqlObj) + } + return err +} + +// Begin starts a nested transaction procedure. +func (tx *TX) Begin() error { + _, err := tx.Exec("SAVEPOINT " + tx.transactionKeyForNestedPoint()) + if err != nil { + return err + } + tx.transactionCount++ + return nil +} + +// SavePoint performs `SAVEPOINT xxx` SQL statement that saves transaction at current point. +// The parameter `point` specifies the point name that will be saved to server. +func (tx *TX) SavePoint(point string) error { + _, err := tx.Exec("SAVEPOINT " + tx.db.GetCore().QuoteWord(point)) + return err +} + +// RollbackTo performs `ROLLBACK TO SAVEPOINT xxx` SQL statement that rollbacks to specified saved transaction. +// The parameter `point` specifies the point name that was saved previously. +func (tx *TX) RollbackTo(point string) error { + _, err := tx.Exec("ROLLBACK TO SAVEPOINT " + tx.db.GetCore().QuoteWord(point)) + return err +} + +// Transaction wraps the transaction logic using function `f`. +// It rollbacks the transaction and returns the error from function `f` if +// it returns non-nil error. It commits the transaction and returns nil if +// function `f` returns nil. +// +// Note that, you should not Commit or Rollback the transaction in function `f` +// as it is automatically handled by this function. +func (tx *TX) Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) (err error) { + if ctx != nil { + tx.ctx = ctx + } + // Check transaction object from context. + if TXFromCtx(tx.ctx, tx.db.GetGroup()) == nil { + // Inject transaction object into context. + tx.ctx = WithTX(tx.ctx, tx) + } + err = tx.Begin() + if err != nil { + return err + } + defer func() { + if err == nil { + if e := recover(); e != nil { + err = fmt.Errorf("%v", e) + } + } + if err != nil { + if e := tx.Rollback(); e != nil { + err = e + } + } else { + if e := tx.Commit(); e != nil { + err = e + } + } + }() + err = f(tx.ctx, tx) + return +} + +// Query does query operation on transaction. +// See Core.Query. +func (tx *TX) Query(sql string, args ...interface{}) (rows *sql.Rows, err error) { + return tx.db.DoQuery(tx.ctx, &txLink{tx.tx}, sql, args...) +} + +// Exec does none query operation on transaction. +// See Core.Exec. +func (tx *TX) Exec(sql string, args ...interface{}) (sql.Result, error) { + return tx.db.DoExec(tx.ctx, &txLink{tx.tx}, sql, args...) +} + +// Prepare creates a prepared statement for later queries or executions. +// Multiple queries or executions may be run concurrently from the +// returned statement. +// The caller must call the statement's Close method +// when the statement is no longer needed. +func (tx *TX) Prepare(sql string) (*Stmt, error) { + return tx.db.DoPrepare(tx.ctx, &txLink{tx.tx}, sql) +} + +// GetAll queries and returns data records from database. +func (tx *TX) GetAll(sql string, args ...interface{}) (Result, error) { + rows, err := tx.Query(sql, args...) + if err != nil || rows == nil { + return nil, err + } + defer rows.Close() + return tx.db.GetCore().convertRowsToResult(rows) +} + +// GetOne queries and returns one record from database. +func (tx *TX) GetOne(sql string, args ...interface{}) (Record, error) { + list, err := tx.GetAll(sql, args...) + if err != nil { + return nil, err + } + if len(list) > 0 { + return list[0], nil + } + return nil, nil +} + +// GetStruct queries one record from database and converts it to given struct. +// The parameter `pointer` should be a pointer to struct. +func (tx *TX) GetStruct(obj interface{}, sql string, args ...interface{}) error { + one, err := tx.GetOne(sql, args...) + if err != nil { + return err + } + return one.Struct(obj) +} + +// GetStructs queries records from database and converts them to given struct. +// The parameter `pointer` should be type of struct slice: []struct/[]*struct. +func (tx *TX) GetStructs(objPointerSlice interface{}, sql string, args ...interface{}) error { + all, err := tx.GetAll(sql, args...) + if err != nil { + return err + } + return all.Structs(objPointerSlice) +} + +// GetScan queries one or more records from database and converts them to given struct or +// struct array. +// +// If parameter `pointer` is type of struct pointer, it calls GetStruct internally for +// the conversion. If parameter `pointer` is type of slice, it calls GetStructs internally +// for conversion. +func (tx *TX) GetScan(pointer interface{}, sql string, args ...interface{}) error { + t := reflect.TypeOf(pointer) + k := t.Kind() + if k != reflect.Ptr { + return fmt.Errorf("params should be type of pointer, but got: %v", k) + } + k = t.Elem().Kind() + switch k { + case reflect.Array, reflect.Slice: + return tx.GetStructs(pointer, sql, args...) + case reflect.Struct: + return tx.GetStruct(pointer, sql, args...) + default: + return fmt.Errorf("element type should be type of struct/slice, unsupported: %v", k) + } +} + +// GetValue queries and returns the field value from database. +// The sql should queries only one field from database, or else it returns only one +// field of the result. +func (tx *TX) GetValue(sql string, args ...interface{}) (Value, error) { + one, err := tx.GetOne(sql, args...) + if err != nil { + return nil, err + } + for _, v := range one { + return v, nil + } + return nil, nil +} + +// GetCount queries and returns the count from database. +func (tx *TX) GetCount(sql string, args ...interface{}) (int, error) { + if !gregex.IsMatchString(`(?i)SELECT\s+COUNT\(.+\)\s+FROM`, sql) { + sql, _ = gregex.ReplaceString(`(?i)(SELECT)\s+(.+)\s+(FROM)`, `$1 COUNT($2) $3`, sql) + } + value, err := tx.GetValue(sql, args...) + if err != nil { + return 0, err + } + return value.Int(), nil +} + +// Insert does "INSERT INTO ..." statement for the table. +// If there's already one unique record of the data in the table, it returns error. +// +// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. +// Eg: +// Data(g.Map{"uid": 10000, "name":"john"}) +// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) +// +// The parameter `batch` specifies the batch operation count when given data is slice. +func (tx *TX) Insert(table string, data interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).Insert() + } + return tx.Model(table).Ctx(tx.ctx).Data(data).Insert() +} + +// InsertIgnore does "INSERT IGNORE INTO ..." statement for the table. +// If there's already one unique record of the data in the table, it ignores the inserting. +// +// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. +// Eg: +// Data(g.Map{"uid": 10000, "name":"john"}) +// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) +// +// The parameter `batch` specifies the batch operation count when given data is slice. +func (tx *TX) InsertIgnore(table string, data interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).InsertIgnore() + } + return tx.Model(table).Ctx(tx.ctx).Data(data).InsertIgnore() +} + +// InsertAndGetId performs action Insert and returns the last insert id that automatically generated. +func (tx *TX) InsertAndGetId(table string, data interface{}, batch ...int) (int64, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).InsertAndGetId() + } + return tx.Model(table).Ctx(tx.ctx).Data(data).InsertAndGetId() +} + +// Replace does "REPLACE INTO ..." statement for the table. +// If there's already one unique record of the data in the table, it deletes the record +// and inserts a new one. +// +// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. +// Eg: +// Data(g.Map{"uid": 10000, "name":"john"}) +// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) +// +// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. +// If given data is type of slice, it then does batch replacing, and the optional parameter +// `batch` specifies the batch operation count. +func (tx *TX) Replace(table string, data interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).Replace() + } + return tx.Model(table).Ctx(tx.ctx).Data(data).Replace() +} + +// Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the table. +// It updates the record if there's primary or unique index in the saving data, +// or else it inserts a new record into the table. +// +// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. +// Eg: +// Data(g.Map{"uid": 10000, "name":"john"}) +// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) +// +// If given data is type of slice, it then does batch saving, and the optional parameter +// `batch` specifies the batch operation count. +func (tx *TX) Save(table string, data interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).Save() + } + return tx.Model(table).Ctx(tx.ctx).Data(data).Save() +} + +// BatchInsert batch inserts data. +// The parameter `list` must be type of slice of map or struct. +func (tx *TX) BatchInsert(table string, list interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(list).Batch(batch[0]).Insert() + } + return tx.Model(table).Ctx(tx.ctx).Data(list).Insert() +} + +// BatchInsertIgnore batch inserts data with ignore option. +// The parameter `list` must be type of slice of map or struct. +func (tx *TX) BatchInsertIgnore(table string, list interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(list).Batch(batch[0]).InsertIgnore() + } + return tx.Model(table).Ctx(tx.ctx).Data(list).InsertIgnore() +} + +// BatchReplace batch replaces data. +// The parameter `list` must be type of slice of map or struct. +func (tx *TX) BatchReplace(table string, list interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(list).Batch(batch[0]).Replace() + } + return tx.Model(table).Ctx(tx.ctx).Data(list).Replace() +} + +// BatchSave batch replaces data. +// The parameter `list` must be type of slice of map or struct. +func (tx *TX) BatchSave(table string, list interface{}, batch ...int) (sql.Result, error) { + if len(batch) > 0 { + return tx.Model(table).Ctx(tx.ctx).Data(list).Batch(batch[0]).Save() + } + return tx.Model(table).Ctx(tx.ctx).Data(list).Save() +} + +// Update does "UPDATE ... " statement for the table. +// +// The parameter `data` can be type of string/map/gmap/struct/*struct, etc. +// Eg: "uid=10000", "uid", 10000, g.Map{"uid": 10000, "name":"john"} +// +// The parameter `condition` can be type of string/map/gmap/slice/struct/*struct, etc. +// It is commonly used with parameter `args`. +// Eg: +// "uid=10000", +// "uid", 10000 +// "money>? AND name like ?", 99999, "vip_%" +// "status IN (?)", g.Slice{1,2,3} +// "age IN(?,?)", 18, 50 +// User{ Id : 1, UserName : "john"} +func (tx *TX) Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) { + return tx.Model(table).Ctx(tx.ctx).Data(data).Where(condition, args...).Update() +} + +// Delete does "DELETE FROM ... " statement for the table. +// +// The parameter `condition` can be type of string/map/gmap/slice/struct/*struct, etc. +// It is commonly used with parameter `args`. +// Eg: +// "uid=10000", +// "uid", 10000 +// "money>? AND name like ?", 99999, "vip_%" +// "status IN (?)", g.Slice{1,2,3} +// "age IN(?,?)", 18, 50 +// User{ Id : 1, UserName : "john"} +func (tx *TX) Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) { + return tx.Model(table).Ctx(tx.ctx).Where(condition, args...).Delete() +} diff --git a/database/gdb/gdb_core_underlying.go b/database/gdb/gdb_core_underlying.go new file mode 100644 index 000000000..baa0e5027 --- /dev/null +++ b/database/gdb/gdb_core_underlying.go @@ -0,0 +1,186 @@ +// 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 gdb + +import ( + "context" + "database/sql" + + "github.com/gogf/gf/os/gtime" +) + +// Query commits one query SQL to underlying driver and returns the execution result. +// It is most commonly used for data querying. +func (c *Core) Query(sql string, args ...interface{}) (rows *sql.Rows, err error) { + return c.db.DoQuery(c.GetCtx(), nil, sql, args...) +} + +// DoQuery commits the sql string and its arguments to underlying driver +// through given link object and returns the execution result. +func (c *Core) DoQuery(ctx context.Context, link Link, sql string, args ...interface{}) (rows *sql.Rows, err error) { + // Transaction checks. + if link == nil { + if link, err = c.SlaveLink(); err != nil { + return nil, err + } + } else if !link.IsTransaction() { + if tx := TXFromCtx(ctx, c.db.GetGroup()); tx != nil { + link = &txLink{tx.tx} + } + } + // Link execution. + sql, args = formatSql(sql, args) + sql, args = c.db.HandleSqlBeforeCommit(ctx, link, sql, args) + if c.GetConfig().QueryTimeout > 0 { + ctx, _ = context.WithTimeout(ctx, c.GetConfig().QueryTimeout) + } + mTime1 := gtime.TimestampMilli() + rows, err = link.QueryContext(ctx, sql, args...) + mTime2 := gtime.TimestampMilli() + sqlObj := &Sql{ + Sql: sql, + Type: "DB.QueryContext", + Args: args, + Format: FormatSqlWithArgs(sql, args), + Error: err, + Start: mTime1, + End: mTime2, + Group: c.db.GetGroup(), + IsTransaction: link.IsTransaction(), + } + // Tracing and logging. + c.addSqlToTracing(ctx, sqlObj) + if c.db.GetDebug() { + c.writeSqlToLogger(ctx, sqlObj) + } + if err == nil { + return rows, nil + } else { + err = formatError(err, sql, args...) + } + return nil, err +} + +// Exec commits one query SQL to underlying driver and returns the execution result. +// It is most commonly used for data inserting and updating. +func (c *Core) Exec(sql string, args ...interface{}) (result sql.Result, err error) { + return c.db.DoExec(c.GetCtx(), nil, sql, args...) +} + +// DoExec commits the sql string and its arguments to underlying driver +// through given link object and returns the execution result. +func (c *Core) DoExec(ctx context.Context, link Link, sql string, args ...interface{}) (result sql.Result, err error) { + // Transaction checks. + if link == nil { + if link, err = c.MasterLink(); err != nil { + return nil, err + } + } else if !link.IsTransaction() { + if tx := TXFromCtx(ctx, c.db.GetGroup()); tx != nil { + link = &txLink{tx.tx} + } + } + // Link execution. + sql, args = formatSql(sql, args) + sql, args = c.db.HandleSqlBeforeCommit(ctx, link, sql, args) + if c.GetConfig().ExecTimeout > 0 { + var cancelFunc context.CancelFunc + ctx, cancelFunc = context.WithTimeout(ctx, c.GetConfig().ExecTimeout) + defer cancelFunc() + } + + mTime1 := gtime.TimestampMilli() + if !c.db.GetDryRun() { + result, err = link.ExecContext(ctx, sql, args...) + } else { + result = new(SqlResult) + } + mTime2 := gtime.TimestampMilli() + sqlObj := &Sql{ + Sql: sql, + Type: "DB.ExecContext", + Args: args, + Format: FormatSqlWithArgs(sql, args), + Error: err, + Start: mTime1, + End: mTime2, + Group: c.db.GetGroup(), + IsTransaction: link.IsTransaction(), + } + // Tracing and logging. + c.addSqlToTracing(ctx, sqlObj) + if c.db.GetDebug() { + c.writeSqlToLogger(ctx, sqlObj) + } + return result, formatError(err, sql, args...) +} + +// Prepare creates a prepared statement for later queries or executions. +// Multiple queries or executions may be run concurrently from the +// returned statement. +// The caller must call the statement's Close method +// when the statement is no longer needed. +// +// The parameter `execOnMaster` specifies whether executing the sql on master node, +// or else it executes the sql on slave node if master-slave configured. +func (c *Core) Prepare(sql string, execOnMaster ...bool) (*Stmt, error) { + var ( + err error + link Link + ) + if len(execOnMaster) > 0 && execOnMaster[0] { + if link, err = c.MasterLink(); err != nil { + return nil, err + } + } else { + if link, err = c.SlaveLink(); err != nil { + return nil, err + } + } + return c.db.DoPrepare(c.GetCtx(), link, sql) +} + +// DoPrepare calls prepare function on given link object and returns the statement object. +func (c *Core) DoPrepare(ctx context.Context, link Link, sql string) (*Stmt, error) { + if link != nil && !link.IsTransaction() { + if tx := TXFromCtx(ctx, c.db.GetGroup()); tx != nil { + link = &txLink{tx.tx} + } + } + if c.GetConfig().PrepareTimeout > 0 { + // DO NOT USE cancel function in prepare statement. + ctx, _ = context.WithTimeout(ctx, c.GetConfig().PrepareTimeout) + } + var ( + mTime1 = gtime.TimestampMilli() + stmt, err = link.PrepareContext(ctx, sql) + mTime2 = gtime.TimestampMilli() + sqlObj = &Sql{ + Sql: sql, + Type: "DB.PrepareContext", + Args: nil, + Format: FormatSqlWithArgs(sql, nil), + Error: err, + Start: mTime1, + End: mTime2, + Group: c.db.GetGroup(), + IsTransaction: link.IsTransaction(), + } + ) + // Tracing and logging. + c.addSqlToTracing(ctx, sqlObj) + if c.db.GetDebug() { + c.writeSqlToLogger(ctx, sqlObj) + } + return &Stmt{ + Stmt: stmt, + core: c, + link: link, + sql: sql, + }, err +} diff --git a/database/gdb/gdb_core_utility.go b/database/gdb/gdb_core_utility.go index 3528d873e..0eaeb16c4 100644 --- a/database/gdb/gdb_core_utility.go +++ b/database/gdb/gdb_core_utility.go @@ -7,22 +7,26 @@ package gdb -import ( - "database/sql" -) - -// GetMaster acts like function Master but with additional `schema` parameter specifying +// MasterLink acts like function Master but with additional `schema` parameter specifying // the schema for the connection. It is defined for internal usage. // Also see Master. -func (c *Core) GetMaster(schema ...string) (*sql.DB, error) { - return c.getSqlDb(true, schema...) +func (c *Core) MasterLink(schema ...string) (Link, error) { + db, err := c.db.Master(schema...) + if err != nil { + return nil, err + } + return &dbLink{db}, nil } -// GetSlave acts like function Slave but with additional `schema` parameter specifying +// SlaveLink acts like function Slave but with additional `schema` parameter specifying // the schema for the connection. It is defined for internal usage. // Also see Slave. -func (c *Core) GetSlave(schema ...string) (*sql.DB, error) { - return c.getSqlDb(false, schema...) +func (c *Core) SlaveLink(schema ...string) (Link, error) { + db, err := c.db.Slave(schema...) + if err != nil { + return nil, err + } + return &dbLink{db}, nil } // QuoteWord checks given string `s` a word, if true quotes it with security chars of the database diff --git a/database/gdb/gdb_driver_mssql.go b/database/gdb/gdb_driver_mssql.go index 6ba855489..33d3f8732 100644 --- a/database/gdb/gdb_driver_mssql.go +++ b/database/gdb/gdb_driver_mssql.go @@ -12,12 +12,14 @@ package gdb import ( + "context" "database/sql" "fmt" - "github.com/gogf/gf/errors/gerror" "strconv" "strings" + "github.com/gogf/gf/errors/gerror" + "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/text/gstr" @@ -77,7 +79,7 @@ func (d *DriverMssql) GetChars() (charLeft string, charRight string) { } // HandleSqlBeforeCommit deals with the sql string before commits it to underlying sql driver. -func (d *DriverMssql) HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{}) { +func (d *DriverMssql) HandleSqlBeforeCommit(ctx context.Context, link Link, sql string, args []interface{}) (string, []interface{}) { var index int // Convert place holder char '?' to string "@px". str, _ := gregex.ReplaceStringFunc("\\?", sql, func(s string) string { @@ -183,14 +185,14 @@ func (d *DriverMssql) parseSql(sql string) string { // Tables retrieves and returns the tables of current schema. // It's mainly used in cli tool chain for automatically generating the models. -func (d *DriverMssql) Tables(schema ...string) (tables []string, err error) { +func (d *DriverMssql) Tables(ctx context.Context, schema ...string) (tables []string, err error) { var result Result - link, err := d.db.GetSlave(schema...) + link, err := d.SlaveLink(schema...) if err != nil { return nil, err } - result, err = d.db.DoGetAll(link, `SELECT NAME FROM SYSOBJECTS WHERE XTYPE='U' AND STATUS >= 0 ORDER BY NAME`) + result, err = d.DoGetAll(ctx, link, `SELECT NAME FROM SYSOBJECTS WHERE XTYPE='U' AND STATUS >= 0 ORDER BY NAME`) if err != nil { return } @@ -203,28 +205,31 @@ func (d *DriverMssql) Tables(schema ...string) (tables []string, err error) { } // TableFields retrieves and returns the fields information of specified table of current schema. -func (d *DriverMssql) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { +// +// Also see DriverMysql.TableFields. +func (d *DriverMssql) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*TableField, err error) { charL, charR := d.GetChars() table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { return nil, gerror.New("function TableFields supports only single table operations") } - checkSchema := d.db.GetSchema() + useSchema := d.db.GetSchema() if len(schema) > 0 && schema[0] != "" { - checkSchema = schema[0] + useSchema = schema[0] } - v, _ := internalCache.GetOrSetFunc( - fmt.Sprintf(`mssql_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()), - func() (interface{}, error) { - var ( - result Result - link *sql.DB - ) - link, err = d.db.GetSlave(checkSchema) - if err != nil { - return nil, err - } - structureSql := fmt.Sprintf(` + tableFieldsCacheKey := fmt.Sprintf( + `mssql_table_fields_%s_%s@group:%s`, + table, useSchema, d.GetGroup(), + ) + v := tableFieldsMap.GetOrSetFuncLock(tableFieldsCacheKey, func() interface{} { + var ( + result Result + link, err = d.SlaveLink(useSchema) + ) + if err != nil { + return nil + } + structureSql := fmt.Sprintf(` SELECT a.name Field, CASE b.name @@ -252,29 +257,29 @@ LEFT JOIN sys.extended_properties g ON a.id=g.major_id AND a.colid=g.minor_id LEFT JOIN sys.extended_properties f ON d.id=f.major_id AND f.minor_id =0 WHERE d.name='%s' ORDER BY a.id,a.colorder`, - strings.ToUpper(table), - ) - structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.db.DoGetAll(link, structureSql) - if err != nil { - return nil, err + strings.ToUpper(table), + ) + structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) + result, err = d.DoGetAll(ctx, link, structureSql) + if err != nil { + return nil + } + fields = make(map[string]*TableField) + for i, m := range result { + fields[strings.ToLower(m["Field"].String())] = &TableField{ + Index: i, + Name: strings.ToLower(m["Field"].String()), + Type: strings.ToLower(m["Type"].String()), + Null: m["Null"].Bool(), + Key: m["Key"].String(), + Default: m["Default"].Val(), + Extra: m["Extra"].String(), + Comment: m["Comment"].String(), } - fields = make(map[string]*TableField) - for i, m := range result { - fields[strings.ToLower(m["Field"].String())] = &TableField{ - Index: i, - Name: strings.ToLower(m["Field"].String()), - Type: strings.ToLower(m["Type"].String()), - Null: m["Null"].Bool(), - Key: m["Key"].String(), - Default: m["Default"].Val(), - Extra: m["Extra"].String(), - Comment: m["Comment"].String(), - } - } - return fields, nil - }, 0) - if err == nil { + } + return fields + }) + if v != nil { fields = v.(map[string]*TableField) } return diff --git a/database/gdb/gdb_driver_mysql.go b/database/gdb/gdb_driver_mysql.go index 5ac100350..aab15faec 100644 --- a/database/gdb/gdb_driver_mysql.go +++ b/database/gdb/gdb_driver_mysql.go @@ -7,14 +7,16 @@ package gdb import ( + "context" "database/sql" "fmt" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/text/gstr" - _ "github.com/go-sql-driver/mysql" + _ "github.com/gogf/mysql" ) // DriverMysql is the driver for mysql database. @@ -75,19 +77,19 @@ func (d *DriverMysql) GetChars() (charLeft string, charRight string) { } // HandleSqlBeforeCommit handles the sql before posts it to database. -func (d *DriverMysql) HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{}) { +func (d *DriverMysql) HandleSqlBeforeCommit(ctx context.Context, link Link, sql string, args []interface{}) (string, []interface{}) { return sql, args } // Tables retrieves and returns the tables of current schema. // It's mainly used in cli tool chain for automatically generating the models. -func (d *DriverMysql) Tables(schema ...string) (tables []string, err error) { +func (d *DriverMysql) Tables(ctx context.Context, schema ...string) (tables []string, err error) { var result Result - link, err := d.db.GetSlave(schema...) + link, err := d.SlaveLink(schema...) if err != nil { return nil, err } - result, err = d.db.DoGetAll(link, `SHOW TABLES`) + result, err = d.DoGetAll(ctx, link, `SHOW TABLES`) if err != nil { return } @@ -102,56 +104,57 @@ func (d *DriverMysql) Tables(schema ...string) (tables []string, err error) { // TableFields retrieves and returns the fields information of specified table of current // schema. // +// The parameter `link` is optional, if given nil it automatically retrieves a raw sql connection +// as its link to proceed necessary sql query. +// // Note that it returns a map containing the field name and its corresponding fields. // As a map is unsorted, the TableField struct has a "Index" field marks its sequence in // the fields. // // It's using cache feature to enhance the performance, which is never expired util the // process restarts. -func (d *DriverMysql) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { +func (d *DriverMysql) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*TableField, err error) { charL, charR := d.GetChars() table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { return nil, gerror.New("function TableFields supports only single table operations") } - checkSchema := d.schema.Val() + useSchema := d.schema.Val() if len(schema) > 0 && schema[0] != "" { - checkSchema = schema[0] + useSchema = schema[0] } - v, _ := internalCache.GetOrSetFunc( - fmt.Sprintf(`mysql_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()), - func() (interface{}, error) { - var ( - result Result - link *sql.DB - ) - link, err = d.db.GetSlave(checkSchema) - if err != nil { - return nil, err + tableFieldsCacheKey := fmt.Sprintf( + `mysql_table_fields_%s_%s@group:%s`, + table, useSchema, d.GetGroup(), + ) + v := tableFieldsMap.GetOrSetFuncLock(tableFieldsCacheKey, func() interface{} { + var ( + result Result + link, err = d.SlaveLink(useSchema) + ) + if err != nil { + return nil + } + result, err = d.DoGetAll(ctx, link, fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.QuoteWord(table))) + if err != nil { + return nil + } + fields = make(map[string]*TableField) + for i, m := range result { + fields[m["Field"].String()] = &TableField{ + Index: i, + Name: m["Field"].String(), + Type: m["Type"].String(), + Null: m["Null"].Bool(), + Key: m["Key"].String(), + Default: m["Default"].Val(), + Extra: m["Extra"].String(), + Comment: m["Comment"].String(), } - result, err = d.db.DoGetAll( - link, - fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.db.QuoteWord(table)), - ) - if err != nil { - return nil, err - } - fields = make(map[string]*TableField) - for i, m := range result { - fields[m["Field"].String()] = &TableField{ - Index: i, - Name: m["Field"].String(), - Type: m["Type"].String(), - Null: m["Null"].Bool(), - Key: m["Key"].String(), - Default: m["Default"].Val(), - Extra: m["Extra"].String(), - Comment: m["Comment"].String(), - } - } - return fields, nil - }, 0) - if err == nil { + } + return fields + }) + if v != nil { fields = v.(map[string]*TableField) } return diff --git a/database/gdb/gdb_driver_oracle.go b/database/gdb/gdb_driver_oracle.go index e0bac602f..9d8543936 100644 --- a/database/gdb/gdb_driver_oracle.go +++ b/database/gdb/gdb_driver_oracle.go @@ -12,17 +12,19 @@ package gdb import ( + "context" "database/sql" "fmt" + "reflect" + "strconv" + "strings" + "time" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" - "reflect" - "strconv" - "strings" - "time" ) // DriverOracle is the driver for oracle database. @@ -83,7 +85,7 @@ func (d *DriverOracle) GetChars() (charLeft string, charRight string) { } // HandleSqlBeforeCommit deals with the sql string before commits it to underlying sql driver. -func (d *DriverOracle) HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (newSql string, newArgs []interface{}) { +func (d *DriverOracle) HandleSqlBeforeCommit(ctx context.Context, link Link, sql string, args []interface{}) (newSql string, newArgs []interface{}) { var index int // Convert place holder char '?' to string ":vx". newSql, _ = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { @@ -164,9 +166,9 @@ func (d *DriverOracle) parseSql(sql string) string { // Tables retrieves and returns the tables of current schema. // It's mainly used in cli tool chain for automatically generating the models. // Note that it ignores the parameter `schema` in oracle database, as it is not necessary. -func (d *DriverOracle) Tables(schema ...string) (tables []string, err error) { +func (d *DriverOracle) Tables(ctx context.Context, schema ...string) (tables []string, err error) { var result Result - result, err = d.db.DoGetAll(nil, "SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME") + result, err = d.DoGetAll(ctx, nil, "SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME") if err != nil { return } @@ -179,21 +181,27 @@ func (d *DriverOracle) Tables(schema ...string) (tables []string, err error) { } // TableFields retrieves and returns the fields information of specified table of current schema. -func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { +// +// Also see DriverMysql.TableFields. +func (d *DriverOracle) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*TableField, err error) { charL, charR := d.GetChars() table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { return nil, gerror.New("function TableFields supports only single table operations") } - checkSchema := d.db.GetSchema() + useSchema := d.db.GetSchema() if len(schema) > 0 && schema[0] != "" { - checkSchema = schema[0] + useSchema = schema[0] } - v, _ := internalCache.GetOrSetFunc( - fmt.Sprintf(`oracle_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()), - func() (interface{}, error) { - result := (Result)(nil) - structureSql := fmt.Sprintf(` + tableFieldsCacheKey := fmt.Sprintf( + `oracle_table_fields_%s_%s@group:%s`, + table, useSchema, d.GetGroup(), + ) + v := tableFieldsMap.GetOrSetFuncLock(tableFieldsCacheKey, func() interface{} { + var ( + result Result + link, err = d.SlaveLink(useSchema) + structureSql = fmt.Sprintf(` SELECT COLUMN_NAME AS FIELD, CASE DATA_TYPE @@ -203,22 +211,26 @@ SELECT FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID`, strings.ToUpper(table), ) - structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.db.GetAll(structureSql) - if err != nil { - return nil, err + ) + if err != nil { + return nil + } + structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) + result, err = d.DoGetAll(ctx, link, structureSql) + if err != nil { + return nil + } + fields = make(map[string]*TableField) + for i, m := range result { + fields[strings.ToLower(m["FIELD"].String())] = &TableField{ + Index: i, + Name: strings.ToLower(m["FIELD"].String()), + Type: strings.ToLower(m["TYPE"].String()), } - fields = make(map[string]*TableField) - for i, m := range result { - fields[strings.ToLower(m["FIELD"].String())] = &TableField{ - Index: i, - Name: strings.ToLower(m["FIELD"].String()), - Type: strings.ToLower(m["TYPE"].String()), - } - } - return fields, nil - }, 0) - if err == nil { + } + return fields + }) + if v != nil { fields = v.(map[string]*TableField) } return @@ -252,7 +264,7 @@ func (d *DriverOracle) getTableUniqueIndex(table string) (fields map[string]map[ return } -func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, option int, batch ...int) (result sql.Result, err error) { +func (d *DriverOracle) DoInsert(ctx context.Context, link Link, table string, data interface{}, option int, batch ...int) (result sql.Result, err error) { var ( fields []string values []string @@ -267,7 +279,7 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio } switch kind { case reflect.Slice, reflect.Array: - return d.db.DoBatchInsert(link, table, data, option, batch...) + return d.DoBatchInsert(ctx, link, table, data, option, batch...) case reflect.Map: fallthrough case reflect.Struct: @@ -326,7 +338,7 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio } if link == nil { - if link, err = d.db.Master(); err != nil { + if link, err = d.MasterLink(); err != nil { return nil, err } } @@ -341,20 +353,17 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio table, tableAlias1, strings.Join(subSqlStr, ","), tableAlias2, strings.Join(onStr, "AND"), strings.Join(updateStr, ","), strings.Join(fields, ","), strings.Join(values, ","), ) - return d.db.DoExec(link, tmp, params...) + return d.DoExec(ctx, link, tmp, params...) case insertOptionIgnore: - return d.db.DoExec(link, - fmt.Sprintf( - "INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(%s(%s)) */ INTO %s(%s) VALUES(%s)", - table, strings.Join(indexes, ","), table, strings.Join(fields, ","), strings.Join(values, ","), - ), - params...) + return d.DoExec(ctx, link, fmt.Sprintf( + "INSERT /*+ IGNORE_ROW_ON_DUPKEY_INDEX(%s(%s)) */ INTO %s(%s) VALUES(%s)", + table, strings.Join(indexes, ","), table, strings.Join(fields, ","), strings.Join(values, ","), + ), params...) } } - return d.db.DoExec( - link, + return d.DoExec(ctx, link, fmt.Sprintf( "INSERT INTO %s(%s) VALUES(%s)", table, strings.Join(fields, ","), strings.Join(values, ","), @@ -362,7 +371,7 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio params...) } -func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) { +func (d *DriverOracle) DoBatchInsert(ctx context.Context, link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) { var ( keys []string values []string @@ -405,7 +414,7 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{}, return result, gerror.New("empty data list") } if link == nil { - if link, err = d.db.Master(); err != nil { + if link, err = d.MasterLink(); err != nil { return } } @@ -423,7 +432,7 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{}, ) if option != insertOptionDefault { for _, v := range listMap { - r, err := d.db.DoInsert(link, table, v, option, 1) + r, err := d.DoInsert(ctx, link, table, v, option, 1) if err != nil { return r, err } @@ -451,7 +460,7 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{}, values = append(values, valueHolderStr) intoStr = append(intoStr, fmt.Sprintf(" INTO %s(%s) VALUES(%s) ", table, keyStr, valueHolderStr)) if len(intoStr) == batchNum { - r, err := d.db.DoExec(link, fmt.Sprintf("INSERT ALL %s SELECT * FROM DUAL", strings.Join(intoStr, " ")), params...) + r, err := d.DoExec(ctx, link, fmt.Sprintf("INSERT ALL %s SELECT * FROM DUAL", strings.Join(intoStr, " ")), params...) if err != nil { return r, err } @@ -467,7 +476,7 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{}, } // The leftover data. if len(intoStr) > 0 { - r, err := d.db.DoExec(link, fmt.Sprintf("INSERT ALL %s SELECT * FROM DUAL", strings.Join(intoStr, " ")), params...) + r, err := d.DoExec(ctx, link, fmt.Sprintf("INSERT ALL %s SELECT * FROM DUAL", strings.Join(intoStr, " ")), params...) if err != nil { return r, err } diff --git a/database/gdb/gdb_driver_pgsql.go b/database/gdb/gdb_driver_pgsql.go index b587c9fca..e167a64eb 100644 --- a/database/gdb/gdb_driver_pgsql.go +++ b/database/gdb/gdb_driver_pgsql.go @@ -12,12 +12,14 @@ package gdb import ( + "context" "database/sql" "fmt" + "strings" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/text/gstr" - "strings" "github.com/gogf/gf/text/gregex" ) @@ -75,7 +77,7 @@ func (d *DriverPgsql) GetChars() (charLeft string, charRight string) { } // HandleSqlBeforeCommit deals with the sql string before commits it to underlying sql driver. -func (d *DriverPgsql) HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{}) { +func (d *DriverPgsql) HandleSqlBeforeCommit(ctx context.Context, link Link, sql string, args []interface{}) (string, []interface{}) { var index int // Convert place holder char '?' to string "$x". sql, _ = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { @@ -88,9 +90,9 @@ func (d *DriverPgsql) HandleSqlBeforeCommit(link Link, sql string, args []interf // Tables retrieves and returns the tables of current schema. // It's mainly used in cli tool chain for automatically generating the models. -func (d *DriverPgsql) Tables(schema ...string) (tables []string, err error) { +func (d *DriverPgsql) Tables(ctx context.Context, schema ...string) (tables []string, err error) { var result Result - link, err := d.db.GetSlave(schema...) + link, err := d.SlaveLink(schema...) if err != nil { return nil, err } @@ -98,7 +100,7 @@ func (d *DriverPgsql) Tables(schema ...string) (tables []string, err error) { if len(schema) > 0 && schema[0] != "" { query = fmt.Sprintf("SELECT TABLENAME FROM PG_TABLES WHERE SCHEMANAME = '%s' ORDER BY TABLENAME", schema[0]) } - result, err = d.db.DoGetAll(link, query) + result, err = d.DoGetAll(ctx, link, query) if err != nil { return } @@ -111,53 +113,55 @@ func (d *DriverPgsql) Tables(schema ...string) (tables []string, err error) { } // TableFields retrieves and returns the fields information of specified table of current schema. -func (d *DriverPgsql) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { +// +// Also see DriverMysql.TableFields. +func (d *DriverPgsql) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*TableField, err error) { charL, charR := d.GetChars() table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { return nil, gerror.New("function TableFields supports only single table operations") } table, _ = gregex.ReplaceString("\"", "", table) - checkSchema := d.db.GetSchema() + useSchema := d.db.GetSchema() if len(schema) > 0 && schema[0] != "" { - checkSchema = schema[0] + useSchema = schema[0] } - v, _ := internalCache.GetOrSetFunc( - fmt.Sprintf(`pgsql_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()), - func() (interface{}, error) { - var ( - result Result - link *sql.DB - ) - link, err = d.db.GetSlave(checkSchema) - if err != nil { - return nil, err - } - structureSql := fmt.Sprintf(` + tableFieldsCacheKey := fmt.Sprintf( + `pgsql_table_fields_%s_%s@group:%s`, + table, useSchema, d.GetGroup(), + ) + v := tableFieldsMap.GetOrSetFuncLock(tableFieldsCacheKey, func() interface{} { + var ( + result Result + link, err = d.SlaveLink(useSchema) + structureSql = fmt.Sprintf(` SELECT a.attname AS field, t.typname AS type,b.description as comment FROM pg_class c, pg_attribute a LEFT OUTER JOIN pg_description b ON a.attrelid=b.objoid AND a.attnum = b.objsubid,pg_type t WHERE c.relname = '%s' and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid ORDER BY a.attnum`, strings.ToLower(table), ) - structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) - result, err = d.db.DoGetAll(link, structureSql) - if err != nil { - return nil, err + ) + if err != nil { + return nil + } + structureSql, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(structureSql)) + result, err = d.DoGetAll(ctx, link, structureSql) + if err != nil { + return nil + } + fields = make(map[string]*TableField) + for i, m := range result { + fields[m["field"].String()] = &TableField{ + Index: i, + Name: m["field"].String(), + Type: m["type"].String(), + Comment: m["comment"].String(), } - - fields = make(map[string]*TableField) - for i, m := range result { - fields[m["field"].String()] = &TableField{ - Index: i, - Name: m["field"].String(), - Type: m["type"].String(), - Comment: m["comment"].String(), - } - } - return fields, nil - }, 0) - if err == nil { + } + return fields + }) + if v != nil { fields = v.(map[string]*TableField) } return diff --git a/database/gdb/gdb_driver_sqlite.go b/database/gdb/gdb_driver_sqlite.go index ed577217a..99ae55fcf 100644 --- a/database/gdb/gdb_driver_sqlite.go +++ b/database/gdb/gdb_driver_sqlite.go @@ -11,13 +11,15 @@ package gdb import ( + "context" "database/sql" "fmt" + "strings" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/os/gfile" "github.com/gogf/gf/text/gstr" - "strings" ) // DriverSqlite is the driver for sqlite database. @@ -67,20 +69,20 @@ func (d *DriverSqlite) GetChars() (charLeft string, charRight string) { // HandleSqlBeforeCommit deals with the sql string before commits it to underlying sql driver. // TODO 需要增加对Save方法的支持,可使用正则来实现替换, // TODO 将ON DUPLICATE KEY UPDATE触发器修改为两条SQL语句(INSERT OR IGNORE & UPDATE) -func (d *DriverSqlite) HandleSqlBeforeCommit(link Link, sql string, args []interface{}) (string, []interface{}) { +func (d *DriverSqlite) HandleSqlBeforeCommit(ctx context.Context, link Link, sql string, args []interface{}) (string, []interface{}) { return sql, args } // Tables retrieves and returns the tables of current schema. // It's mainly used in cli tool chain for automatically generating the models. -func (d *DriverSqlite) Tables(schema ...string) (tables []string, err error) { +func (d *DriverSqlite) Tables(ctx context.Context, schema ...string) (tables []string, err error) { var result Result - link, err := d.db.GetSlave(schema...) + link, err := d.SlaveLink(schema...) if err != nil { return nil, err } - result, err = d.db.DoGetAll(link, `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME`) + result, err = d.DoGetAll(ctx, link, `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME`) if err != nil { return } @@ -93,42 +95,45 @@ func (d *DriverSqlite) Tables(schema ...string) (tables []string, err error) { } // TableFields retrieves and returns the fields information of specified table of current schema. -func (d *DriverSqlite) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { +// +// Also see DriverMysql.TableFields. +func (d *DriverSqlite) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*TableField, err error) { charL, charR := d.GetChars() table = gstr.Trim(table, charL+charR) if gstr.Contains(table, " ") { return nil, gerror.New("function TableFields supports only single table operations") } - checkSchema := d.db.GetSchema() + useSchema := d.db.GetSchema() if len(schema) > 0 && schema[0] != "" { - checkSchema = schema[0] + useSchema = schema[0] } - v, _ := internalCache.GetOrSetFunc( - fmt.Sprintf(`sqlite_table_fields_%s_%s@group:%s`, table, checkSchema, d.GetGroup()), - func() (interface{}, error) { - var ( - result Result - link *sql.DB - ) - link, err = d.db.GetSlave(checkSchema) - if err != nil { - return nil, err + tableFieldsCacheKey := fmt.Sprintf( + `sqlite_table_fields_%s_%s@group:%s`, + table, useSchema, d.GetGroup(), + ) + v := tableFieldsMap.GetOrSetFuncLock(tableFieldsCacheKey, func() interface{} { + var ( + result Result + link, err = d.SlaveLink(useSchema) + ) + if err != nil { + return nil + } + result, err = d.DoGetAll(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, table)) + if err != nil { + return nil + } + fields = make(map[string]*TableField) + for i, m := range result { + fields[strings.ToLower(m["name"].String())] = &TableField{ + Index: i, + Name: strings.ToLower(m["name"].String()), + Type: strings.ToLower(m["type"].String()), } - result, err = d.db.DoGetAll(link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, table)) - if err != nil { - return nil, err - } - fields = make(map[string]*TableField) - for i, m := range result { - fields[strings.ToLower(m["name"].String())] = &TableField{ - Index: i, - Name: strings.ToLower(m["name"].String()), - Type: strings.ToLower(m["type"].String()), - } - } - return fields, nil - }, 0) - if err == nil { + } + return fields + }) + if v != nil { fields = v.(map[string]*TableField) } return diff --git a/database/gdb/gdb_func.go b/database/gdb/gdb_func.go index 162f08e8c..fc2c4d1ca 100644 --- a/database/gdb/gdb_func.go +++ b/database/gdb/gdb_func.go @@ -9,6 +9,11 @@ package gdb import ( "bytes" "fmt" + "reflect" + "regexp" + "strings" + "time" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/empty" "github.com/gogf/gf/internal/json" @@ -16,10 +21,6 @@ import ( "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/util/gmeta" "github.com/gogf/gf/util/gutil" - "reflect" - "regexp" - "strings" - "time" "github.com/gogf/gf/internal/structs" @@ -235,7 +236,7 @@ func DataToMapDeep(value interface{}) map[string]interface{} { name = "" fieldTag = rtField.Tag for _, tag := range structTagPriority { - if s := fieldTag.Get(tag); s != "" { + if s := fieldTag.Get(tag); s != "" && gregex.IsMatchString(regularFieldNameWithoutDotRegPattern, s) { name = s break } @@ -443,14 +444,14 @@ func formatSql(sql string, args []interface{}) (newSql string, newArgs []interfa return handleArguments(sql, args) } -// formatWhere formats where statement and its arguments. +// formatWhere formats where statement and its arguments for `Where` and `Having` statements. func formatWhere(db DB, where interface{}, args []interface{}, omitEmpty bool) (newWhere string, newArgs []interface{}) { var ( buffer = bytes.NewBuffer(nil) rv = reflect.ValueOf(where) kind = rv.Kind() ) - if kind == reflect.Ptr { + for kind == reflect.Ptr { rv = rv.Elem() kind = rv.Kind() } @@ -490,7 +491,36 @@ func formatWhere(db DB, where interface{}, args []interface{}, omitEmpty bool) ( } default: - buffer.WriteString(gconv.String(where)) + // Usually a string. + var ( + i = 0 + whereStr = gconv.String(where) + ) + for { + if i >= len(args) { + break + } + // Sub query, which is always used along with a string condition. + if model, ok := args[i].(*Model); ok { + var ( + index = -1 + ) + whereStr, _ = gregex.ReplaceStringFunc(`(\?)`, whereStr, func(s string) string { + index++ + if i+len(newArgs) == index { + sqlWithHolder, holderArgs := model.getFormattedSqlAndArgs(queryTypeNormal, false) + newArgs = append(newArgs, holderArgs...) + // Automatically adding the brackets. + return "(" + sqlWithHolder + ")" + } + return s + }) + args = gutil.SliceDelete(args, i) + continue + } + i++ + } + buffer.WriteString(whereStr) } if buffer.Len() == 0 { @@ -504,7 +534,7 @@ func formatWhere(db DB, where interface{}, args []interface{}, omitEmpty bool) ( // Eg: Where/And/Or("uid>=", 1) newWhere += "?" } else if gregex.IsMatchString(regularFieldNameRegPattern, newWhere) { - newWhere = db.QuoteString(newWhere) + newWhere = db.GetCore().QuoteString(newWhere) if len(newArgs) > 0 { if utils.IsArray(newArgs[0]) { // Eg: @@ -543,9 +573,9 @@ func formatWhereInterfaces(db DB, where []interface{}, buffer *bytes.Buffer, new for i := 0; i < len(where); i += 2 { str = gconv.String(where[i]) if buffer.Len() > 0 { - buffer.WriteString(" AND " + db.QuoteWord(str) + "=?") + buffer.WriteString(" AND " + db.GetCore().QuoteWord(str) + "=?") } else { - buffer.WriteString(db.QuoteWord(str) + "=?") + buffer.WriteString(db.GetCore().QuoteWord(str) + "=?") } if s, ok := where[i+1].(Raw); ok { buffer.WriteString(gconv.String(s)) @@ -558,7 +588,7 @@ func formatWhereInterfaces(db DB, where []interface{}, buffer *bytes.Buffer, new // formatWhereKeyValue handles each key-value pair of the parameter map. func formatWhereKeyValue(db DB, buffer *bytes.Buffer, newArgs []interface{}, key string, value interface{}) []interface{} { - quotedKey := db.QuoteWord(key) + quotedKey := db.GetCore().QuoteWord(key) if buffer.Len() > 0 { buffer.WriteString(" AND ") } diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index 4b109534f..627051adb 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -9,9 +9,11 @@ package gdb import ( "context" "fmt" - "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/util/gconv" "time" + "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" ) @@ -27,7 +29,7 @@ type Model struct { fieldsEx string // Excluded operation fields, multiple fields joined using char ','. withArray []interface{} // Arguments for With feature. withAll bool // Enable model association operations on all objects that have "with" tag in the struct. - extraArgs []interface{} // Extra custom arguments for sql. + extraArgs []interface{} // Extra custom arguments for sql, which are prepended to the arguments before sql committed to underlying driver. whereHolder []*whereHolder // Condition strings for where operation. groupBy string // Used for "group by" statement. orderBy string // Used for "order by" statement. @@ -39,6 +41,7 @@ type Model struct { data interface{} // Data for operation, which can be type of map/[]map/struct/*struct/string, etc. batch int // Batch number for batch Insert/Replace/Save operations. filter bool // Filter data and where key-value pairs according to the fields of the table. + 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. @@ -55,67 +58,92 @@ type whereHolder struct { } const ( - OPTION_OMITEMPTY = 1 // Deprecated, use OptionOmitEmpty instead. - OPTION_ALLOWEMPTY = 2 // Deprecated, use OptionAllowEmpty instead. - OptionOmitEmpty = 1 - OptionAllowEmpty = 2 - linkTypeMaster = 1 - linkTypeSlave = 2 - whereHolderWhere = 1 - whereHolderAnd = 2 - whereHolderOr = 3 + OptionOmitEmpty = 1 + OptionAllowEmpty = 2 + linkTypeMaster = 1 + linkTypeSlave = 2 + whereHolderWhere = 1 + whereHolderAnd = 2 + whereHolderOr = 3 ) // Table is alias of Core.Model. // See Core.Model. // Deprecated, use Model instead. -func (c *Core) Table(table ...string) *Model { - return c.db.Model(table...) +func (c *Core) Table(tableNameQueryOrStruct ...interface{}) *Model { + return c.db.Model(tableNameQueryOrStruct...) } // Model creates and returns a new ORM model from given schema. -// The parameter `table` can be more than one table names, and also alias name, like: +// The parameter `tableNameQueryOrStruct` can be more than one table names, and also alias name, like: // 1. Model names: // Model("user") // Model("user u") // Model("user, user_detail") // Model("user u, user_detail ud") // 2. Model name with alias: Model("user", "u") -func (c *Core) Model(table ...string) *Model { - tables := "" - if len(table) > 1 { - tables = fmt.Sprintf( - `%s AS %s`, c.db.QuotePrefixTableName(table[0]), c.db.QuoteWord(table[1]), - ) - } else if len(table) == 1 { - tables = c.db.QuotePrefixTableName(table[0]) +func (c *Core) Model(tableNameQueryOrStruct ...interface{}) *Model { + var ( + tableStr string + tableName string + extraArgs []interface{} + tableNames = make([]string, len(tableNameQueryOrStruct)) + ) + // Model creation with sub-query. + if len(tableNameQueryOrStruct) > 1 { + conditionStr := gconv.String(tableNameQueryOrStruct[0]) + if gstr.Contains(conditionStr, "?") { + tableStr, extraArgs = formatWhere( + c.db, conditionStr, tableNameQueryOrStruct[1:], false, + ) + } + } + // Normal model creation. + if tableStr == "" { + for k, v := range tableNameQueryOrStruct { + if s, ok := v.(string); ok { + tableNames[k] = s + } else if tableName = getTableNameFromOrmTag(v); tableName != "" { + tableNames[k] = tableName + } + } + + if len(tableNames) > 1 { + tableStr = fmt.Sprintf( + `%s AS %s`, c.QuotePrefixTableName(tableNames[0]), c.QuoteWord(tableNames[1]), + ) + } else if len(tableNames) == 1 { + tableStr = c.QuotePrefixTableName(tableNames[0]) + } } return &Model{ db: c.db, - tablesInit: tables, - tables: tables, + tablesInit: tableStr, + tables: tableStr, fields: "*", start: -1, offset: -1, option: OptionAllowEmpty, + filter: true, + extraArgs: extraArgs, } } // With creates and returns an ORM model based on meta data of given object. -func (c *Core) With(object interface{}) *Model { - return c.db.Model().With(object) +func (c *Core) With(objects ...interface{}) *Model { + return c.db.Model().With(objects...) } // Table is alias of tx.Model. // Deprecated, use Model instead. -func (tx *TX) Table(table ...string) *Model { - return tx.Model(table...) +func (tx *TX) Table(tableNameQueryOrStruct ...interface{}) *Model { + return tx.Model(tableNameQueryOrStruct...) } // Model acts like Core.Model except it operates on transaction. // See Core.Model. -func (tx *TX) Model(table ...string) *Model { - model := tx.db.Model(table...) +func (tx *TX) Model(tableNameQueryOrStruct ...interface{}) *Model { + model := tx.db.Model(tableNameQueryOrStruct...) model.db = tx.db model.tx = tx return model @@ -134,9 +162,21 @@ func (m *Model) Ctx(ctx context.Context) *Model { } model := m.getModel() model.db = model.db.Ctx(ctx) + if m.tx != nil { + model.tx = model.tx.Ctx(ctx) + } return model } +// GetCtx returns the context for current Model. +// It returns `context.Background()` is there's no context previously set. +func (m *Model) GetCtx() context.Context { + if m.tx != nil && m.tx.ctx != nil { + return m.tx.ctx + } + return m.db.GetCtx() +} + // As sets an alias name for current table. func (m *Model) As(as string) *Model { if m.tables != "" { diff --git a/database/gdb/gdb_model_cache.go b/database/gdb/gdb_model_cache.go index 0529bf5f6..b9ba72873 100644 --- a/database/gdb/gdb_model_cache.go +++ b/database/gdb/gdb_model_cache.go @@ -38,6 +38,6 @@ func (m *Model) Cache(duration time.Duration, name ...string) *Model { // cache feature is enabled. func (m *Model) checkAndRemoveCache() { if m.cacheEnabled && m.cacheDuration < 0 && len(m.cacheName) > 0 { - m.db.GetCache().Ctx(m.db.GetCtx()).Remove(m.cacheName) + m.db.GetCache().Ctx(m.GetCtx()).Remove(m.cacheName) } } diff --git a/database/gdb/gdb_model_condition.go b/database/gdb/gdb_model_condition.go index a78c43047..7a7d41e5e 100644 --- a/database/gdb/gdb_model_condition.go +++ b/database/gdb/gdb_model_condition.go @@ -7,6 +7,9 @@ package gdb import ( + "fmt" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/util/gconv" "strings" ) @@ -58,7 +61,188 @@ func (m *Model) WherePri(where interface{}, args ...interface{}) *Model { return m.Where(newWhere[0], newWhere[1:]...) } +// Wheref builds condition string using fmt.Sprintf and arguments. +func (m *Model) Wheref(format string, args ...interface{}) *Model { + var ( + placeHolderCount = gstr.Count(format, "?") + conditionStr = fmt.Sprintf(format, args[:len(args)-placeHolderCount]...) + ) + return m.Where(conditionStr, args[len(args)-placeHolderCount:]...) +} + +// WhereLT builds `column < value` statement. +func (m *Model) WhereLT(column string, value interface{}) *Model { + return m.Wheref(`%s < ?`, column, value) +} + +// WhereLTE builds `column <= value` statement. +func (m *Model) WhereLTE(column string, value interface{}) *Model { + return m.Wheref(`%s <= ?`, column, value) +} + +// WhereGT builds `column > value` statement. +func (m *Model) WhereGT(column string, value interface{}) *Model { + return m.Wheref(`%s > ?`, column, value) +} + +// WhereGTE builds `column >= value` statement. +func (m *Model) WhereGTE(column string, value interface{}) *Model { + return m.Wheref(`%s >= ?`, column, value) +} + +// WhereBetween builds `column BETWEEN min AND max` statement. +func (m *Model) WhereBetween(column string, min, max interface{}) *Model { + return m.Wheref(`%s BETWEEN ? AND ?`, m.db.GetCore().QuoteWord(column), min, max) +} + +// WhereLike builds `column LIKE like` statement. +func (m *Model) WhereLike(column string, like interface{}) *Model { + return m.Wheref(`%s LIKE ?`, m.db.GetCore().QuoteWord(column), like) +} + +// WhereIn builds `column IN (in)` statement. +func (m *Model) WhereIn(column string, in interface{}) *Model { + return m.Wheref(`%s IN (?)`, m.db.GetCore().QuoteWord(column), in) +} + +// WhereNull builds `columns[0] IS NULL AND columns[1] IS NULL ...` statement. +func (m *Model) WhereNull(columns ...string) *Model { + model := m + for _, column := range columns { + model = m.Wheref(`%s IS NULL`, m.db.GetCore().QuoteWord(column)) + } + return model +} + +// WhereNotBetween builds `column NOT BETWEEN min AND max` statement. +func (m *Model) WhereNotBetween(column string, min, max interface{}) *Model { + return m.Wheref(`%s NOT BETWEEN ? AND ?`, m.db.GetCore().QuoteWord(column), min, max) +} + +// WhereNotLike builds `column NOT LIKE like` statement. +func (m *Model) WhereNotLike(column string, like interface{}) *Model { + return m.Wheref(`%s NOT LIKE ?`, m.db.GetCore().QuoteWord(column), like) +} + +// WhereNot builds `column != value` statement. +func (m *Model) WhereNot(column string, value interface{}) *Model { + return m.Wheref(`%s != ?`, m.db.GetCore().QuoteWord(column), value) +} + +// WhereNotIn builds `column NOT IN (in)` statement. +func (m *Model) WhereNotIn(column string, in interface{}) *Model { + return m.Wheref(`%s NOT IN (?)`, m.db.GetCore().QuoteWord(column), in) +} + +// WhereNotNull builds `columns[0] IS NOT NULL AND columns[1] IS NOT NULL ...` statement. +func (m *Model) WhereNotNull(columns ...string) *Model { + model := m + for _, column := range columns { + model = m.Wheref(`%s IS NOT NULL`, m.db.GetCore().QuoteWord(column)) + } + return model +} + +// WhereOr adds "OR" condition to the where statement. +func (m *Model) WhereOr(where interface{}, args ...interface{}) *Model { + model := m.getModel() + if model.whereHolder == nil { + model.whereHolder = make([]*whereHolder, 0) + } + model.whereHolder = append(model.whereHolder, &whereHolder{ + operator: whereHolderOr, + where: where, + args: args, + }) + return model +} + +// WhereOrf builds `OR` condition string using fmt.Sprintf and arguments. +func (m *Model) WhereOrf(format string, args ...interface{}) *Model { + var ( + placeHolderCount = gstr.Count(format, "?") + conditionStr = fmt.Sprintf(format, args[:len(args)-placeHolderCount]...) + ) + return m.WhereOr(conditionStr, args[len(args)-placeHolderCount:]...) +} + +// WhereOrLT builds `column < value` statement in `OR` conditions.. +func (m *Model) WhereOrLT(column string, value interface{}) *Model { + return m.WhereOrf(`%s < ?`, column, value) +} + +// WhereOrLTE builds `column <= value` statement in `OR` conditions.. +func (m *Model) WhereOrLTE(column string, value interface{}) *Model { + return m.WhereOrf(`%s <= ?`, column, value) +} + +// WhereOrGT builds `column > value` statement in `OR` conditions.. +func (m *Model) WhereOrGT(column string, value interface{}) *Model { + return m.WhereOrf(`%s > ?`, column, value) +} + +// WhereOrGTE builds `column >= value` statement in `OR` conditions.. +func (m *Model) WhereOrGTE(column string, value interface{}) *Model { + return m.WhereOrf(`%s >= ?`, column, value) +} + +// WhereOrBetween builds `column BETWEEN min AND max` statement in `OR` conditions. +func (m *Model) WhereOrBetween(column string, min, max interface{}) *Model { + return m.WhereOrf(`%s BETWEEN ? AND ?`, m.db.GetCore().QuoteWord(column), min, max) +} + +// WhereOrLike builds `column LIKE like` statement in `OR` conditions. +func (m *Model) WhereOrLike(column string, like interface{}) *Model { + return m.WhereOrf(`%s LIKE ?`, m.db.GetCore().QuoteWord(column), like) +} + +// WhereOrIn builds `column IN (in)` statement in `OR` conditions. +func (m *Model) WhereOrIn(column string, in interface{}) *Model { + return m.WhereOrf(`%s IN (?)`, m.db.GetCore().QuoteWord(column), in) +} + +// WhereOrNull builds `columns[0] IS NULL OR columns[1] IS NULL ...` statement in `OR` conditions. +func (m *Model) WhereOrNull(columns ...string) *Model { + model := m + for _, column := range columns { + model = m.WhereOrf(`%s IS NULL`, m.db.GetCore().QuoteWord(column)) + } + return model +} + +// WhereOrNotBetween builds `column NOT BETWEEN min AND max` statement in `OR` conditions. +func (m *Model) WhereOrNotBetween(column string, min, max interface{}) *Model { + return m.WhereOrf(`%s NOT BETWEEN ? AND ?`, m.db.GetCore().QuoteWord(column), min, max) +} + +// WhereOrNotLike builds `column NOT LIKE like` statement in `OR` conditions. +func (m *Model) WhereOrNotLike(column string, like interface{}) *Model { + return m.WhereOrf(`%s NOT LIKE ?`, m.db.GetCore().QuoteWord(column), like) +} + +// WhereOrNotIn builds `column NOT IN (in)` statement. +func (m *Model) WhereOrNotIn(column string, in interface{}) *Model { + return m.WhereOrf(`%s NOT IN (?)`, m.db.GetCore().QuoteWord(column), in) +} + +// WhereOrNotNull builds `columns[0] IS NOT NULL OR columns[1] IS NOT NULL ...` statement in `OR` conditions. +func (m *Model) WhereOrNotNull(columns ...string) *Model { + model := m + for _, column := range columns { + model = m.WhereOrf(`%s IS NOT NULL`, m.db.GetCore().QuoteWord(column)) + } + return model +} + +// Group sets the "GROUP BY" statement for the model. +func (m *Model) Group(groupBy string) *Model { + model := m.getModel() + model.groupBy = m.db.GetCore().QuoteString(groupBy) + return model +} + // And adds "AND" condition to the where statement. +// Deprecated, use Where instead. func (m *Model) And(where interface{}, args ...interface{}) *Model { model := m.getModel() if model.whereHolder == nil { @@ -73,24 +257,9 @@ func (m *Model) And(where interface{}, args ...interface{}) *Model { } // Or adds "OR" condition to the where statement. +// Deprecated, use WhereOr instead. func (m *Model) Or(where interface{}, args ...interface{}) *Model { - model := m.getModel() - if model.whereHolder == nil { - model.whereHolder = make([]*whereHolder, 0) - } - model.whereHolder = append(model.whereHolder, &whereHolder{ - operator: whereHolderOr, - where: where, - args: args, - }) - return model -} - -// Group sets the "GROUP BY" statement for the model. -func (m *Model) Group(groupBy string) *Model { - model := m.getModel() - model.groupBy = m.db.QuoteString(groupBy) - return model + return m.WhereOr(where, args...) } // GroupBy is alias of Model.Group. @@ -102,8 +271,38 @@ func (m *Model) GroupBy(groupBy string) *Model { // Order sets the "ORDER BY" statement for the model. func (m *Model) Order(orderBy ...string) *Model { + if len(orderBy) == 0 { + return m + } model := m.getModel() - model.orderBy = m.db.QuoteString(strings.Join(orderBy, " ")) + model.orderBy = m.db.GetCore().QuoteString(strings.Join(orderBy, " ")) + return model +} + +// OrderAsc sets the "ORDER BY xxx ASC" statement for the model. +func (m *Model) OrderAsc(column string) *Model { + if len(column) == 0 { + return m + } + model := m.getModel() + model.orderBy = m.db.GetCore().QuoteWord(column) + " ASC" + return model +} + +// OrderDesc sets the "ORDER BY xxx DESC" statement for the model. +func (m *Model) OrderDesc(column string) *Model { + if len(column) == 0 { + return m + } + model := m.getModel() + model.orderBy = m.db.GetCore().QuoteWord(column) + " DESC" + return model +} + +// OrderRandom sets the "ORDER BY RANDOM()" statement for the model. +func (m *Model) OrderRandom() *Model { + model := m.getModel() + model.orderBy = "RAND()" return model } @@ -138,6 +337,13 @@ func (m *Model) Offset(offset int) *Model { return model } +// Distinct forces the query to only return distinct results. +func (m *Model) Distinct() *Model { + model := m.getModel() + model.distinct = "DISTINCT " + return model +} + // Page sets the paging number for the model. // The parameter `page` is started from 1 for paging. // Note that, it differs that the Limit function starts from 0 for "LIMIT" statement. @@ -157,3 +363,110 @@ func (m *Model) Page(page, limit int) *Model { func (m *Model) ForPage(page, limit int) *Model { return m.Page(page, limit) } + +// formatCondition formats where arguments of the model and returns a new condition sql and its arguments. +// Note that this function does not change any attribute value of the `m`. +// +// The parameter `limit1` specifies whether limits querying only one record if m.limit is not set. +func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWhere string, conditionExtra string, conditionArgs []interface{}) { + if len(m.whereHolder) > 0 { + for _, v := range m.whereHolder { + switch v.operator { + case whereHolderWhere: + if conditionWhere == "" { + newWhere, newArgs := formatWhere( + m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, + ) + if len(newWhere) > 0 { + conditionWhere = newWhere + conditionArgs = newArgs + } + continue + } + fallthrough + + case whereHolderAnd: + newWhere, newArgs := formatWhere( + m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, + ) + if len(newWhere) > 0 { + if len(conditionWhere) == 0 { + conditionWhere = newWhere + } else if conditionWhere[0] == '(' { + conditionWhere = fmt.Sprintf(`%s AND (%s)`, conditionWhere, newWhere) + } else { + conditionWhere = fmt.Sprintf(`(%s) AND (%s)`, conditionWhere, newWhere) + } + conditionArgs = append(conditionArgs, newArgs...) + } + + case whereHolderOr: + newWhere, newArgs := formatWhere( + m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, + ) + if len(newWhere) > 0 { + if len(conditionWhere) == 0 { + conditionWhere = newWhere + } else if conditionWhere[0] == '(' { + conditionWhere = fmt.Sprintf(`%s OR (%s)`, conditionWhere, newWhere) + } else { + conditionWhere = fmt.Sprintf(`(%s) OR (%s)`, conditionWhere, newWhere) + } + conditionArgs = append(conditionArgs, newArgs...) + } + } + } + } + // Soft deletion. + softDeletingCondition := m.getConditionForSoftDeleting() + if !m.unscoped && softDeletingCondition != "" { + if conditionWhere == "" { + conditionWhere = fmt.Sprintf(` WHERE %s`, softDeletingCondition) + } else { + conditionWhere = fmt.Sprintf(` WHERE (%s) AND %s`, conditionWhere, softDeletingCondition) + } + } else { + if conditionWhere != "" { + conditionWhere = " WHERE " + conditionWhere + } + } + // GROUP BY. + if m.groupBy != "" { + conditionExtra += " GROUP BY " + m.groupBy + } + // HAVING. + if len(m.having) > 0 { + havingStr, havingArgs := formatWhere( + m.db, m.having[0], gconv.Interfaces(m.having[1]), m.option&OptionOmitEmpty > 0, + ) + if len(havingStr) > 0 { + conditionExtra += " HAVING " + havingStr + conditionArgs = append(conditionArgs, havingArgs...) + } + } + // ORDER BY. + if m.orderBy != "" { + conditionExtra += " ORDER BY " + m.orderBy + } + // LIMIT. + if !isCountStatement { + if m.limit != 0 { + if m.start >= 0 { + conditionExtra += fmt.Sprintf(" LIMIT %d,%d", m.start, m.limit) + } else { + conditionExtra += fmt.Sprintf(" LIMIT %d", m.limit) + } + } else if limit1 { + conditionExtra += " LIMIT 1" + } + + if m.offset >= 0 { + conditionExtra += fmt.Sprintf(" OFFSET %d", m.offset) + } + } + + if m.lockInfo != "" { + conditionExtra += " " + m.lockInfo + } + return +} diff --git a/database/gdb/gdb_model_delete.go b/database/gdb/gdb_model_delete.go index 4c56cdcd4..1e08d68da 100644 --- a/database/gdb/gdb_model_delete.go +++ b/database/gdb/gdb_model_delete.go @@ -9,6 +9,7 @@ package gdb import ( "database/sql" "fmt" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gstr" @@ -33,9 +34,10 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { // Soft deleting. if !m.unscoped && fieldNameDelete != "" { return m.db.DoUpdate( + m.GetCtx(), m.getLink(true), m.tables, - fmt.Sprintf(`%s=?`, m.db.QuoteString(fieldNameDelete)), + fmt.Sprintf(`%s=?`, m.db.GetCore().QuoteString(fieldNameDelete)), conditionWhere+conditionExtra, append([]interface{}{gtime.Now().String()}, conditionArgs...), ) @@ -44,5 +46,5 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) { if !gstr.ContainsI(conditionStr, " WHERE ") { return nil, gerror.New("there should be WHERE condition statement for DELETE operation") } - return m.db.DoDelete(m.getLink(true), m.tables, conditionStr, conditionArgs...) + return m.db.DoDelete(m.GetCtx(), m.getLink(true), m.tables, conditionStr, conditionArgs...) } diff --git a/database/gdb/gdb_model_fields.go b/database/gdb/gdb_model_fields.go index bbcb5978f..46d7af223 100644 --- a/database/gdb/gdb_model_fields.go +++ b/database/gdb/gdb_model_fields.go @@ -14,17 +14,6 @@ import ( "github.com/gogf/gf/util/gutil" ) -// Filter marks filtering the fields which does not exist in the fields of the operated table. -// Note that this function supports only single table operations. -func (m *Model) Filter() *Model { - if gstr.Contains(m.tables, " ") { - panic("function Filter supports only single table operations") - } - model := m.getModel() - model.filter = true - return model -} - // Fields sets the operation fields of the model, multiple fields joined using char ','. // The parameter `fieldNamesOrMapStruct` can be type of string/map/*map/struct/*struct. func (m *Model) Fields(fieldNamesOrMapStruct ...interface{}) *Model { @@ -81,20 +70,33 @@ func (m *Model) FieldsEx(fieldNamesOrMapStruct ...interface{}) *Model { return m } +// Filter marks filtering the fields which does not exist in the fields of the operated table. +// Note that this function supports only single table operations. +// Deprecated, filter feature is automatically enabled from GoFrame v1.16.0, it is so no longer used. +func (m *Model) Filter() *Model { + if gstr.Contains(m.tables, " ") { + panic("function Filter supports only single table operations") + } + model := m.getModel() + model.filter = true + return model +} + +// FieldsStr retrieves and returns all fields from the table, joined with char ','. +// The optional parameter `prefix` specifies the prefix for each field, eg: FieldsStr("u."). // Deprecated, use GetFieldsStr instead. -// This function name confuses the user that it was a chaining function. func (m *Model) FieldsStr(prefix ...string) string { return m.GetFieldsStr(prefix...) } -// FieldsStr retrieves and returns all fields from the table, joined with char ','. +// GetFieldsStr retrieves and returns all fields from the table, joined with char ','. // The optional parameter `prefix` specifies the prefix for each field, eg: FieldsStr("u."). func (m *Model) GetFieldsStr(prefix ...string) string { prefixStr := "" if len(prefix) > 0 { prefixStr = prefix[0] } - tableFields, err := m.db.TableFields(m.tables) + tableFields, err := m.TableFields(m.tables) if err != nil { panic(err) } @@ -112,17 +114,20 @@ func (m *Model) GetFieldsStr(prefix ...string) string { } newFields += prefixStr + k } - newFields = m.db.QuoteString(newFields) + newFields = m.db.GetCore().QuoteString(newFields) return newFields } +// FieldsExStr retrieves and returns fields which are not in parameter `fields` from the table, +// joined with char ','. +// The parameter `fields` specifies the fields that are excluded. +// The optional parameter `prefix` specifies the prefix for each field, eg: FieldsExStr("id", "u."). // Deprecated, use GetFieldsExStr instead. -// This function name confuses the user that it was a chaining function. func (m *Model) FieldsExStr(fields string, prefix ...string) string { return m.GetFieldsExStr(fields, prefix...) } -// FieldsExStr retrieves and returns fields which are not in parameter `fields` from the table, +// GetFieldsExStr retrieves and returns fields which are not in parameter `fields` from the table, // joined with char ','. // The parameter `fields` specifies the fields that are excluded. // The optional parameter `prefix` specifies the prefix for each field, eg: FieldsExStr("id", "u."). @@ -131,7 +136,7 @@ func (m *Model) GetFieldsExStr(fields string, prefix ...string) string { if len(prefix) > 0 { prefixStr = prefix[0] } - tableFields, err := m.db.TableFields(m.tables) + tableFields, err := m.TableFields(m.tables) if err != nil { panic(err) } @@ -153,13 +158,13 @@ func (m *Model) GetFieldsExStr(fields string, prefix ...string) string { } newFields += prefixStr + k } - newFields = m.db.QuoteString(newFields) + newFields = m.db.GetCore().QuoteString(newFields) return newFields } // HasField determine whether the field exists in the table. func (m *Model) HasField(field string) (bool, error) { - tableFields, err := m.db.TableFields(m.tables) + tableFields, err := m.TableFields(m.tables) if err != nil { return false, err } diff --git a/database/gdb/gdb_model_insert.go b/database/gdb/gdb_model_insert.go index b0959d304..09001e7d3 100644 --- a/database/gdb/gdb_model_insert.go +++ b/database/gdb/gdb_model_insert.go @@ -8,12 +8,13 @@ package gdb import ( "database/sql" + "reflect" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" "github.com/gogf/gf/util/gutil" - "reflect" ) // Batch sets the batch operation number for the model. @@ -109,6 +110,18 @@ func (m *Model) Insert(data ...interface{}) (result sql.Result, err error) { return m.doInsertWithOption(insertOptionDefault) } +// InsertAndGetId performs action Insert and returns the last insert id that automatically generated. +func (m *Model) InsertAndGetId(data ...interface{}) (lastInsertId int64, err error) { + if len(data) > 0 { + return m.Data(data...).InsertAndGetId() + } + result, err := m.doInsertWithOption(insertOptionDefault) + if err != nil { + return 0, err + } + return result.LastInsertId() +} + // InsertIgnore does "INSERT IGNORE INTO ..." statement for the model. // The optional parameter `data` is the same as the parameter of Model.Data function, // see Model.Data. @@ -183,6 +196,7 @@ func (m *Model) doInsertWithOption(option int) (result sql.Result, err error) { } } return m.db.DoBatchInsert( + m.GetCtx(), m.getLink(true), m.tables, newData, @@ -208,6 +222,7 @@ func (m *Model) doInsertWithOption(option int) (result sql.Result, err error) { } } return m.db.DoInsert( + m.GetCtx(), m.getLink(true), m.tables, newData, diff --git a/database/gdb/gdb_model_join.go b/database/gdb/gdb_model_join.go index 847c11abd..10e4c9255 100644 --- a/database/gdb/gdb_model_join.go +++ b/database/gdb/gdb_model_join.go @@ -8,6 +8,7 @@ package gdb import ( "fmt" + "github.com/gogf/gf/text/gstr" ) @@ -72,13 +73,13 @@ func (m *Model) doJoin(operator string, table ...string) *Model { joinStr = "(" + joinStr + ")" } } else { - joinStr = m.db.QuotePrefixTableName(table[0]) + joinStr = m.db.GetCore().QuotePrefixTableName(table[0]) } } if len(table) > 2 { model.tables += fmt.Sprintf( " %s JOIN %s AS %s ON (%s)", - operator, joinStr, m.db.QuoteWord(table[1]), table[2], + operator, joinStr, m.db.GetCore().QuoteWord(table[1]), table[2], ) } else if len(table) == 2 { model.tables += fmt.Sprintf( diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 1cd5139fa..ff366f218 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -8,18 +8,24 @@ package gdb import ( "fmt" + "reflect" + "github.com/gogf/gf/container/gset" "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/internal/json" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" - "reflect" +) + +const ( + queryTypeNormal = "NormalQuery" + queryTypeCount = "CountQuery" ) // Select is alias of Model.All. // See Model.All. -// Deprecated. +// Deprecated, use All instead. func (m *Model) Select(where ...interface{}) (Result, error) { return m.All(where...) } @@ -45,18 +51,8 @@ func (m *Model) doGetAll(limit1 bool, where ...interface{}) (Result, error) { if len(where) > 0 { return m.Where(where[0], where[1:]...).All() } - conditionWhere, conditionExtra, conditionArgs := m.formatCondition(limit1, false) - // DO NOT quote the m.fields where, in case of fields like: - // DISTINCT t.user_id uid - return m.doGetAllBySql( - fmt.Sprintf( - "SELECT %s FROM %s%s", - m.getFieldsFiltered(), - m.tables, - conditionWhere+conditionExtra, - ), - conditionArgs..., - ) + sqlWithHolder, holderArgs := m.getFormattedSqlAndArgs(queryTypeNormal, limit1) + return m.doGetAllBySql(sqlWithHolder, holderArgs...) } // getFieldsFiltered checks the fields and fieldsEx attributes, filters and returns the fields that will @@ -65,7 +61,7 @@ func (m *Model) getFieldsFiltered() string { if m.fieldsEx == "" { // No filtering. if !gstr.Contains(m.fields, ".") && !gstr.Contains(m.fields, " ") { - return m.db.QuoteString(m.fields) + return m.db.GetCore().QuoteString(m.fields) } return m.fields } @@ -84,7 +80,7 @@ func (m *Model) getFieldsFiltered() string { panic("function FieldsEx supports only single table operations") } // Filter table fields with fieldEx. - tableFields, err := m.db.TableFields(m.tables) + tableFields, err := m.TableFields(m.tables) if err != nil { panic(err) } @@ -104,7 +100,7 @@ func (m *Model) getFieldsFiltered() string { if len(newFields) > 0 { newFields += "," } - newFields += m.db.QuoteWord(k) + newFields += m.db.GetCore().QuoteWord(k) } return newFields } @@ -182,7 +178,7 @@ func (m *Model) Value(fieldsAndWhere ...interface{}) (Value, error) { } // Array queries and returns data values as slice from database. -// Note that if there're multiple columns in the result, it returns just one column values randomly. +// Note that if there are multiple columns in the result, it returns just one column values randomly. // // If the optional parameter `fieldsAndWhere` is given, the fieldsAndWhere[0] is the selected fields // and fieldsAndWhere[1:] is treated as where condition fields. @@ -330,18 +326,10 @@ func (m *Model) Count(where ...interface{}) (int, error) { if len(where) > 0 { return m.Where(where[0], where[1:]...).Count() } - countFields := "COUNT(1)" - if m.fields != "" && m.fields != "*" { - // DO NOT quote the m.fields here, in case of fields like: - // DISTINCT t.user_id uid - countFields = fmt.Sprintf(`COUNT(%s)`, m.fields) - } - conditionWhere, conditionExtra, conditionArgs := m.formatCondition(false, true) - s := fmt.Sprintf("SELECT %s FROM %s%s", countFields, m.tables, conditionWhere+conditionExtra) - if len(m.groupBy) > 0 { - s = fmt.Sprintf("SELECT COUNT(1) FROM (%s) count_alias", s) - } - list, err := m.doGetAllBySql(s, conditionArgs...) + var ( + sqlWithHolder, holderArgs = m.getFormattedSqlAndArgs(queryTypeCount, false) + list, err = m.doGetAllBySql(sqlWithHolder, holderArgs...) + ) if err != nil { return 0, err } @@ -353,6 +341,62 @@ func (m *Model) Count(where ...interface{}) (int, error) { return 0, nil } +// CountColumn does "SELECT COUNT(x) FROM ..." statement for the model. +func (m *Model) CountColumn(column string) (int, error) { + if len(column) == 0 { + return 0, nil + } + return m.Fields(column).Count() +} + +// Min does "SELECT MIN(x) FROM ..." statement for the model. +func (m *Model) Min(column string) (float64, error) { + if len(column) == 0 { + return 0, nil + } + value, err := m.Fields(fmt.Sprintf(`MIN(%s)`, m.db.GetCore().QuoteWord(column))).Value() + if err != nil { + return 0, err + } + return value.Float64(), err +} + +// Max does "SELECT MAX(x) FROM ..." statement for the model. +func (m *Model) Max(column string) (float64, error) { + if len(column) == 0 { + return 0, nil + } + value, err := m.Fields(fmt.Sprintf(`MAX(%s)`, m.db.GetCore().QuoteWord(column))).Value() + if err != nil { + return 0, err + } + return value.Float64(), err +} + +// Avg does "SELECT AVG(x) FROM ..." statement for the model. +func (m *Model) Avg(column string) (float64, error) { + if len(column) == 0 { + return 0, nil + } + value, err := m.Fields(fmt.Sprintf(`AVG(%s)`, m.db.GetCore().QuoteWord(column))).Value() + if err != nil { + return 0, err + } + return value.Float64(), err +} + +// Sum does "SELECT SUM(x) FROM ..." statement for the model. +func (m *Model) Sum(column string) (float64, error) { + if len(column) == 0 { + return 0, nil + } + value, err := m.Fields(fmt.Sprintf(`SUM(%s)`, m.db.GetCore().QuoteWord(column))).Value() + if err != nil { + return 0, err + } + return value.Float64(), err +} + // FindOne retrieves and returns a single Record by Model.WherePri and Model.One. // Also see Model.WherePri and Model.One. func (m *Model) FindOne(where ...interface{}) (Record, error) { @@ -417,7 +461,7 @@ func (m *Model) FindScan(pointer interface{}, where ...interface{}) error { // doGetAllBySql does the select statement on the database. func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, err error) { cacheKey := "" - cacheObj := m.db.GetCache().Ctx(m.db.GetCtx()) + cacheObj := m.db.GetCache().Ctx(m.GetCtx()) // Retrieve from cache. if m.cacheEnabled && m.tx == nil { cacheKey = m.cacheName @@ -431,7 +475,7 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e } else { // Other cache, it needs conversion. var result Result - if err = json.Unmarshal(v.Bytes(), &result); err != nil { + if err = json.UnmarshalUseNumber(v.Bytes(), &result); err != nil { return nil, err } else { return result, nil @@ -439,7 +483,9 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e } } } - result, err = m.db.DoGetAll(m.getLink(false), sql, m.mergeArguments(args)...) + result, err = m.db.DoGetAll( + m.GetCtx(), m.getLink(false), sql, m.mergeArguments(args)..., + ) // Cache the result. if cacheKey != "" && err == nil { if m.cacheDuration < 0 { @@ -454,3 +500,34 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e } return result, err } + +func (m *Model) getFormattedSqlAndArgs(queryType string, limit1 bool) (sqlWithHolder string, holderArgs []interface{}) { + switch queryType { + case queryTypeCount: + countFields := "COUNT(1)" + if m.fields != "" && m.fields != "*" { + // DO NOT quote the m.fields here, in case of fields like: + // DISTINCT t.user_id uid + countFields = fmt.Sprintf(`COUNT(%s%s)`, m.distinct, m.fields) + } + conditionWhere, conditionExtra, conditionArgs := m.formatCondition(false, true) + sqlWithHolder = fmt.Sprintf("SELECT %s FROM %s%s", countFields, m.tables, conditionWhere+conditionExtra) + if len(m.groupBy) > 0 { + sqlWithHolder = fmt.Sprintf("SELECT COUNT(1) FROM (%s) count_alias", sqlWithHolder) + } + return sqlWithHolder, conditionArgs + + default: + conditionWhere, conditionExtra, conditionArgs := m.formatCondition(limit1, false) + // DO NOT quote the m.fields where, in case of fields like: + // DISTINCT t.user_id uid + sqlWithHolder = fmt.Sprintf( + "SELECT %s%s FROM %s%s", + m.distinct, + m.getFieldsFiltered(), + m.tables, + conditionWhere+conditionExtra, + ) + return sqlWithHolder, conditionArgs + } +} diff --git a/database/gdb/gdb_model_time.go b/database/gdb/gdb_model_time.go index ce14d831e..e4047e4a1 100644 --- a/database/gdb/gdb_model_time.go +++ b/database/gdb/gdb_model_time.go @@ -93,7 +93,7 @@ func (m *Model) getSoftFieldNameDeleted(table ...string) (field string) { // getSoftFieldName retrieves and returns the field name of the table for possible key. func (m *Model) getSoftFieldName(table string, keys []string) (field string) { - fieldsMap, _ := m.db.TableFields(table) + fieldsMap, _ := m.TableFields(table) if len(fieldsMap) > 0 { for _, key := range keys { field, _ = gutil.MapPossibleItemByKey( @@ -140,7 +140,7 @@ func (m *Model) getConditionForSoftDeleting() string { } // Only one table. if fieldName := m.getSoftFieldNameDeleted(); fieldName != "" { - return fmt.Sprintf(`%s IS NULL`, m.db.QuoteWord(fieldName)) + return fmt.Sprintf(`%s IS NULL`, m.db.GetCore().QuoteWord(fieldName)) } return "" } @@ -163,12 +163,12 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string { return "" } if len(array1) >= 3 { - return fmt.Sprintf(`%s.%s IS NULL`, m.db.QuoteWord(array1[2]), m.db.QuoteWord(field)) + return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[2]), m.db.GetCore().QuoteWord(field)) } if len(array1) >= 2 { - return fmt.Sprintf(`%s.%s IS NULL`, m.db.QuoteWord(array1[1]), m.db.QuoteWord(field)) + return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(array1[1]), m.db.GetCore().QuoteWord(field)) } - return fmt.Sprintf(`%s.%s IS NULL`, m.db.QuoteWord(table), m.db.QuoteWord(field)) + return fmt.Sprintf(`%s.%s IS NULL`, m.db.GetCore().QuoteWord(table), m.db.GetCore().QuoteWord(field)) } // getPrimaryTableName parses and returns the primary table name. diff --git a/database/gdb/gdb_model_transaction.go b/database/gdb/gdb_model_transaction.go new file mode 100644 index 000000000..6aaf933bf --- /dev/null +++ b/database/gdb/gdb_model_transaction.go @@ -0,0 +1,28 @@ +// 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 gdb + +import ( + "context" +) + +// Transaction wraps the transaction logic using function `f`. +// It rollbacks the transaction and returns the error from function `f` if +// it returns non-nil error. It commits the transaction and returns nil if +// function `f` returns nil. +// +// Note that, you should not Commit or Rollback the transaction in function `f` +// as it is automatically handled by this function. +func (m *Model) Transaction(ctx context.Context, f func(ctx context.Context, tx *TX) error) (err error) { + if ctx == nil { + ctx = m.GetCtx() + } + if m.tx != nil { + return m.tx.Transaction(ctx, f) + } + return m.db.Transaction(ctx, f) +} diff --git a/database/gdb/gdb_model_update.go b/database/gdb/gdb_model_update.go index b1f15ff62..b32d1f5f8 100644 --- a/database/gdb/gdb_model_update.go +++ b/database/gdb/gdb_model_update.go @@ -9,12 +9,13 @@ package gdb import ( "database/sql" "fmt" + "reflect" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gstr" "github.com/gogf/gf/util/gconv" "github.com/gogf/gf/util/gutil" - "reflect" ) // Update does "UPDATE ... " statement for the model. @@ -82,6 +83,7 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro return nil, gerror.New("there should be WHERE condition statement for UPDATE operation") } return m.db.DoUpdate( + m.GetCtx(), m.getLink(true), m.tables, newData, @@ -89,3 +91,19 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro m.mergeArguments(conditionArgs)..., ) } + +// Increment increments a column's value by a given amount. +func (m *Model) Increment(column string, amount float64) (sql.Result, error) { + return m.getModel().Data(column, &Counter{ + Field: column, + Value: amount, + }).Update() +} + +// Decrement decrements a column's value by a given amount. +func (m *Model) Decrement(column string, amount float64) (sql.Result, error) { + return m.getModel().Data(column, &Counter{ + Field: column, + Value: -amount, + }).Update() +} diff --git a/database/gdb/gdb_model_utility.go b/database/gdb/gdb_model_utility.go index 93ede1a04..7b1c36b60 100644 --- a/database/gdb/gdb_model_utility.go +++ b/database/gdb/gdb_model_utility.go @@ -7,17 +7,31 @@ package gdb import ( - "fmt" + "time" + "github.com/gogf/gf/container/gset" "github.com/gogf/gf/internal/empty" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/text/gstr" - "github.com/gogf/gf/util/gconv" "github.com/gogf/gf/util/gutil" - "time" ) +// TableFields retrieves and returns the fields information of specified table of current +// schema. +// +// Also see DriverMysql.TableFields. +func (m *Model) TableFields(table string, schema ...string) (fields map[string]*TableField, err error) { + charL, charR := m.db.GetChars() + if charL != "" || charR != "" { + table = gstr.Trim(table, charL+charR) + } + if !gregex.IsMatchString(regularFieldNameRegPattern, table) { + return nil, nil + } + return m.db.TableFields(m.GetCtx(), table, schema...) +} + // getModel creates and returns a cloned model of current model if `safe` is true, or else it returns // the current model. func (m *Model) getModel() *Model { @@ -33,7 +47,7 @@ func (m *Model) getModel() *Model { // ID -> id // NICK_Name -> nickname func (m *Model) mappingAndFilterToTableFields(fields []string, filter bool) []string { - fieldsMap, err := m.db.TableFields(m.tables) + fieldsMap, err := m.TableFields(m.tables) if err != nil || len(fieldsMap) == 0 { return fields } @@ -92,7 +106,7 @@ func (m *Model) filterDataForInsertOrUpdate(data interface{}) (interface{}, erro // Note that, it does not filter list item, which is also type of map, for "omit empty" feature. func (m *Model) doMappingAndFilterForInsertOrUpdateDataMap(data Map, allowOmitEmpty bool) (Map, error) { var err error - data, err = m.db.mappingAndFilterData(m.schema, m.tables, data, m.filter) + data, err = m.db.GetCore().mappingAndFilterData(m.schema, m.tables, data, m.filter) if err != nil { return nil, err } @@ -156,7 +170,7 @@ func (m *Model) doMappingAndFilterForInsertOrUpdateDataMap(data Map, allowOmitEm // The parameter `master` specifies whether using the master node if master-slave configured. func (m *Model) getLink(master bool) Link { if m.tx != nil { - return m.tx.tx + return &txLink{m.tx.tx} } linkType := m.linkType if linkType == 0 { @@ -168,13 +182,13 @@ func (m *Model) getLink(master bool) Link { } switch linkType { case linkTypeMaster: - link, err := m.db.GetMaster(m.schema) + link, err := m.db.GetCore().MasterLink(m.schema) if err != nil { panic(err) } return link case linkTypeSlave: - link, err := m.db.GetSlave(m.schema) + link, err := m.db.GetCore().SlaveLink(m.schema) if err != nil { panic(err) } @@ -188,7 +202,7 @@ func (m *Model) getLink(master bool) Link { // "user", "user u", "user as u, user_detail as ud". func (m *Model) getPrimaryKey() string { table := gstr.SplitAndTrim(m.tables, " ")[0] - tableFields, err := m.db.TableFields(table) + tableFields, err := m.TableFields(table) if err != nil { return "" } @@ -200,113 +214,6 @@ func (m *Model) getPrimaryKey() string { return "" } -// formatCondition formats where arguments of the model and returns a new condition sql and its arguments. -// Note that this function does not change any attribute value of the `m`. -// -// The parameter `limit1` specifies whether limits querying only one record if m.limit is not set. -func (m *Model) formatCondition(limit1 bool, isCountStatement bool) (conditionWhere string, conditionExtra string, conditionArgs []interface{}) { - if len(m.whereHolder) > 0 { - for _, v := range m.whereHolder { - switch v.operator { - case whereHolderWhere: - if conditionWhere == "" { - newWhere, newArgs := formatWhere( - m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, - ) - if len(newWhere) > 0 { - conditionWhere = newWhere - conditionArgs = newArgs - } - continue - } - fallthrough - - case whereHolderAnd: - newWhere, newArgs := formatWhere( - m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, - ) - if len(newWhere) > 0 { - if len(conditionWhere) == 0 { - conditionWhere = newWhere - } else if conditionWhere[0] == '(' { - conditionWhere = fmt.Sprintf(`%s AND (%s)`, conditionWhere, newWhere) - } else { - conditionWhere = fmt.Sprintf(`(%s) AND (%s)`, conditionWhere, newWhere) - } - conditionArgs = append(conditionArgs, newArgs...) - } - - case whereHolderOr: - newWhere, newArgs := formatWhere( - m.db, v.where, v.args, m.option&OptionOmitEmpty > 0, - ) - if len(newWhere) > 0 { - if len(conditionWhere) == 0 { - conditionWhere = newWhere - } else if conditionWhere[0] == '(' { - conditionWhere = fmt.Sprintf(`%s OR (%s)`, conditionWhere, newWhere) - } else { - conditionWhere = fmt.Sprintf(`(%s) OR (%s)`, conditionWhere, newWhere) - } - conditionArgs = append(conditionArgs, newArgs...) - } - } - } - } - // Soft deletion. - softDeletingCondition := m.getConditionForSoftDeleting() - if !m.unscoped && softDeletingCondition != "" { - if conditionWhere == "" { - conditionWhere = fmt.Sprintf(` WHERE %s`, softDeletingCondition) - } else { - conditionWhere = fmt.Sprintf(` WHERE (%s) AND %s`, conditionWhere, softDeletingCondition) - } - } else { - if conditionWhere != "" { - conditionWhere = " WHERE " + conditionWhere - } - } - // GROUP BY. - if m.groupBy != "" { - conditionExtra += " GROUP BY " + m.groupBy - } - // HAVING. - if len(m.having) > 0 { - havingStr, havingArgs := formatWhere( - m.db, m.having[0], gconv.Interfaces(m.having[1]), m.option&OptionOmitEmpty > 0, - ) - if len(havingStr) > 0 { - conditionExtra += " HAVING " + havingStr - conditionArgs = append(conditionArgs, havingArgs...) - } - } - // ORDER BY. - if m.orderBy != "" { - conditionExtra += " ORDER BY " + m.orderBy - } - // LIMIT. - if !isCountStatement { - if m.limit != 0 { - if m.start >= 0 { - conditionExtra += fmt.Sprintf(" LIMIT %d,%d", m.start, m.limit) - } else { - conditionExtra += fmt.Sprintf(" LIMIT %d", m.limit) - } - } else if limit1 { - conditionExtra += " LIMIT 1" - } - - if m.offset >= 0 { - conditionExtra += fmt.Sprintf(" OFFSET %d", m.offset) - } - } - - if m.lockInfo != "" { - conditionExtra += " " + m.lockInfo - } - return -} - // mergeArguments creates and returns new arguments by merging and given `args`. func (m *Model) mergeArguments(args []interface{}) []interface{} { if len(m.extraArgs) > 0 { diff --git a/database/gdb/gdb_model_with.go b/database/gdb/gdb_model_with.go index 8ead1401c..1fbd891d3 100644 --- a/database/gdb/gdb_model_with.go +++ b/database/gdb/gdb_model_with.go @@ -8,12 +8,13 @@ package gdb import ( "fmt" + "reflect" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/structs" "github.com/gogf/gf/internal/utils" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/text/gstr" - "reflect" ) // With creates and returns an ORM model based on meta data of given object. @@ -32,13 +33,17 @@ import ( // db.With(User{}.UserDetail).With(User{}.UserDetail).Scan(xxx) // Or: // db.With(UserDetail{}).With(UserDetail{}).Scan(xxx) -func (m *Model) With(object interface{}) *Model { +// Or: +// db.With(UserDetail{}, UserDetail{}).Scan(xxx) +func (m *Model) With(objects ...interface{}) *Model { model := m.getModel() - if m.tables == "" { - m.tables = m.db.QuotePrefixTableName(getTableNameFromOrmTag(object)) - return model + for _, object := range objects { + if m.tables == "" { + m.tables = m.db.GetCore().QuotePrefixTableName(getTableNameFromOrmTag(object)) + return model + } + model.withArray = append(model.withArray, object) } - model.withArray = append(model.withArray, object) return model } @@ -49,18 +54,41 @@ func (m *Model) WithAll() *Model { return model } -// getWithTagObjectArrayFrom retrieves and returns object array that have "with" tag in the struct. -func (m *Model) getWithTagObjectArrayFrom(pointer interface{}) ([]interface{}, error) { - fieldMap, err := structs.FieldMap(pointer, nil) +// doWithScanStruct handles model association operations feature for single struct. +func (m *Model) doWithScanStruct(pointer interface{}) error { + var ( + err error + allowedTypeStrArray = make([]string, 0) + ) + fieldMap, err := structs.FieldMap(pointer, nil, false) if err != nil { - return nil, err + return err } - withTagObjectArray := make([]interface{}, 0) - for _, fieldValue := range fieldMap { + // It checks the with array and automatically calls the ScanList to complete association querying. + if !m.withAll { + for _, field := range fieldMap { + for _, withItem := range m.withArray { + withItemReflectValueType, err := structs.StructType(withItem) + if err != nil { + return err + } + var ( + fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]") + withItemReflectValueTypeStr = gstr.TrimAll(withItemReflectValueType.String(), "*[]") + ) + // It does select operation if the field type is in the specified with type array. + if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 { + allowedTypeStrArray = append(allowedTypeStrArray, fieldTypeStr) + } + } + } + } + for _, field := range fieldMap { var ( - withTag string - ormTag = fieldValue.Tag(OrmTagForStruct) - match, _ = gregex.MatchString( + withTag string + ormTag = field.Tag(OrmTagForStruct) + fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]") + match, _ = gregex.MatchString( fmt.Sprintf(`%s\s*:\s*([^,]+)`, OrmTagForWith), ormTag, ) @@ -71,186 +99,163 @@ func (m *Model) getWithTagObjectArrayFrom(pointer interface{}) ([]interface{}, e if withTag == "" { continue } - withTagObjectArray = append(withTagObjectArray, fieldValue.Value.Interface()) - } - return withTagObjectArray, nil -} - -// doWithScanStruct handles model association operations feature for single struct. -func (m *Model) doWithScanStruct(pointer interface{}) error { - var ( - err error - withArray = m.withArray - ) - if m.withAll { - withArray, err = m.getWithTagObjectArrayFrom(pointer) - if err != nil { - return err + if !m.withAll && !gstr.InArray(allowedTypeStrArray, fieldTypeStr) { + continue } - } - if len(withArray) == 0 { - return nil - } - fieldMap, err := structs.FieldMap(pointer, nil) - if err != nil { - return err - } - for withIndex, withItem := range withArray { - withItemReflectValueType, err := structs.StructType(withItem) - if err != nil { - return err + array := gstr.SplitAndTrim(withTag, "=") + if len(array) == 1 { + // It supports using only one column name + // if both tables associates using the same column name. + array = append(array, withTag) } - withItemReflectValueTypeStr := gstr.TrimAll(withItemReflectValueType.String(), "*[]") - for _, fieldValue := range fieldMap { - var ( - fieldType = fieldValue.Type() - fieldTypeStr = gstr.TrimAll(fieldType.String(), "*[]") - ) - if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 { - var ( - withTag string - ormTag = fieldValue.Tag(OrmTagForStruct) - match, _ = gregex.MatchString( - fmt.Sprintf(`%s\s*:\s*([^,]+)`, OrmTagForWith), - ormTag, - ) - ) - if len(match) > 1 { - withTag = match[1] - } - if withTag == "" { - continue - } - array := gstr.SplitAndTrim(withTag, "=") - if len(array) != 2 { - return gerror.Newf(`invalid with tag "%s"`, withTag) - } - var ( - relatedFieldName = array[0] - relatedAttrName = array[1] - relatedFieldValue interface{} - ) - // Find the value of related attribute from `pointer`. - for attributeName, attributeValue := range fieldMap { - if utils.EqualFoldWithoutChars(attributeName, relatedAttrName) { - relatedFieldValue = attributeValue.Value.Interface() - break - } - } - if relatedFieldValue == nil { - return gerror.Newf( - `cannot find the related value for attribute name "%s" of with tag "%s"`, - relatedAttrName, withTag, - ) - } - bindToReflectValue := fieldValue.Value - switch bindToReflectValue.Kind() { - case reflect.Array, reflect.Slice: - if bindToReflectValue.CanAddr() { - bindToReflectValue = bindToReflectValue.Addr() - } - } - model := m.db.With(fieldValue.Value) - for i, v := range withArray { - if i == withIndex { - continue - } - model = model.With(v) - } - err = model.Fields(withItemReflectValueType.FieldKeys()). - Where(relatedFieldName, relatedFieldValue). - Scan(bindToReflectValue) - if err != nil { - return err - } + var ( + model *Model + fieldKeys []string + relatedFieldName = array[0] + relatedAttrName = array[1] + relatedFieldValue interface{} + ) + // Find the value of related attribute from `pointer`. + for attributeName, attributeValue := range fieldMap { + if utils.EqualFoldWithoutChars(attributeName, relatedAttrName) { + relatedFieldValue = attributeValue.Value.Interface() + break } } + if relatedFieldValue == nil { + return gerror.Newf( + `cannot find the related value for attribute name "%s" of with tag "%s"`, + relatedAttrName, withTag, + ) + } + bindToReflectValue := field.Value + switch bindToReflectValue.Kind() { + case reflect.Array, reflect.Slice: + if bindToReflectValue.CanAddr() { + bindToReflectValue = bindToReflectValue.Addr() + } + } + + // It automatically retrieves struct field names from current attribute struct/slice. + if structType, err := structs.StructType(field.Value); err != nil { + return err + } else { + fieldKeys = structType.FieldKeys() + } + + // Recursively with feature checks. + model = m.db.With(field.Value) + if m.withAll { + model = model.WithAll() + } else { + model = model.With(m.withArray...) + } + + err = model.Fields(fieldKeys).Where(relatedFieldName, relatedFieldValue).Scan(bindToReflectValue) + if err != nil { + return err + } + } return nil } // doWithScanStructs handles model association operations feature for struct slice. +// Also see doWithScanStruct. func (m *Model) doWithScanStructs(pointer interface{}) error { var ( - err error - withArray = m.withArray + err error + allowedTypeStrArray = make([]string, 0) ) - if m.withAll { - withArray, err = m.getWithTagObjectArrayFrom(pointer) - if err != nil { - return err - } - } - if len(withArray) == 0 { - return nil - } - fieldMap, err := structs.FieldMap(pointer, nil) + fieldMap, err := structs.FieldMap(pointer, nil, false) if err != nil { return err } - for withIndex, withItem := range withArray { - withItemReflectValueType, err := structs.StructType(withItem) - if err != nil { - return err - } - withItemReflectValueTypeStr := gstr.TrimAll(withItemReflectValueType.String(), "*[]") - for fieldName, fieldValue := range fieldMap { - var ( - fieldType = fieldValue.Type() - fieldTypeStr = gstr.TrimAll(fieldType.String(), "*[]") - ) - if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 { - var ( - withTag string - ormTag = fieldValue.Tag(OrmTagForStruct) - match, _ = gregex.MatchString( - fmt.Sprintf(`%s\s*:\s*([^,]+)`, OrmTagForWith), - ormTag, - ) - ) - if len(match) > 1 { - withTag = match[1] - } - if withTag == "" { - continue - } - array := gstr.SplitAndTrim(withTag, "=") - if len(array) != 2 { - return gerror.Newf(`invalid with tag "%s"`, withTag) - } - var ( - relatedFieldName = array[0] - relatedAttrName = array[1] - relatedFieldValue interface{} - ) - // Find the value slice of related attribute from `pointer`. - for attributeName, _ := range fieldMap { - if utils.EqualFoldWithoutChars(attributeName, relatedAttrName) { - relatedFieldValue = ListItemValuesUnique(pointer, attributeName) - break - } - } - if relatedFieldValue == nil { - return gerror.Newf( - `cannot find the related value for attribute name "%s" of with tag "%s"`, - relatedAttrName, withTag, - ) - } - model := m.db.With(fieldValue.Value) - for i, v := range withArray { - if i == withIndex { - continue - } - model = model.With(v) - } - err = model.Fields(withItemReflectValueType.FieldKeys()). - Where(relatedFieldName, relatedFieldValue). - ScanList(pointer, fieldName, withTag) + // It checks the with array and automatically calls the ScanList to complete association querying. + if !m.withAll { + for _, field := range fieldMap { + for _, withItem := range m.withArray { + withItemReflectValueType, err := structs.StructType(withItem) if err != nil { return err } + var ( + fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]") + withItemReflectValueTypeStr = gstr.TrimAll(withItemReflectValueType.String(), "*[]") + ) + // It does select operation if the field type is in the specified with type array. + if gstr.Compare(fieldTypeStr, withItemReflectValueTypeStr) == 0 { + allowedTypeStrArray = append(allowedTypeStrArray, fieldTypeStr) + } } } } + + for fieldName, field := range fieldMap { + var ( + withTag string + ormTag = field.Tag(OrmTagForStruct) + fieldTypeStr = gstr.TrimAll(field.Type().String(), "*[]") + match, _ = gregex.MatchString( + fmt.Sprintf(`%s\s*:\s*([^,]+)`, OrmTagForWith), + ormTag, + ) + ) + if len(match) > 1 { + withTag = match[1] + } + if withTag == "" { + continue + } + if !m.withAll && !gstr.InArray(allowedTypeStrArray, fieldTypeStr) { + continue + } + array := gstr.SplitAndTrim(withTag, "=") + if len(array) == 1 { + // It supports using only one column name + // if both tables associates using the same column name. + array = append(array, withTag) + } + var ( + model *Model + fieldKeys []string + relatedFieldName = array[0] + relatedAttrName = array[1] + relatedFieldValue interface{} + ) + // Find the value slice of related attribute from `pointer`. + for attributeName, _ := range fieldMap { + if utils.EqualFoldWithoutChars(attributeName, relatedAttrName) { + relatedFieldValue = ListItemValuesUnique(pointer, attributeName) + break + } + } + if relatedFieldValue == nil { + return gerror.Newf( + `cannot find the related value for attribute name "%s" of with tag "%s"`, + relatedAttrName, withTag, + ) + } + + // It automatically retrieves struct field names from current attribute struct/slice. + if structType, err := structs.StructType(field.Value); err != nil { + return err + } else { + fieldKeys = structType.FieldKeys() + } + + // Recursively with feature checks. + model = m.db.With(field.Value) + if m.withAll { + model = model.WithAll() + } else { + model = model.With(m.withArray...) + } + + err = model.Fields(fieldKeys).Where(relatedFieldName, relatedFieldValue).ScanList(pointer, fieldName, withTag) + if err != nil { + return err + } + } return nil } diff --git a/database/gdb/gdb_statement.go b/database/gdb/gdb_statement.go index e856ba0b0..81f14affe 100644 --- a/database/gdb/gdb_statement.go +++ b/database/gdb/gdb_statement.go @@ -9,6 +9,7 @@ package gdb import ( "context" "database/sql" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/os/gtime" ) @@ -25,6 +26,7 @@ import ( type Stmt struct { *sql.Stmt core *Core + link Link sql string } @@ -62,19 +64,21 @@ func (s *Stmt) doStmtCommit(stmtType string, ctx context.Context, args ...interf var ( timestampMilli2 = gtime.TimestampMilli() sqlObj = &Sql{ - Sql: s.sql, - Type: stmtType, - Args: args, - Format: FormatSqlWithArgs(s.sql, args), - Error: err, - Start: timestampMilli1, - End: timestampMilli2, - Group: s.core.db.GetGroup(), + Sql: s.sql, + Type: stmtType, + Args: args, + Format: FormatSqlWithArgs(s.sql, args), + Error: err, + Start: timestampMilli1, + End: timestampMilli2, + Group: s.core.db.GetGroup(), + IsTransaction: s.link.IsTransaction(), } ) + // Tracing and logging. s.core.addSqlToTracing(ctx, sqlObj) if s.core.db.GetDebug() { - s.core.writeSqlToLogger(sqlObj) + s.core.writeSqlToLogger(ctx, sqlObj) } return result, err } diff --git a/database/gdb/gdb_transaction.go b/database/gdb/gdb_transaction.go deleted file mode 100644 index 5f4a35f79..000000000 --- a/database/gdb/gdb_transaction.go +++ /dev/null @@ -1,282 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gdb - -import ( - "database/sql" - "fmt" - "reflect" - - "github.com/gogf/gf/text/gregex" -) - -// TX is the struct for transaction management. -type TX struct { - db DB - tx *sql.Tx - master *sql.DB -} - -// Commit commits the transaction. -func (tx *TX) Commit() error { - return tx.tx.Commit() -} - -// Rollback aborts the transaction. -func (tx *TX) Rollback() error { - return tx.tx.Rollback() -} - -// Query does query operation on transaction. -// See Core.Query. -func (tx *TX) Query(sql string, args ...interface{}) (rows *sql.Rows, err error) { - return tx.db.DoQuery(tx.tx, sql, args...) -} - -// Exec does none query operation on transaction. -// See Core.Exec. -func (tx *TX) Exec(sql string, args ...interface{}) (sql.Result, error) { - return tx.db.DoExec(tx.tx, sql, args...) -} - -// Prepare creates a prepared statement for later queries or executions. -// Multiple queries or executions may be run concurrently from the -// returned statement. -// The caller must call the statement's Close method -// when the statement is no longer needed. -func (tx *TX) Prepare(sql string) (*Stmt, error) { - return tx.db.DoPrepare(tx.tx, sql) -} - -// GetAll queries and returns data records from database. -func (tx *TX) GetAll(sql string, args ...interface{}) (Result, error) { - rows, err := tx.Query(sql, args...) - if err != nil || rows == nil { - return nil, err - } - defer rows.Close() - return tx.db.convertRowsToResult(rows) -} - -// GetOne queries and returns one record from database. -func (tx *TX) GetOne(sql string, args ...interface{}) (Record, error) { - list, err := tx.GetAll(sql, args...) - if err != nil { - return nil, err - } - if len(list) > 0 { - return list[0], nil - } - return nil, nil -} - -// GetStruct queries one record from database and converts it to given struct. -// The parameter `pointer` should be a pointer to struct. -func (tx *TX) GetStruct(obj interface{}, sql string, args ...interface{}) error { - one, err := tx.GetOne(sql, args...) - if err != nil { - return err - } - return one.Struct(obj) -} - -// GetStructs queries records from database and converts them to given struct. -// The parameter `pointer` should be type of struct slice: []struct/[]*struct. -func (tx *TX) GetStructs(objPointerSlice interface{}, sql string, args ...interface{}) error { - all, err := tx.GetAll(sql, args...) - if err != nil { - return err - } - return all.Structs(objPointerSlice) -} - -// GetScan queries one or more records from database and converts them to given struct or -// struct array. -// -// If parameter `pointer` is type of struct pointer, it calls GetStruct internally for -// the conversion. If parameter `pointer` is type of slice, it calls GetStructs internally -// for conversion. -func (tx *TX) GetScan(objPointer interface{}, sql string, args ...interface{}) error { - t := reflect.TypeOf(objPointer) - k := t.Kind() - if k != reflect.Ptr { - return fmt.Errorf("params should be type of pointer, but got: %v", k) - } - k = t.Elem().Kind() - switch k { - case reflect.Array, reflect.Slice: - return tx.db.GetStructs(objPointer, sql, args...) - case reflect.Struct: - return tx.db.GetStruct(objPointer, sql, args...) - default: - return fmt.Errorf("element type should be type of struct/slice, unsupported: %v", k) - } -} - -// GetValue queries and returns the field value from database. -// The sql should queries only one field from database, or else it returns only one -// field of the result. -func (tx *TX) GetValue(sql string, args ...interface{}) (Value, error) { - one, err := tx.GetOne(sql, args...) - if err != nil { - return nil, err - } - for _, v := range one { - return v, nil - } - return nil, nil -} - -// GetCount queries and returns the count from database. -func (tx *TX) GetCount(sql string, args ...interface{}) (int, error) { - if !gregex.IsMatchString(`(?i)SELECT\s+COUNT\(.+\)\s+FROM`, sql) { - sql, _ = gregex.ReplaceString(`(?i)(SELECT)\s+(.+)\s+(FROM)`, `$1 COUNT($2) $3`, sql) - } - value, err := tx.GetValue(sql, args...) - if err != nil { - return 0, err - } - return value.Int(), nil -} - -// Insert does "INSERT INTO ..." statement for the table. -// If there's already one unique record of the data in the table, it returns error. -// -// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. -// Eg: -// Data(g.Map{"uid": 10000, "name":"john"}) -// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) -// -// The parameter `batch` specifies the batch operation count when given data is slice. -func (tx *TX) Insert(table string, data interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(data).Batch(batch[0]).Insert() - } - return tx.Model(table).Data(data).Insert() -} - -// InsertIgnore does "INSERT IGNORE INTO ..." statement for the table. -// If there's already one unique record of the data in the table, it ignores the inserting. -// -// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. -// Eg: -// Data(g.Map{"uid": 10000, "name":"john"}) -// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) -// -// The parameter `batch` specifies the batch operation count when given data is slice. -func (tx *TX) InsertIgnore(table string, data interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(data).Batch(batch[0]).InsertIgnore() - } - return tx.Model(table).Data(data).InsertIgnore() -} - -// Replace does "REPLACE INTO ..." statement for the table. -// If there's already one unique record of the data in the table, it deletes the record -// and inserts a new one. -// -// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. -// Eg: -// Data(g.Map{"uid": 10000, "name":"john"}) -// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) -// -// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. -// If given data is type of slice, it then does batch replacing, and the optional parameter -// `batch` specifies the batch operation count. -func (tx *TX) Replace(table string, data interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(data).Batch(batch[0]).Replace() - } - return tx.Model(table).Data(data).Replace() -} - -// Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the table. -// It updates the record if there's primary or unique index in the saving data, -// or else it inserts a new record into the table. -// -// The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc. -// Eg: -// Data(g.Map{"uid": 10000, "name":"john"}) -// Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"}) -// -// If given data is type of slice, it then does batch saving, and the optional parameter -// `batch` specifies the batch operation count. -func (tx *TX) Save(table string, data interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(data).Batch(batch[0]).Save() - } - return tx.Model(table).Data(data).Save() -} - -// BatchInsert batch inserts data. -// The parameter `list` must be type of slice of map or struct. -func (tx *TX) BatchInsert(table string, list interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(list).Batch(batch[0]).Insert() - } - return tx.Model(table).Data(list).Insert() -} - -// BatchInsert batch inserts data with ignore option. -// The parameter `list` must be type of slice of map or struct. -func (tx *TX) BatchInsertIgnore(table string, list interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(list).Batch(batch[0]).InsertIgnore() - } - return tx.Model(table).Data(list).InsertIgnore() -} - -// BatchReplace batch replaces data. -// The parameter `list` must be type of slice of map or struct. -func (tx *TX) BatchReplace(table string, list interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(list).Batch(batch[0]).Replace() - } - return tx.Model(table).Data(list).Replace() -} - -// BatchSave batch replaces data. -// The parameter `list` must be type of slice of map or struct. -func (tx *TX) BatchSave(table string, list interface{}, batch ...int) (sql.Result, error) { - if len(batch) > 0 { - return tx.Model(table).Data(list).Batch(batch[0]).Save() - } - return tx.Model(table).Data(list).Save() -} - -// Update does "UPDATE ... " statement for the table. -// -// The parameter `data` can be type of string/map/gmap/struct/*struct, etc. -// Eg: "uid=10000", "uid", 10000, g.Map{"uid": 10000, "name":"john"} -// -// The parameter `condition` can be type of string/map/gmap/slice/struct/*struct, etc. -// It is commonly used with parameter `args`. -// Eg: -// "uid=10000", -// "uid", 10000 -// "money>? AND name like ?", 99999, "vip_%" -// "status IN (?)", g.Slice{1,2,3} -// "age IN(?,?)", 18, 50 -// User{ Id : 1, UserName : "john"} -func (tx *TX) Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) { - return tx.Model(table).Data(data).Where(condition, args...).Update() -} - -// Delete does "DELETE FROM ... " statement for the table. -// -// The parameter `condition` can be type of string/map/gmap/slice/struct/*struct, etc. -// It is commonly used with parameter `args`. -// Eg: -// "uid=10000", -// "uid", 10000 -// "money>? AND name like ?", 99999, "vip_%" -// "status IN (?)", g.Slice{1,2,3} -// "age IN(?,?)", 18, 50 -// User{ Id : 1, UserName : "john"} -func (tx *TX) Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) { - return tx.Model(table).Where(condition, args...).Delete() -} diff --git a/database/gdb/gdb_z_driver_test.go b/database/gdb/gdb_z_driver_test.go index 80686d203..e166856d9 100644 --- a/database/gdb/gdb_z_driver_test.go +++ b/database/gdb/gdb_z_driver_test.go @@ -7,11 +7,13 @@ package gdb_test import ( + "context" + "testing" + "github.com/gogf/gf/container/gtype" "github.com/gogf/gf/database/gdb" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/test/gtest" - "testing" ) // MyDriver is a custom database driver, which is used for testing only. @@ -41,9 +43,9 @@ func (d *MyDriver) New(core *gdb.Core, node *gdb.ConfigNode) (gdb.DB, error) { // HandleSqlBeforeCommit handles the sql before posts it to database. // It here overwrites the same method of gdb.DriverMysql and makes some custom changes. -func (d *MyDriver) HandleSqlBeforeCommit(link gdb.Link, sql string, args []interface{}) (string, []interface{}) { +func (d *MyDriver) HandleSqlBeforeCommit(ctx context.Context, link gdb.Link, sql string, args []interface{}) (string, []interface{}) { latestSqlString.Set(sql) - return d.DriverMysql.HandleSqlBeforeCommit(link, sql, args) + return d.DriverMysql.HandleSqlBeforeCommit(ctx, link, sql, args) } func init() { diff --git a/database/gdb/gdb_z_example_test.go b/database/gdb/gdb_z_example_test.go index 4ede0e233..8b692a788 100644 --- a/database/gdb/gdb_z_example_test.go +++ b/database/gdb/gdb_z_example_test.go @@ -7,12 +7,14 @@ package gdb_test import ( + "context" + "github.com/gogf/gf/database/gdb" "github.com/gogf/gf/frame/g" ) func Example_transaction() { - db.Transaction(func(tx *gdb.TX) error { + db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { // user result, err := tx.Insert("user", g.Map{ "passport": "john", diff --git a/database/gdb/gdb_z_init_test.go b/database/gdb/gdb_z_init_test.go index fe9fcc47a..abe205257 100644 --- a/database/gdb/gdb_z_init_test.go +++ b/database/gdb/gdb_z_init_test.go @@ -52,7 +52,7 @@ func init() { Weight: 1, MaxIdleConnCount: 10, MaxOpenConnCount: 10, - MaxConnLifetime: 600, + MaxConnLifeTime: 600, } nodePrefix := configNode nodePrefix.Prefix = TableNamePrefix1 diff --git a/database/gdb/gdb_z_mysql_association_test.go b/database/gdb/gdb_z_mysql_association_scanlist_test.go similarity index 99% rename from database/gdb/gdb_z_mysql_association_test.go rename to database/gdb/gdb_z_mysql_association_scanlist_test.go index 4aa3b11b8..54bf0ad9e 100644 --- a/database/gdb/gdb_z_mysql_association_test.go +++ b/database/gdb/gdb_z_mysql_association_scanlist_test.go @@ -7,11 +7,13 @@ package gdb_test import ( + "context" "fmt" + "testing" + "github.com/gogf/gf/database/gdb" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gconv" - "testing" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/test/gtest" @@ -84,7 +86,7 @@ CREATE TABLE %s ( // Initialize the data. var err error gtest.C(t, func(t *gtest.T) { - err = db.Transaction(func(tx *gdb.TX) error { + err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { r, err := tx.Model(tableUser).Save(EntityUser{ Name: "john", }) diff --git a/database/gdb/gdb_z_mysql_association_with_test.go b/database/gdb/gdb_z_mysql_association_with_test.go index f4633f50b..43a0f3ab7 100644 --- a/database/gdb/gdb_z_mysql_association_with_test.go +++ b/database/gdb/gdb_z_mysql_association_with_test.go @@ -18,7 +18,7 @@ func Test_Table_Relation_With_Scan(t *testing.T) { var ( tableUser = "user" tableUserDetail = "user_detail" - tableUserScores = "user_scores" + tableUserScores = "user_score" ) if _, err := db.Exec(fmt.Sprintf(` CREATE TABLE IF NOT EXISTS %s ( @@ -60,8 +60,8 @@ PRIMARY KEY (id) Address string `json:"address"` } - type UserScores struct { - gmeta.Meta `orm:"table:user_scores"` + type UserScore struct { + gmeta.Meta `orm:"table:user_score"` Id int `json:"id"` Uid int `json:"uid"` Score int `json:"score"` @@ -69,34 +69,61 @@ PRIMARY KEY (id) type User struct { gmeta.Meta `orm:"table:user"` - Id int `json:"id"` - Name string `json:"name"` - UserDetail *UserDetail `orm:"with:uid=id"` - UserScores []*UserScores `orm:"with:uid=id"` + Id int `json:"id"` + Name string `json:"name"` + UserDetail *UserDetail `orm:"with:uid=id"` + UserScores []*UserScore `orm:"with:uid=id"` } // Initialize the data. - var err error + gtest.C(t, func(t *gtest.T) { + for i := 1; i <= 5; i++ { + // User. + user := User{ + Name: fmt.Sprintf(`name_%d`, i), + } + lastInsertId, err := db.Model(user).Data(user).OmitEmpty().InsertAndGetId() + t.AssertNil(err) + // Detail. + userDetail := UserDetail{ + Uid: int(lastInsertId), + Address: fmt.Sprintf(`address_%d`, lastInsertId), + } + _, err = db.Model(userDetail).Data(userDetail).OmitEmpty().Insert() + t.AssertNil(err) + // Scores. + for j := 1; j <= 5; j++ { + userScore := UserScore{ + Uid: int(lastInsertId), + Score: j, + } + _, err = db.Model(userScore).Data(userScore).OmitEmpty().Insert() + t.AssertNil(err) + } + } + }) for i := 1; i <= 5; i++ { // User. - _, err = db.Insert(tableUser, g.Map{ - "id": i, - "name": fmt.Sprintf(`name_%d`, i), - }) - gtest.Assert(err, nil) + user := User{ + Name: fmt.Sprintf(`name_%d`, i), + } + lastInsertId, err := db.Model(user).Data(user).OmitEmpty().InsertAndGetId() + gtest.AssertNil(err) // Detail. - _, err = db.Insert(tableUserDetail, g.Map{ - "uid": i, - "address": fmt.Sprintf(`address_%d`, i), - }) - gtest.Assert(err, nil) + userDetail := UserDetail{ + Uid: int(lastInsertId), + Address: fmt.Sprintf(`address_%d`, lastInsertId), + } + _, err = db.Model(userDetail).Data(userDetail).Insert() + gtest.AssertNil(err) // Scores. for j := 1; j <= 5; j++ { - _, err = db.Insert(tableUserScores, g.Map{ - "uid": i, - "score": j, - }) - gtest.Assert(err, nil) + userScore := UserScore{ + Uid: int(lastInsertId), + Score: j, + } + _, err = db.Model(userScore).Data(userScore).Insert() + gtest.AssertNil(err) } } gtest.C(t, func(t *gtest.T) { @@ -139,7 +166,7 @@ PRIMARY KEY (id) var user *User err := db.With(User{}). With(UserDetail{}). - With(UserScores{}). + With(UserScore{}). Where("id", 4). Scan(&user) t.AssertNil(err) @@ -185,7 +212,7 @@ PRIMARY KEY (id) }) } -func Test_Table_Relation_With_ScanList(t *testing.T) { +func Test_Table_Relation_With(t *testing.T) { var ( tableUser = "user" tableUserDetail = "user_detail" @@ -384,7 +411,7 @@ PRIMARY KEY (id) }) } -func Test_Table_Relation_WithAll_Scan(t *testing.T) { +func Test_Table_Relation_WithAll(t *testing.T) { var ( tableUser = "user" tableUserDetail = "user_detail" @@ -499,7 +526,7 @@ PRIMARY KEY (id) }) } -func Test_Table_Relation_WithAll_ScanList(t *testing.T) { +func Test_Table_Relation_WithAll_List(t *testing.T) { var ( tableUser = "user" tableUserDetail = "user_detail" @@ -639,3 +666,526 @@ PRIMARY KEY (id) t.Assert(users[1].UserScores[4].Score, 5) }) } + +func Test_Table_Relation_WithAll_Embedded(t *testing.T) { + var ( + tableUser = "user" + tableUserDetail = "user_detail" + tableUserScores = "user_scores" + ) + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +name varchar(45) NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUser)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUser) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +uid int(10) unsigned NOT NULL AUTO_INCREMENT, +address varchar(45) NOT NULL, +PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUserDetail)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserDetail) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +uid int(10) unsigned NOT NULL, +score int(10) unsigned NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + `, tableUserScores)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserScores) + + type UserDetail struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + } + + type UserScores struct { + gmeta.Meta `orm:"table:user_scores"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + type User struct { + gmeta.Meta `orm:"table:user"` + *UserDetail `orm:"with:uid=id"` + Id int `json:"id"` + Name string `json:"name"` + UserScores []*UserScores `orm:"with:uid=id"` + } + + // Initialize the data. + var err error + for i := 1; i <= 5; i++ { + // User. + _, err = db.Insert(tableUser, g.Map{ + "id": i, + "name": fmt.Sprintf(`name_%d`, i), + }) + gtest.Assert(err, nil) + // Detail. + _, err = db.Insert(tableUserDetail, g.Map{ + "uid": i, + "address": fmt.Sprintf(`address_%d`, i), + }) + gtest.Assert(err, nil) + // Scores. + for j := 1; j <= 5; j++ { + _, err = db.Insert(tableUserScores, g.Map{ + "uid": i, + "score": j, + }) + gtest.Assert(err, nil) + } + } + gtest.C(t, func(t *gtest.T) { + var user *User + err := db.Model(tableUser).WithAll().Where("id", 3).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 3) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 3) + t.Assert(user.UserDetail.Address, `address_3`) + t.Assert(len(user.UserScores), 5) + t.Assert(user.UserScores[0].Uid, 3) + t.Assert(user.UserScores[0].Score, 1) + t.Assert(user.UserScores[4].Uid, 3) + t.Assert(user.UserScores[4].Score, 5) + }) + gtest.C(t, func(t *gtest.T) { + var user User + err := db.Model(tableUser).WithAll().Where("id", 4).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 4) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 4) + t.Assert(user.UserDetail.Address, `address_4`) + t.Assert(len(user.UserScores), 5) + t.Assert(user.UserScores[0].Uid, 4) + t.Assert(user.UserScores[0].Score, 1) + t.Assert(user.UserScores[4].Uid, 4) + t.Assert(user.UserScores[4].Score, 5) + }) +} + +func Test_Table_Relation_WithAll_AttributeStructAlsoHasWithTag(t *testing.T) { + var ( + tableUser = "user" + tableUserDetail = "user_detail" + tableUserScores = "user_scores" + ) + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +name varchar(45) NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUser)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUser) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +uid int(10) unsigned NOT NULL AUTO_INCREMENT, +address varchar(45) NOT NULL, +PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUserDetail)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserDetail) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +uid int(10) unsigned NOT NULL, +score int(10) unsigned NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUserScores)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserScores) + + type UserScores struct { + gmeta.Meta `orm:"table:user_scores"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + type UserDetail struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserScores []*UserScores `orm:"with:uid"` + } + + type User struct { + gmeta.Meta `orm:"table:user"` + *UserDetail `orm:"with:uid=id"` + Id int `json:"id"` + Name string `json:"name"` + } + + // Initialize the data. + var err error + for i := 1; i <= 5; i++ { + // User. + _, err = db.Insert(tableUser, g.Map{ + "id": i, + "name": fmt.Sprintf(`name_%d`, i), + }) + gtest.Assert(err, nil) + // Detail. + _, err = db.Insert(tableUserDetail, g.Map{ + "uid": i, + "address": fmt.Sprintf(`address_%d`, i), + }) + gtest.Assert(err, nil) + // Scores. + for j := 1; j <= 5; j++ { + _, err = db.Insert(tableUserScores, g.Map{ + "uid": i, + "score": j, + }) + gtest.Assert(err, nil) + } + } + + gtest.C(t, func(t *gtest.T) { + var user *User + err := db.Model(tableUser).WithAll().Where("id", 3).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 3) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 3) + t.Assert(user.UserDetail.Address, `address_3`) + t.Assert(len(user.UserDetail.UserScores), 5) + t.Assert(user.UserDetail.UserScores[0].Uid, 3) + t.Assert(user.UserDetail.UserScores[0].Score, 1) + t.Assert(user.UserDetail.UserScores[4].Uid, 3) + t.Assert(user.UserDetail.UserScores[4].Score, 5) + }) + gtest.C(t, func(t *gtest.T) { + var user User + err := db.Model(tableUser).WithAll().Where("id", 4).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 4) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 4) + t.Assert(user.UserDetail.Address, `address_4`) + t.Assert(len(user.UserDetail.UserScores), 5) + t.Assert(user.UserDetail.UserScores[0].Uid, 4) + t.Assert(user.UserDetail.UserScores[0].Score, 1) + t.Assert(user.UserDetail.UserScores[4].Uid, 4) + t.Assert(user.UserDetail.UserScores[4].Score, 5) + }) +} + +func Test_Table_Relation_WithAll_AttributeStructAlsoHasWithTag_MoreDeep(t *testing.T) { + var ( + tableUser = "user" + tableUserDetail = "user_detail" + tableUserScores = "user_scores" + ) + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +name varchar(45) NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUser)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUser) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +uid int(10) unsigned NOT NULL AUTO_INCREMENT, +address varchar(45) NOT NULL, +PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUserDetail)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserDetail) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +uid int(10) unsigned NOT NULL, +score int(10) unsigned NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUserScores)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserScores) + + type UserScores struct { + gmeta.Meta `orm:"table:user_scores"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + type UserDetail1 struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserScores []*UserScores `orm:"with:uid"` + } + + type UserDetail2 struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserDetail1 *UserDetail1 `orm:"with:uid"` + UserScores []*UserScores `orm:"with:uid"` + } + + type UserDetail3 struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserDetail2 *UserDetail2 `orm:"with:uid"` + UserScores []*UserScores `orm:"with:uid"` + } + + type UserDetail struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserDetail3 *UserDetail3 `orm:"with:uid"` + UserScores []*UserScores `orm:"with:uid"` + } + + type User struct { + gmeta.Meta `orm:"table:user"` + *UserDetail `orm:"with:uid=id"` + Id int `json:"id"` + Name string `json:"name"` + } + + // Initialize the data. + var err error + for i := 1; i <= 5; i++ { + // User. + _, err = db.Insert(tableUser, g.Map{ + "id": i, + "name": fmt.Sprintf(`name_%d`, i), + }) + gtest.Assert(err, nil) + // Detail. + _, err = db.Insert(tableUserDetail, g.Map{ + "uid": i, + "address": fmt.Sprintf(`address_%d`, i), + }) + gtest.Assert(err, nil) + // Scores. + for j := 1; j <= 5; j++ { + _, err = db.Insert(tableUserScores, g.Map{ + "uid": i, + "score": j, + }) + gtest.Assert(err, nil) + } + } + + gtest.C(t, func(t *gtest.T) { + var user *User + err := db.Model(tableUser).WithAll().Where("id", 3).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 3) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 3) + t.Assert(user.UserDetail.UserDetail3.Uid, 3) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.Uid, 3) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.UserDetail1.Uid, 3) + t.Assert(user.UserDetail.Address, `address_3`) + t.Assert(len(user.UserDetail.UserScores), 5) + t.Assert(user.UserDetail.UserScores[0].Uid, 3) + t.Assert(user.UserDetail.UserScores[0].Score, 1) + t.Assert(user.UserDetail.UserScores[4].Uid, 3) + t.Assert(user.UserDetail.UserScores[4].Score, 5) + }) + gtest.C(t, func(t *gtest.T) { + var user User + err := db.Model(tableUser).WithAll().Where("id", 4).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 4) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 4) + t.Assert(user.UserDetail.UserDetail3.Uid, 4) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.Uid, 4) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.UserDetail1.Uid, 4) + t.Assert(user.UserDetail.Address, `address_4`) + t.Assert(len(user.UserDetail.UserScores), 5) + t.Assert(user.UserDetail.UserScores[0].Uid, 4) + t.Assert(user.UserDetail.UserScores[0].Score, 1) + t.Assert(user.UserDetail.UserScores[4].Uid, 4) + t.Assert(user.UserDetail.UserScores[4].Score, 5) + }) +} + +func Test_Table_Relation_With_AttributeStructAlsoHasWithTag_MoreDeep(t *testing.T) { + var ( + tableUser = "user" + tableUserDetail = "user_detail" + tableUserScores = "user_scores" + ) + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +name varchar(45) NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUser)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUser) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +uid int(10) unsigned NOT NULL AUTO_INCREMENT, +address varchar(45) NOT NULL, +PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUserDetail)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserDetail) + + if _, err := db.Exec(fmt.Sprintf(` +CREATE TABLE IF NOT EXISTS %s ( +id int(10) unsigned NOT NULL AUTO_INCREMENT, +uid int(10) unsigned NOT NULL, +score int(10) unsigned NOT NULL, +PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +`, tableUserScores)); err != nil { + gtest.Error(err) + } + defer dropTable(tableUserScores) + + type UserScores struct { + gmeta.Meta `orm:"table:user_scores"` + Id int `json:"id"` + Uid int `json:"uid"` + Score int `json:"score"` + } + + type UserDetail1 struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserScores []*UserScores `orm:"with:uid"` + } + + type UserDetail2 struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserDetail1 *UserDetail1 `orm:"with:uid"` + UserScores []*UserScores `orm:"with:uid"` + } + + type UserDetail3 struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserDetail2 *UserDetail2 `orm:"with:uid"` + UserScores []*UserScores `orm:"with:uid"` + } + + type UserDetail struct { + gmeta.Meta `orm:"table:user_detail"` + Uid int `json:"uid"` + Address string `json:"address"` + UserDetail3 *UserDetail3 `orm:"with:uid"` + UserScores []*UserScores `orm:"with:uid"` + } + + type User struct { + gmeta.Meta `orm:"table:user"` + *UserDetail `orm:"with:uid=id"` + Id int `json:"id"` + Name string `json:"name"` + } + + // Initialize the data. + var err error + for i := 1; i <= 5; i++ { + // User. + _, err = db.Insert(tableUser, g.Map{ + "id": i, + "name": fmt.Sprintf(`name_%d`, i), + }) + gtest.Assert(err, nil) + // Detail. + _, err = db.Insert(tableUserDetail, g.Map{ + "uid": i, + "address": fmt.Sprintf(`address_%d`, i), + }) + gtest.Assert(err, nil) + // Scores. + for j := 1; j <= 5; j++ { + _, err = db.Insert(tableUserScores, g.Map{ + "uid": i, + "score": j, + }) + gtest.Assert(err, nil) + } + } + + gtest.C(t, func(t *gtest.T) { + var user *User + err := db.Model(tableUser).With(UserDetail{}, UserDetail2{}, UserDetail3{}, UserScores{}).Where("id", 3).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 3) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 3) + t.Assert(user.UserDetail.UserDetail3.Uid, 3) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.Uid, 3) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.UserDetail1, nil) + t.Assert(user.UserDetail.Address, `address_3`) + t.Assert(len(user.UserDetail.UserScores), 5) + t.Assert(user.UserDetail.UserScores[0].Uid, 3) + t.Assert(user.UserDetail.UserScores[0].Score, 1) + t.Assert(user.UserDetail.UserScores[4].Uid, 3) + t.Assert(user.UserDetail.UserScores[4].Score, 5) + }) + gtest.C(t, func(t *gtest.T) { + var user User + err := db.Model(tableUser).With(UserDetail{}, UserDetail2{}, UserDetail3{}, UserScores{}).Where("id", 4).Scan(&user) + t.AssertNil(err) + t.Assert(user.Id, 4) + t.AssertNE(user.UserDetail, nil) + t.Assert(user.UserDetail.Uid, 4) + t.Assert(user.UserDetail.UserDetail3.Uid, 4) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.Uid, 4) + t.Assert(user.UserDetail.UserDetail3.UserDetail2.UserDetail1, nil) + t.Assert(user.UserDetail.Address, `address_4`) + t.Assert(len(user.UserDetail.UserScores), 5) + t.Assert(user.UserDetail.UserScores[0].Uid, 4) + t.Assert(user.UserDetail.UserScores[0].Score, 1) + t.Assert(user.UserDetail.UserScores[4].Uid, 4) + t.Assert(user.UserDetail.UserScores[4].Score, 5) + }) +} diff --git a/database/gdb/gdb_z_mysql_filter_test.go b/database/gdb/gdb_z_mysql_filter_test.go new file mode 100644 index 000000000..cd9c30107 --- /dev/null +++ b/database/gdb/gdb_z_mysql_filter_test.go @@ -0,0 +1,218 @@ +// 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 gdb_test + +import ( + "fmt" + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/test/gtest" + "testing" +) + +// Using filter dose not affect the outside value inside function. +func Test_Model_Insert_Filter(t *testing.T) { + // map + gtest.C(t, func(t *gtest.T) { + table := createTable() + defer dropTable(table) + data := g.Map{ + "id": 1, + "uid": 1, + "passport": "t1", + "password": "25d55ad283aa400af464c76d713c07ad", + "nickname": "name_1", + "create_time": gtime.Now().String(), + } + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := result.LastInsertId() + t.Assert(n, 1) + + t.Assert(data["uid"], 1) + }) + // slice + gtest.C(t, func(t *gtest.T) { + table := createTable() + defer dropTable(table) + data := g.List{ + g.Map{ + "id": 1, + "uid": 1, + "passport": "t1", + "password": "25d55ad283aa400af464c76d713c07ad", + "nickname": "name_1", + "create_time": gtime.Now().String(), + }, + g.Map{ + "id": 2, + "uid": 2, + "passport": "t1", + "password": "25d55ad283aa400af464c76d713c07ad", + "nickname": "name_1", + "create_time": gtime.Now().String(), + }, + } + + result, err := db.Model(table).Data(data).Insert() + t.AssertNil(err) + n, _ := result.LastInsertId() + t.Assert(n, 2) + + t.Assert(data[0]["uid"], 1) + t.Assert(data[1]["uid"], 2) + }) +} + +func Test_Model_Embedded_Filter(t *testing.T) { + table := createTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + type Base struct { + Id int + Uid int + CreateTime string + NoneExist string + } + type User struct { + Base + Passport string + Password string + Nickname string + } + result, err := db.Model(table).Data(User{ + Passport: "john-test", + Password: "123456", + Nickname: "John", + Base: Base{ + Id: 100, + Uid: 100, + CreateTime: gtime.Now().String(), + }, + }).Insert() + t.AssertNil(err) + n, _ := result.RowsAffected() + t.Assert(n, 1) + + var user *User + err = db.Model(table).Fields(user).Where("id=100").Scan(&user) + t.AssertNil(err) + t.Assert(user.Passport, "john-test") + t.Assert(user.Id, 100) + }) +} + +// This is no longer used as the filter feature is automatically enabled from GoFrame v1.16.0. +//func Test_Model_Insert_KeyFieldNameMapping_Error(t *testing.T) { +// table := createTable() +// defer dropTable(table) +// +// gtest.C(t, func(t *gtest.T) { +// type User struct { +// Id int +// Passport string +// Password string +// Nickname string +// CreateTime string +// NoneExistFiled string +// } +// data := User{ +// Id: 1, +// Passport: "user_1", +// Password: "pass_1", +// Nickname: "name_1", +// CreateTime: "2020-10-10 12:00:01", +// } +// _, err := db.Model(table).Data(data).Insert() +// t.AssertNE(err, nil) +// }) +//} + +func Test_Model_Fields_AutoFilterInJoinStatement(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var err error + table1 := "user" + table2 := "score" + table3 := "info" + if _, err := db.Exec(fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id int(11) NOT NULL AUTO_INCREMENT, + name varchar(500) NOT NULL DEFAULT '', + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; + `, table1, + )); err != nil { + t.AssertNil(err) + } + defer dropTable(table1) + _, err = db.Model(table1).Insert(g.Map{ + "id": 1, + "name": "john", + }) + t.AssertNil(err) + + if _, err := db.Exec(fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id int(11) NOT NULL AUTO_INCREMENT, + user_id int(11) NOT NULL DEFAULT 0, + number varchar(500) NOT NULL DEFAULT '', + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; + `, table2, + )); err != nil { + t.AssertNil(err) + } + defer dropTable(table2) + _, err = db.Model(table2).Insert(g.Map{ + "id": 1, + "user_id": 1, + "number": "n", + }) + t.AssertNil(err) + + if _, err := db.Exec(fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS %s ( + id int(11) NOT NULL AUTO_INCREMENT, + user_id int(11) NOT NULL DEFAULT 0, + description varchar(500) NOT NULL DEFAULT '', + PRIMARY KEY (id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; + `, table3, + )); err != nil { + t.AssertNil(err) + } + defer dropTable(table3) + _, err = db.Model(table3).Insert(g.Map{ + "id": 1, + "user_id": 1, + "description": "brief", + }) + t.AssertNil(err) + + one, err := db.Model("user"). + Where("user.id", 1). + Fields("score.number,user.name"). + LeftJoin("score", "user.id=score.user_id"). + LeftJoin("info", "info.id=info.user_id"). + Order("user.id asc"). + One() + t.AssertNil(err) + t.Assert(len(one), 2) + t.Assert(one["name"].String(), "john") + t.Assert(one["number"].String(), "n") + + one, err = db.Model("user"). + LeftJoin("score", "user.id=score.user_id"). + LeftJoin("info", "info.id=info.user_id"). + Fields("score.number,user.name"). + One() + t.AssertNil(err) + t.Assert(len(one), 2) + t.Assert(one["name"].String(), "john") + t.Assert(one["number"].String(), "n") + }) +} diff --git a/database/gdb/gdb_z_mysql_internal_test.go b/database/gdb/gdb_z_mysql_internal_test.go index 736a1c1f0..016aef2cb 100644 --- a/database/gdb/gdb_z_mysql_internal_test.go +++ b/database/gdb/gdb_z_mysql_internal_test.go @@ -8,11 +8,11 @@ package gdb import ( "fmt" - "github.com/go-sql-driver/mysql" "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/os/gcmd" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/test/gtest" + "github.com/gogf/mysql" "testing" ) @@ -45,7 +45,7 @@ func init() { Weight: 1, MaxIdleConnCount: 10, MaxOpenConnCount: 10, - MaxConnLifetime: 600, + MaxConnLifeTime: 600, } AddConfigNode(DefaultGroupName, configNode) // Default db. diff --git a/database/gdb/gdb_z_mysql_method_test.go b/database/gdb/gdb_z_mysql_method_test.go index 9db3b9f6c..d83f2f364 100644 --- a/database/gdb/gdb_z_mysql_method_test.go +++ b/database/gdb/gdb_z_mysql_method_test.go @@ -274,30 +274,31 @@ func Test_DB_Upadte_KeyFieldNameMapping(t *testing.T) { }) } -func Test_DB_Insert_KeyFieldNameMapping_Error(t *testing.T) { - table := createTable() - defer dropTable(table) - - gtest.C(t, func(t *gtest.T) { - type User struct { - Id int - Passport string - Password string - Nickname string - CreateTime string - NoneExistField string - } - data := User{ - Id: 1, - Passport: "user_1", - Password: "pass_1", - Nickname: "name_1", - CreateTime: "2020-10-10 12:00:01", - } - _, err := db.Insert(table, data) - t.AssertNE(err, nil) - }) -} +// This is no longer used as the filter feature is automatically enabled from GoFrame v1.16.0. +//func Test_DB_Insert_KeyFieldNameMapping_Error(t *testing.T) { +// table := createTable() +// defer dropTable(table) +// +// gtest.C(t, func(t *gtest.T) { +// type User struct { +// Id int +// Passport string +// Password string +// Nickname string +// CreateTime string +// NoneExistField string +// } +// data := User{ +// Id: 1, +// Passport: "user_1", +// Password: "pass_1", +// Nickname: "name_1", +// CreateTime: "2020-10-10 12:00:01", +// } +// _, err := db.Insert(table, data) +// t.AssertNE(err, nil) +// }) +//} func Test_DB_InsertIgnore(t *testing.T) { table := createInitTable() diff --git a/database/gdb/gdb_z_mysql_model_test.go b/database/gdb/gdb_z_mysql_model_test.go index 0eb1b454a..7370e5bfc 100644 --- a/database/gdb/gdb_z_mysql_model_test.go +++ b/database/gdb/gdb_z_mysql_model_test.go @@ -7,16 +7,18 @@ package gdb_test import ( + "context" "database/sql" "fmt" + "testing" + "time" + "github.com/gogf/gf/container/garray" "github.com/gogf/gf/container/gmap" "github.com/gogf/gf/debug/gdebug" "github.com/gogf/gf/encoding/gparser" "github.com/gogf/gf/os/gfile" "github.com/gogf/gf/util/gutil" - "testing" - "time" "github.com/gogf/gf/database/gdb" @@ -30,7 +32,7 @@ func Test_Model_Insert(t *testing.T) { defer dropTable(table) gtest.C(t, func(t *gtest.T) { user := db.Model(table) - result, err := user.Filter().Data(g.Map{ + result, err := user.Data(g.Map{ "id": 1, "uid": 1, "passport": "t1", @@ -42,7 +44,7 @@ func Test_Model_Insert(t *testing.T) { n, _ := result.LastInsertId() t.Assert(n, 1) - result, err = db.Model(table).Filter().Data(g.Map{ + result, err = db.Model(table).Data(g.Map{ "id": "2", "uid": "2", "passport": "t2", @@ -63,7 +65,7 @@ func Test_Model_Insert(t *testing.T) { CreateTime *gtime.Time `json:"create_time"` } // Model inserting. - result, err = db.Model(table).Filter().Data(User{ + result, err = db.Model(table).Data(User{ Id: 3, Uid: 3, Passport: "t3", @@ -77,7 +79,7 @@ func Test_Model_Insert(t *testing.T) { t.AssertNil(err) t.Assert(value.String(), "t3") - result, err = db.Model(table).Filter().Data(&User{ + result, err = db.Model(table).Data(&User{ Id: 4, Uid: 4, Passport: "t4", @@ -99,60 +101,6 @@ func Test_Model_Insert(t *testing.T) { }) } -// Using filter dose not affect the outside value inside function. -func Test_Model_Insert_Filter(t *testing.T) { - // map - gtest.C(t, func(t *gtest.T) { - table := createTable() - defer dropTable(table) - data := g.Map{ - "id": 1, - "uid": 1, - "passport": "t1", - "password": "25d55ad283aa400af464c76d713c07ad", - "nickname": "name_1", - "create_time": gtime.Now().String(), - } - result, err := db.Model(table).Filter().Data(data).Insert() - t.AssertNil(err) - n, _ := result.LastInsertId() - t.Assert(n, 1) - - t.Assert(data["uid"], 1) - }) - // slice - gtest.C(t, func(t *gtest.T) { - table := createTable() - defer dropTable(table) - data := g.List{ - g.Map{ - "id": 1, - "uid": 1, - "passport": "t1", - "password": "25d55ad283aa400af464c76d713c07ad", - "nickname": "name_1", - "create_time": gtime.Now().String(), - }, - g.Map{ - "id": 2, - "uid": 2, - "passport": "t1", - "password": "25d55ad283aa400af464c76d713c07ad", - "nickname": "name_1", - "create_time": gtime.Now().String(), - }, - } - - result, err := db.Model(table).Filter().Data(data).Insert() - t.AssertNil(err) - n, _ := result.LastInsertId() - t.Assert(n, 2) - - t.Assert(data[0]["uid"], 1) - t.Assert(data[1]["uid"], 2) - }) -} - // Fix issue: https://github.com/gogf/gf/issues/819 func Test_Model_Insert_WithStructAndSliceAttribute(t *testing.T) { table := createTable() @@ -240,31 +188,6 @@ func Test_Model_Update_KeyFieldNameMapping(t *testing.T) { }) } -func Test_Model_Insert_KeyFieldNameMapping_Error(t *testing.T) { - table := createTable() - defer dropTable(table) - - gtest.C(t, func(t *gtest.T) { - type User struct { - Id int - Passport string - Password string - Nickname string - CreateTime string - NoneExistFiled string - } - data := User{ - Id: 1, - Passport: "user_1", - Password: "pass_1", - Nickname: "name_1", - CreateTime: "2020-10-10 12:00:01", - } - _, err := db.Model(table).Data(data).Insert() - t.AssertNE(err, nil) - }) -} - func Test_Model_Insert_Time(t *testing.T) { table := createTable() defer dropTable(table) @@ -304,7 +227,7 @@ func Test_Model_BatchInsertWithArrayStruct(t *testing.T) { }) } - result, err := user.Filter().Data(array).Insert() + result, err := user.Data(array).Insert() t.AssertNil(err) n, _ := result.LastInsertId() t.Assert(n, TableSize) @@ -315,7 +238,7 @@ func Test_Model_InsertIgnore(t *testing.T) { table := createInitTable() defer dropTable(table) gtest.C(t, func(t *gtest.T) { - _, err := db.Model(table).Filter().Data(g.Map{ + _, err := db.Model(table).Data(g.Map{ "id": 1, "uid": 1, "passport": "t1", @@ -326,7 +249,7 @@ func Test_Model_InsertIgnore(t *testing.T) { t.AssertNE(err, nil) }) gtest.C(t, func(t *gtest.T) { - _, err := db.Model(table).Filter().Data(g.Map{ + _, err := db.Model(table).Data(g.Map{ "id": 1, "uid": 1, "passport": "t1", @@ -343,7 +266,7 @@ func Test_Model_Batch(t *testing.T) { gtest.C(t, func(t *gtest.T) { table := createTable() defer dropTable(table) - result, err := db.Model(table).Filter().Data(g.List{ + result, err := db.Model(table).Data(g.List{ { "id": 2, "uid": 2, @@ -2440,7 +2363,7 @@ func Test_Model_Schema1(t *testing.T) { // Model. gtest.C(t, func(t *gtest.T) { i := 1000 - _, err := db.Model(table).Schema(TestSchema1).Filter().Insert(g.Map{ + _, err := db.Model(table).Schema(TestSchema1).Insert(g.Map{ "id": i, "passport": fmt.Sprintf(`user_%d`, i), "password": fmt.Sprintf(`pass_%d`, i), @@ -2502,7 +2425,7 @@ func Test_Model_Schema2(t *testing.T) { // Schema. gtest.C(t, func(t *gtest.T) { i := 1000 - _, err := db.Schema(TestSchema1).Table(table).Filter().Insert(g.Map{ + _, err := db.Schema(TestSchema1).Table(table).Insert(g.Map{ "id": i, "passport": fmt.Sprintf(`user_%d`, i), "password": fmt.Sprintf(`pass_%d`, i), @@ -2697,8 +2620,8 @@ func Test_Model_Cache(t *testing.T) { t.AssertNil(err) t.Assert(n, 1) - err = db.Transaction(func(tx *gdb.TX) error { - one, err := tx.Table(table).Cache(time.Second, "test3").FindOne(3) + err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { + one, err := tx.Model(table).Cache(time.Second, "test3").FindOne(3) t.AssertNil(err) t.Assert(one["passport"], "user_300") return nil @@ -2721,13 +2644,13 @@ func Test_Model_Cache(t *testing.T) { t.AssertNil(err) t.Assert(n, 1) - err = db.Transaction(func(tx *gdb.TX) error { + err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { // Cache feature disabled. - one, err := tx.Table(table).Cache(time.Second, "test4").FindOne(4) + one, err := tx.Model(table).Cache(time.Second, "test4").FindOne(4) t.AssertNil(err) t.Assert(one["passport"], "user_400") // Update the cache. - r, err := tx.Table(table).Data("passport", "user_4000"). + r, err := tx.Model(table).Data("passport", "user_4000"). Cache(-1, "test4").WherePri(4).Update() t.AssertNil(err) n, err := r.RowsAffected() @@ -2778,6 +2701,11 @@ func Test_Model_Distinct(t *testing.T) { t.AssertNil(err) t.Assert(len(all), 2) }) + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where("id > 1").Distinct().Count() + t.AssertNil(err) + t.Assert(count, 9) + }) } func Test_Model_Min_Max(t *testing.T) { @@ -2982,13 +2910,13 @@ func Test_Model_HasTable(t *testing.T) { defer dropTable(table) gtest.C(t, func(t *gtest.T) { - result, err := db.HasTable(table) + result, err := db.GetCore().HasTable(table) t.Assert(result, true) t.AssertNil(err) }) gtest.C(t, func(t *gtest.T) { - result, err := db.HasTable("table12321") + result, err := db.GetCore().HasTable("table12321") t.Assert(result, false) t.AssertNil(err) }) @@ -3237,87 +3165,389 @@ func Test_Model_Fields_Map_Struct(t *testing.T) { }) } -func Test_Model_Fields_AutoFilterInJoinStatement(t *testing.T) { +func Test_Model_WhereIn(t *testing.T) { + table := createInitTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { - var err error - table1 := "user" - table2 := "score" - table3 := "info" - if _, err := db.Exec(fmt.Sprintf(` - CREATE TABLE IF NOT EXISTS %s ( - id int(11) NOT NULL AUTO_INCREMENT, - name varchar(500) NOT NULL DEFAULT '', - PRIMARY KEY (id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; - `, table1, - )); err != nil { - t.AssertNil(err) - } - defer dropTable(table1) - _, err = db.Model(table1).Insert(g.Map{ - "id": 1, - "name": "john", - }) + result, err := db.Model(table).WhereIn("id", g.Slice{1, 2, 3, 4}).WhereIn("id", g.Slice{3, 4, 5}).OrderAsc("id").All() t.AssertNil(err) - - if _, err := db.Exec(fmt.Sprintf(` - CREATE TABLE IF NOT EXISTS %s ( - id int(11) NOT NULL AUTO_INCREMENT, - user_id int(11) NOT NULL DEFAULT 0, - number varchar(500) NOT NULL DEFAULT '', - PRIMARY KEY (id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; - `, table2, - )); err != nil { - t.AssertNil(err) - } - defer dropTable(table2) - _, err = db.Model(table2).Insert(g.Map{ - "id": 1, - "user_id": 1, - "number": "n", - }) - t.AssertNil(err) - - if _, err := db.Exec(fmt.Sprintf(` - CREATE TABLE IF NOT EXISTS %s ( - id int(11) NOT NULL AUTO_INCREMENT, - user_id int(11) NOT NULL DEFAULT 0, - description varchar(500) NOT NULL DEFAULT '', - PRIMARY KEY (id) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; - `, table3, - )); err != nil { - t.AssertNil(err) - } - defer dropTable(table3) - _, err = db.Model(table3).Insert(g.Map{ - "id": 1, - "user_id": 1, - "description": "brief", - }) - t.AssertNil(err) - - one, err := db.Model("user"). - Where("user.id", 1). - Fields("score.number,user.name"). - LeftJoin("score", "user.id=score.user_id"). - LeftJoin("info", "info.id=info.user_id"). - Order("user.id asc"). - One() - t.AssertNil(err) - t.Assert(len(one), 2) - t.Assert(one["name"].String(), "john") - t.Assert(one["number"].String(), "n") - - one, err = db.Model("user"). - LeftJoin("score", "user.id=score.user_id"). - LeftJoin("info", "info.id=info.user_id"). - Fields("score.number,user.name"). - One() - t.AssertNil(err) - t.Assert(len(one), 2) - t.Assert(one["name"].String(), "john") - t.Assert(one["number"].String(), "n") + t.Assert(len(result), 2) + t.Assert(result[0]["id"], 3) + t.Assert(result[1]["id"], 4) + }) +} + +func Test_Model_WhereNotIn(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereNotIn("id", g.Slice{1, 2, 3, 4}).WhereNotIn("id", g.Slice{3, 4, 5}).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 5) + t.Assert(result[0]["id"], 6) + t.Assert(result[1]["id"], 7) + }) +} + +func Test_Model_WhereOrIn(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrIn("id", g.Slice{1, 2, 3, 4}).WhereOrIn("id", g.Slice{3, 4, 5}).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 5) + t.Assert(result[0]["id"], 1) + t.Assert(result[4]["id"], 5) + }) +} + +func Test_Model_WhereOrNotIn(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrNotIn("id", g.Slice{1, 2, 3, 4}).WhereOrNotIn("id", g.Slice{3, 4, 5}).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 8) + t.Assert(result[0]["id"], 1) + t.Assert(result[4]["id"], 7) + }) +} + +func Test_Model_WhereBetween(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereBetween("id", 1, 4).WhereBetween("id", 3, 5).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 2) + t.Assert(result[0]["id"], 3) + t.Assert(result[1]["id"], 4) + }) +} + +func Test_Model_WhereNotBetween(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereNotBetween("id", 2, 8).WhereNotBetween("id", 3, 100).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 1) + t.Assert(result[0]["id"], 1) + }) +} + +func Test_Model_WhereOrBetween(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrBetween("id", 1, 4).WhereOrBetween("id", 3, 5).OrderDesc("id").All() + t.AssertNil(err) + t.Assert(len(result), 5) + t.Assert(result[0]["id"], 5) + t.Assert(result[4]["id"], 1) + }) +} + +func Test_Model_WhereOrNotBetween(t *testing.T) { + table := createInitTable() + defer dropTable(table) + //db.SetDebug(true) + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrNotBetween("id", 1, 4).WhereOrNotBetween("id", 3, 5).OrderDesc("id").All() + t.AssertNil(err) + t.Assert(len(result), 8) + t.Assert(result[0]["id"], 10) + t.Assert(result[4]["id"], 6) + }) +} + +func Test_Model_WhereLike(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereLike("nickname", "name%").OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), TableSize) + t.Assert(result[0]["id"], 1) + t.Assert(result[TableSize-1]["id"], TableSize) + }) +} + +func Test_Model_WhereNotLike(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereNotLike("nickname", "name%").OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 0) + }) +} + +func Test_Model_WhereOrLike(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrLike("nickname", "namexxx%").WhereOrLike("nickname", "name%").OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), TableSize) + t.Assert(result[0]["id"], 1) + t.Assert(result[TableSize-1]["id"], TableSize) + }) +} + +func Test_Model_WhereOrNotLike(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrNotLike("nickname", "namexxx%").WhereOrNotLike("nickname", "name%").OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), TableSize) + t.Assert(result[0]["id"], 1) + t.Assert(result[TableSize-1]["id"], TableSize) + }) +} + +func Test_Model_WhereNull(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereNull("nickname").WhereNull("passport").OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 0) + }) +} + +func Test_Model_WhereNotNull(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereNotNull("nickname").WhereNotNull("passport").OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), TableSize) + t.Assert(result[0]["id"], 1) + t.Assert(result[TableSize-1]["id"], TableSize) + }) +} + +func Test_Model_WhereOrNull(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrNull("nickname").WhereOrNull("passport").OrderAsc("id").OrderRandom().All() + t.AssertNil(err) + t.Assert(len(result), 0) + }) +} + +func Test_Model_WhereOrNotNull(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereOrNotNull("nickname").WhereOrNotNull("passport").OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), TableSize) + t.Assert(result[0]["id"], 1) + t.Assert(result[TableSize-1]["id"], TableSize) + }) +} + +func Test_Model_WhereLT(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereLT("id", 3).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 2) + t.Assert(result[0]["id"], 1) + }) +} + +func Test_Model_WhereLTE(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereLTE("id", 3).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 3) + t.Assert(result[0]["id"], 1) + }) +} + +func Test_Model_WhereGT(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereGT("id", 8).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 2) + t.Assert(result[0]["id"], 9) + }) +} + +func Test_Model_WhereGTE(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereGTE("id", 8).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 3) + t.Assert(result[0]["id"], 8) + }) +} + +func Test_Model_WhereOrLT(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereLT("id", 3).WhereOrLT("id", 4).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 3) + t.Assert(result[0]["id"], 1) + t.Assert(result[2]["id"], 3) + }) +} + +func Test_Model_WhereOrLTE(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereLTE("id", 3).WhereOrLTE("id", 4).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 4) + t.Assert(result[0]["id"], 1) + t.Assert(result[3]["id"], 4) + }) +} + +func Test_Model_WhereOrGT(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereGT("id", 8).WhereOrGT("id", 7).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 3) + t.Assert(result[0]["id"], 8) + }) +} + +func Test_Model_WhereOrGTE(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereGTE("id", 8).WhereOrGTE("id", 7).OrderAsc("id").All() + t.AssertNil(err) + t.Assert(len(result), 4) + t.Assert(result[0]["id"], 7) + }) +} + +func Test_Model_Min_Max_Avg_Sum(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).Min("id") + t.AssertNil(err) + t.Assert(result, 1) + }) + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).Max("id") + t.AssertNil(err) + t.Assert(result, TableSize) + }) + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).Avg("id") + t.AssertNil(err) + t.Assert(result, 5.5) + }) + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).Sum("id") + t.AssertNil(err) + t.Assert(result, 55) + }) +} + +func Test_Model_CountColumn(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).CountColumn("id") + t.AssertNil(err) + t.Assert(result, TableSize) + }) + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).WhereIn("id", g.Slice{1, 2, 3}).CountColumn("id") + t.AssertNil(err) + t.Assert(result, 3) + }) +} + +func Test_Model_InsertAndGetId(t *testing.T) { + table := createTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + id, err := db.Model(table).Data(g.Map{ + "id": 1, + "passport": "user_1", + "password": "pass_1", + "nickname": "name_1", + }).InsertAndGetId() + t.AssertNil(err) + t.Assert(id, 1) + }) + gtest.C(t, func(t *gtest.T) { + id, err := db.Model(table).Data(g.Map{ + "passport": "user_2", + "password": "pass_2", + "nickname": "name_2", + }).InsertAndGetId() + t.AssertNil(err) + t.Assert(id, 2) + }) +} + +func Test_Model_Increment_Decrement(t *testing.T) { + table := createInitTable() + defer dropTable(table) + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).Where("id", 1).Increment("id", 100) + t.AssertNil(err) + rows, _ := result.RowsAffected() + t.Assert(rows, 1) + }) + gtest.C(t, func(t *gtest.T) { + result, err := db.Model(table).Where("id", 101).Decrement("id", 10) + t.AssertNil(err) + rows, _ := result.RowsAffected() + t.Assert(rows, 1) + }) + gtest.C(t, func(t *gtest.T) { + count, err := db.Model(table).Where("id", 91).Count() + t.AssertNil(err) + t.Assert(count, 1) }) } diff --git a/database/gdb/gdb_z_mysql_raw_test.go b/database/gdb/gdb_z_mysql_raw_test.go index e4677e12f..3b3f3dc05 100644 --- a/database/gdb/gdb_z_mysql_raw_test.go +++ b/database/gdb/gdb_z_mysql_raw_test.go @@ -20,7 +20,7 @@ func Test_Insert_Raw(t *testing.T) { gtest.C(t, func(t *gtest.T) { user := db.Model(table) - result, err := user.Filter().Data(g.Map{ + result, err := user.Data(g.Map{ "id": gdb.Raw("id+2"), "passport": "port_1", "password": "pass_1", @@ -39,7 +39,7 @@ func Test_BatchInsert_Raw(t *testing.T) { gtest.C(t, func(t *gtest.T) { user := db.Model(table) - result, err := user.Filter().Data( + result, err := user.Data( g.List{ g.Map{ "id": gdb.Raw("id+2"), diff --git a/database/gdb/gdb_z_mysql_struct_test.go b/database/gdb/gdb_z_mysql_struct_test.go index 67a1858cb..dbcf48d97 100644 --- a/database/gdb/gdb_z_mysql_struct_test.go +++ b/database/gdb/gdb_z_mysql_struct_test.go @@ -15,7 +15,7 @@ import ( "testing" ) -func Test_Model_Inherit_Insert(t *testing.T) { +func Test_Model_Embedded_Insert(t *testing.T) { table := createTable() defer dropTable(table) @@ -31,7 +31,7 @@ func Test_Model_Inherit_Insert(t *testing.T) { Password string `json:"password"` Nickname string `json:"nickname"` } - result, err := db.Model(table).Filter().Data(User{ + result, err := db.Model(table).Data(User{ Passport: "john-test", Password: "123456", Nickname: "John", @@ -50,7 +50,7 @@ func Test_Model_Inherit_Insert(t *testing.T) { }) } -func Test_Model_Inherit_MapToStruct(t *testing.T) { +func Test_Model_Embedded_MapToStruct(t *testing.T) { table := createTable() defer dropTable(table) @@ -77,7 +77,7 @@ func Test_Model_Inherit_MapToStruct(t *testing.T) { "nickname": "T1", "create_time": gtime.Now().String(), } - result, err := db.Model(table).Filter().Data(data).Insert() + result, err := db.Model(table).Data(data).Insert() t.AssertNil(err) n, _ := result.RowsAffected() t.Assert(n, 1) @@ -429,3 +429,26 @@ func Test_Model_Scan_UnmarshalValue(t *testing.T) { t.Assert(users[9].CreateTime.String(), CreateTime) }) } + +func Test_Model_Scan_Map(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + var users []*User + err := db.Model(table).Order("id asc").Scan(&users) + t.AssertNil(err) + t.Assert(len(users), TableSize) + t.Assert(users[0].Id, 1) + t.Assert(users[0].Passport, "user_1") + t.Assert(users[0].Password, "") + t.Assert(users[0].Nickname, "name_1") + t.Assert(users[0].CreateTime.String(), CreateTime) + + t.Assert(users[9].Id, 10) + t.Assert(users[9].Passport, "user_10") + t.Assert(users[9].Password, "") + t.Assert(users[9].Nickname, "name_10") + t.Assert(users[9].CreateTime.String(), CreateTime) + }) +} diff --git a/database/gdb/gdb_z_mysql_subquery_test.go b/database/gdb/gdb_z_mysql_subquery_test.go new file mode 100644 index 000000000..6cb76abf2 --- /dev/null +++ b/database/gdb/gdb_z_mysql_subquery_test.go @@ -0,0 +1,66 @@ +// 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 gdb_test + +import ( + "github.com/gogf/gf/frame/g" + "testing" + + "github.com/gogf/gf/test/gtest" +) + +func Test_Model_SubQuery_Where(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).Where( + "id in ?", + db.Model(table).Fields("id").Where("id", g.Slice{1, 3, 5}), + ).OrderAsc("id").All() + t.AssertNil(err) + + t.Assert(len(r), 3) + t.Assert(r[0]["id"], 1) + t.Assert(r[1]["id"], 3) + t.Assert(r[2]["id"], 5) + }) +} + +func Test_Model_SubQuery_Having(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + r, err := db.Model(table).Where( + "id in ?", + db.Model(table).Fields("id").Where("id", g.Slice{1, 3, 5}), + ).Having( + "id > ?", + db.Model(table).Fields("MAX(id)").Where("id", g.Slice{1, 3}), + ).OrderAsc("id").All() + t.AssertNil(err) + + t.Assert(len(r), 1) + t.Assert(r[0]["id"], 5) + }) +} + +func Test_Model_SubQuery_Model(t *testing.T) { + table := createInitTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + subQuery1 := db.Model(table).Where("id", g.Slice{1, 3, 5}) + subQuery2 := db.Model(table).Where("id", g.Slice{5, 7, 9}) + r, err := db.Model("? AS a, ? AS b", subQuery1, subQuery2).Fields("a.id").Where("a.id=b.id").OrderAsc("id").All() + t.AssertNil(err) + + t.Assert(len(r), 1) + t.Assert(r[0]["id"], 5) + }) +} diff --git a/database/gdb/gdb_z_mysql_transaction_test.go b/database/gdb/gdb_z_mysql_transaction_test.go index 96b3dd978..906bc6aca 100644 --- a/database/gdb/gdb_z_mysql_transaction_test.go +++ b/database/gdb/gdb_z_mysql_transaction_test.go @@ -7,10 +7,12 @@ package gdb_test import ( + "context" "fmt" + "testing" + "github.com/gogf/gf/database/gdb" "github.com/gogf/gf/errors/gerror" - "testing" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/os/gtime" @@ -119,7 +121,7 @@ func Test_TX_Insert(t *testing.T) { if err != nil { gtest.Error(err) } - user := tx.Table(table) + user := tx.Model(table) if _, err := user.Data(g.Map{ "id": 1, "passport": "t1", @@ -129,7 +131,6 @@ func Test_TX_Insert(t *testing.T) { }).Insert(); err != nil { gtest.Error(err) } - if _, err := tx.Insert(table, g.Map{ "id": 2, "passport": "t1", @@ -140,7 +141,7 @@ func Test_TX_Insert(t *testing.T) { gtest.Error(err) } - if n, err := tx.Table(table).Count(); err != nil { + if n, err := tx.Model(table).Count(); err != nil { gtest.Error(err) } else { t.Assert(n, 2) @@ -718,7 +719,8 @@ func Test_Transaction(t *testing.T) { defer dropTable(table) gtest.C(t, func(t *gtest.T) { - err := db.Transaction(func(tx *gdb.TX) error { + ctx := context.TODO() + err := db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { if _, err := tx.Replace(table, g.Map{ "id": 1, "passport": "USER_1", @@ -740,7 +742,8 @@ func Test_Transaction(t *testing.T) { }) gtest.C(t, func(t *gtest.T) { - err := db.Transaction(func(tx *gdb.TX) error { + ctx := context.TODO() + err := db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { if _, err := tx.Replace(table, g.Map{ "id": 1, "passport": "USER_1", @@ -767,7 +770,8 @@ func Test_Transaction_Panic(t *testing.T) { defer dropTable(table) gtest.C(t, func(t *gtest.T) { - err := db.Transaction(func(tx *gdb.TX) error { + ctx := context.TODO() + err := db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { if _, err := tx.Replace(table, g.Map{ "id": 1, "passport": "USER_1", @@ -789,3 +793,322 @@ func Test_Transaction_Panic(t *testing.T) { } }) } + +func Test_Transaction_Nested_Begin_Rollback_Commit(t *testing.T) { + table := createTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + tx, err := db.Begin() + t.AssertNil(err) + // tx begin. + err = tx.Begin() + t.AssertNil(err) + // tx rollback. + _, err = tx.Model(table).Data(g.Map{ + "id": 1, + "passport": "user_1", + "password": "pass_1", + "nickname": "name_1", + }).Insert() + err = tx.Rollback() + t.AssertNil(err) + // tx commit. + _, err = tx.Model(table).Data(g.Map{ + "id": 2, + "passport": "user_2", + "password": "pass_2", + "nickname": "name_2", + }).Insert() + err = tx.Commit() + t.AssertNil(err) + // check data. + all, err := db.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 2) + }) +} + +func Test_Transaction_Nested_TX_Transaction_UseTX(t *testing.T) { + table := createTable() + defer dropTable(table) + + db.SetDebug(true) + defer db.SetDebug(false) + + gtest.C(t, func(t *gtest.T) { + var ( + err error + ctx = context.TODO() + ) + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + // commit + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = tx.Model(table).Data(g.Map{ + "id": 1, + "passport": "USER_1", + "password": "PASS_1", + "nickname": "NAME_1", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + // rollback + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = tx.Model(table).Data(g.Map{ + "id": 2, + "passport": "USER_2", + "password": "PASS_2", + "nickname": "NAME_2", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + panic("error") + return err + }) + t.AssertNE(err, nil) + return nil + }) + t.AssertNil(err) + + all, err := db.Ctx(ctx).Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + + // another record. + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + // commit + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = tx.Model(table).Data(g.Map{ + "id": 3, + "passport": "USER_1", + "password": "PASS_1", + "nickname": "NAME_1", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + // rollback + err = tx.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = tx.Model(table).Data(g.Map{ + "id": 4, + "passport": "USER_2", + "password": "PASS_2", + "nickname": "NAME_2", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + panic("error") + return err + }) + t.AssertNE(err, nil) + return nil + }) + t.AssertNil(err) + + all, err = db.Ctx(ctx).Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 2) + t.Assert(all[0]["id"], 1) + t.Assert(all[1]["id"], 3) + }) +} + +func Test_Transaction_Nested_TX_Transaction_UseDB(t *testing.T) { + table := createTable() + defer dropTable(table) + + db.SetDebug(true) + defer db.SetDebug(false) + + gtest.C(t, func(t *gtest.T) { + var ( + err error + ctx = context.TODO() + ) + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + // commit + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = db.Model(table).Ctx(ctx).Data(g.Map{ + "id": 1, + "passport": "USER_1", + "password": "PASS_1", + "nickname": "NAME_1", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + + // rollback + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = tx.Model(table).Ctx(ctx).Data(g.Map{ + "id": 2, + "passport": "USER_2", + "password": "PASS_2", + "nickname": "NAME_2", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + // panic makes this transaction rollback. + panic("error") + return err + }) + t.AssertNE(err, nil) + return nil + }) + t.AssertNil(err) + all, err := db.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + // commit + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = db.Model(table).Ctx(ctx).Data(g.Map{ + "id": 3, + "passport": "USER_1", + "password": "PASS_1", + "nickname": "NAME_1", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + return err + }) + t.AssertNil(err) + + // rollback + err = db.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error { + _, err = tx.Model(table).Ctx(ctx).Data(g.Map{ + "id": 4, + "passport": "USER_2", + "password": "PASS_2", + "nickname": "NAME_2", + "create_time": gtime.Now().String(), + }).Insert() + t.AssertNil(err) + // panic makes this transaction rollback. + panic("error") + return err + }) + t.AssertNE(err, nil) + return nil + }) + t.AssertNil(err) + + all, err = db.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 2) + t.Assert(all[0]["id"], 1) + t.Assert(all[1]["id"], 3) + }) +} + +func Test_Transaction_Nested_SavePoint_RollbackTo(t *testing.T) { + table := createTable() + defer dropTable(table) + + gtest.C(t, func(t *gtest.T) { + tx, err := db.Begin() + t.AssertNil(err) + // tx save point. + _, err = tx.Model(table).Data(g.Map{ + "id": 1, + "passport": "user_1", + "password": "pass_1", + "nickname": "name_1", + }).Insert() + err = tx.SavePoint("MyPoint") + t.AssertNil(err) + _, err = tx.Model(table).Data(g.Map{ + "id": 2, + "passport": "user_2", + "password": "pass_2", + "nickname": "name_2", + }).Insert() + // tx rollback to. + err = tx.RollbackTo("MyPoint") + t.AssertNil(err) + // tx commit. + err = tx.Commit() + t.AssertNil(err) + + // check data. + all, err := db.Model(table).All() + t.AssertNil(err) + t.Assert(len(all), 1) + t.Assert(all[0]["id"], 1) + }) +} diff --git a/database/gredis/gredis.go b/database/gredis/gredis.go index 744ba4da6..13211d81d 100644 --- a/database/gredis/gredis.go +++ b/database/gredis/gredis.go @@ -32,14 +32,14 @@ type Redis struct { ctx context.Context // Context. } -// Redis connection. +// Conn is redis connection. type Conn struct { redis.Conn ctx context.Context redis *Redis } -// Redis configuration. +// Config is redis configuration. type Config struct { Host string `json:"host"` Port int `json:"port"` @@ -54,7 +54,7 @@ type Config struct { TLSSkipVerify bool `json:"tlsSkipVerify"` // Disables server name verification when connecting over TLS. } -// Pool statistics. +// PoolStats is statistics of redis connection pool. type PoolStats struct { redis.PoolStats } @@ -189,7 +189,7 @@ func (r *Redis) Conn() *Conn { } } -// Alias of Conn, see Conn. +// GetConn is alias of Conn, see Conn. // Deprecated, use Conn instead. func (r *Redis) GetConn() *Conn { return r.Conn() diff --git a/debug/gdebug/gdebug_caller.go b/debug/gdebug/gdebug_caller.go index a70ffda6a..485a61228 100644 --- a/debug/gdebug/gdebug_caller.go +++ b/debug/gdebug/gdebug_caller.go @@ -42,13 +42,13 @@ func init() { } } -// CallerPath returns the function name and the absolute file path along with its line +// Caller returns the function name and the absolute file path along with its line // number of the caller. func Caller(skip ...int) (function string, path string, line int) { return CallerWithFilter("", skip...) } -// CallerPathWithFilter returns the function name and the absolute file path along with +// CallerWithFilter returns the function name and the absolute file path along with // its line number of the caller. // // The parameter is used to filter the path of the caller. diff --git a/encoding/gjson/gjson.go b/encoding/gjson/gjson.go index df30d9e75..573acce39 100644 --- a/encoding/gjson/gjson.go +++ b/encoding/gjson/gjson.go @@ -22,7 +22,7 @@ const ( defaultSplitChar = '.' ) -// The customized JSON struct. +// Json is the customized JSON struct. type Json struct { mu *rwmutex.RWMutex p *interface{} // Pointer for hierarchical data access, it's the root of data in default. @@ -30,8 +30,8 @@ type Json struct { vc bool // Violence Check(false in default), which is used to access data when the hierarchical data key contains separator char. } -// Option for Json object creating. -type Option struct { +// Options for Json object creating. +type Options struct { Safe bool // Mark this object is for in concurrent-safe usage. Tags string // Custom priority tags for decoding. StrNumber bool // StrNumber causes the Decoder to unmarshal a number into an interface{} as a string instead of as a float64. diff --git a/encoding/gjson/gjson_api_new_load.go b/encoding/gjson/gjson_api_new_load.go index 8759f2e71..bd6dcb504 100644 --- a/encoding/gjson/gjson_api_new_load.go +++ b/encoding/gjson/gjson_api_new_load.go @@ -42,22 +42,22 @@ func New(data interface{}, safe ...bool) *Json { // The parameter specifies whether using this Json object in concurrent-safe context, which // is false in default. func NewWithTag(data interface{}, tags string, safe ...bool) *Json { - option := Option{ + option := Options{ Tags: tags, } if len(safe) > 0 && safe[0] { option.Safe = true } - return NewWithOption(data, option) + return NewWithOptions(data, option) } -// NewWithOption creates a Json object with any variable type of , but should be a map +// NewWithOptions creates a Json object with any variable type of , but should be a map // or slice for data access reason, or it will make no sense. -func NewWithOption(data interface{}, option Option) *Json { +func NewWithOptions(data interface{}, options Options) *Json { var j *Json switch data.(type) { case string, []byte: - if r, err := loadContentWithOption(data, option); err == nil { + if r, err := loadContentWithOptions(data, options); err == nil { j = r } else { j = &Json{ @@ -86,7 +86,7 @@ func NewWithOption(data interface{}, option Option) *Json { } case reflect.Map, reflect.Struct: i := interface{}(nil) - i = gconv.MapDeep(data, option.Tags) + i = gconv.MapDeep(data, options.Tags) j = &Json{ p: &i, c: byte(defaultSplitChar), @@ -100,7 +100,7 @@ func NewWithOption(data interface{}, option Option) *Json { } } } - j.mu = rwmutex.New(option.Safe) + j.mu = rwmutex.New(options.Safe) return j } @@ -111,56 +111,56 @@ func Load(path string, safe ...bool) (*Json, error) { } else { path = p } - option := Option{} + option := Options{} if len(safe) > 0 && safe[0] { option.Safe = true } - return doLoadContentWithOption(gfile.Ext(path), gfile.GetBytesWithCache(path), option) + return doLoadContentWithOptions(gfile.Ext(path), gfile.GetBytesWithCache(path), option) } // LoadJson creates a Json object from given JSON format content. func LoadJson(data interface{}, safe ...bool) (*Json, error) { - option := Option{} + option := Options{} if len(safe) > 0 && safe[0] { option.Safe = true } - return doLoadContentWithOption("json", gconv.Bytes(data), option) + return doLoadContentWithOptions("json", gconv.Bytes(data), option) } // LoadXml creates a Json object from given XML format content. func LoadXml(data interface{}, safe ...bool) (*Json, error) { - option := Option{} + option := Options{} if len(safe) > 0 && safe[0] { option.Safe = true } - return doLoadContentWithOption("xml", gconv.Bytes(data), option) + return doLoadContentWithOptions("xml", gconv.Bytes(data), option) } // LoadIni creates a Json object from given INI format content. func LoadIni(data interface{}, safe ...bool) (*Json, error) { - option := Option{} + option := Options{} if len(safe) > 0 && safe[0] { option.Safe = true } - return doLoadContentWithOption("ini", gconv.Bytes(data), option) + return doLoadContentWithOptions("ini", gconv.Bytes(data), option) } // LoadYaml creates a Json object from given YAML format content. func LoadYaml(data interface{}, safe ...bool) (*Json, error) { - option := Option{} + option := Options{} if len(safe) > 0 && safe[0] { option.Safe = true } - return doLoadContentWithOption("yaml", gconv.Bytes(data), option) + return doLoadContentWithOptions("yaml", gconv.Bytes(data), option) } // LoadToml creates a Json object from given TOML format content. func LoadToml(data interface{}, safe ...bool) (*Json, error) { - option := Option{} + option := Options{} if len(safe) > 0 && safe[0] { option.Safe = true } - return doLoadContentWithOption("toml", gconv.Bytes(data), option) + return doLoadContentWithOptions("toml", gconv.Bytes(data), option) } // LoadContent creates a Json object from given content, it checks the data type of @@ -186,11 +186,11 @@ func LoadContentType(dataType string, data interface{}, safe ...bool) (*Json, er if content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF { content = content[3:] } - option := Option{} + option := Options{} if len(safe) > 0 && safe[0] { option.Safe = true } - return doLoadContentWithOption(dataType, content, option) + return doLoadContentWithOptions(dataType, content, option) } // IsValidDataType checks and returns whether given a valid data type for loading. @@ -208,36 +208,36 @@ func IsValidDataType(dataType string) bool { return false } -func loadContentWithOption(data interface{}, option Option) (*Json, error) { +func loadContentWithOptions(data interface{}, options Options) (*Json, error) { content := gconv.Bytes(data) if len(content) == 0 { - return NewWithOption(nil, option), nil + return NewWithOptions(nil, options), nil } - return loadContentTypeWithOption(checkDataType(content), content, option) + return loadContentTypeWithOptions(checkDataType(content), content, options) } -func loadContentTypeWithOption(dataType string, data interface{}, option Option) (*Json, error) { +func loadContentTypeWithOptions(dataType string, data interface{}, options Options) (*Json, error) { content := gconv.Bytes(data) if len(content) == 0 { - return NewWithOption(nil, option), nil + return NewWithOptions(nil, options), nil } //ignore UTF8-BOM if content[0] == 0xEF && content[1] == 0xBB && content[2] == 0xBF { content = content[3:] } - return doLoadContentWithOption(dataType, content, option) + return doLoadContentWithOptions(dataType, content, options) } // doLoadContent creates a Json object from given content. // It supports data content type as follows: // JSON, XML, INI, YAML and TOML. -func doLoadContentWithOption(dataType string, data []byte, option Option) (*Json, error) { +func doLoadContentWithOptions(dataType string, data []byte, options Options) (*Json, error) { var ( err error result interface{} ) if len(data) == 0 { - return NewWithOption(nil, option), nil + return NewWithOptions(nil, options), nil } if dataType == "" { dataType = checkDataType(data) @@ -270,7 +270,7 @@ func doLoadContentWithOption(dataType string, data []byte, option Option) (*Json return nil, err } decoder := json.NewDecoder(bytes.NewReader(data)) - if option.StrNumber { + if options.StrNumber { decoder.UseNumber() } if err := decoder.Decode(&result); err != nil { @@ -280,7 +280,7 @@ func doLoadContentWithOption(dataType string, data []byte, option Option) (*Json case string, []byte: return nil, fmt.Errorf(`json decoding failed for content: %s`, string(data)) } - return NewWithOption(result, option), nil + return NewWithOptions(result, options), nil } // checkDataType automatically checks and returns the data type for . @@ -291,7 +291,8 @@ func checkDataType(content []byte) string { return "json" } else if gregex.IsMatch(`^<.+>[\S\s]+<.+>\s*$`, content) { return "xml" - } else if !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*"""[\s\S]+"""`, content) && !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*'''[\s\S]+'''`, content) && + } else if !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*"""[\s\S]+"""`, content) && + !gregex.IsMatch(`[\n\r]*[\s\t\w\-\."]+\s*=\s*'''[\s\S]+'''`, content) && ((gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*".+"`, content) || gregex.IsMatch(`^[\n\r]*[\w\-\s\t]+\s*:\s*\w+`, content)) || (gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*".+"`, content) || gregex.IsMatch(`[\n\r]+[\w\-\s\t]+\s*:\s*\w+`, content))) { return "yml" diff --git a/encoding/gjson/gjson_z_unit_basic_test.go b/encoding/gjson/gjson_z_unit_basic_test.go index 733adea8e..c89b615ad 100644 --- a/encoding/gjson/gjson_z_unit_basic_test.go +++ b/encoding/gjson/gjson_z_unit_basic_test.go @@ -474,7 +474,7 @@ func TestJson_Var(t *testing.T) { }) gtest.C(t, func(t *gtest.T) { data := []byte("[9223372036854775807, 9223372036854775806]") - array := gjson.NewWithOption(data, gjson.Option{StrNumber: true}).Var().Array() + array := gjson.NewWithOptions(data, gjson.Options{StrNumber: true}).Var().Array() t.Assert(array, []uint64{9223372036854775807, 9223372036854775806}) }) } diff --git a/encoding/gjson/gjson_z_unit_implements_test.go b/encoding/gjson/gjson_z_unit_implements_test.go index 8264e1625..8350f3a29 100644 --- a/encoding/gjson/gjson_z_unit_implements_test.go +++ b/encoding/gjson/gjson_z_unit_implements_test.go @@ -20,7 +20,7 @@ func TestJson_UnmarshalJSON(t *testing.T) { data := []byte(`{"n":123456789, "m":{"k":"v"}, "a":[1,2,3]}`) gtest.C(t, func(t *gtest.T) { j := gjson.New(nil) - err := json.Unmarshal(data, j) + err := json.UnmarshalUseNumber(data, j) t.Assert(err, nil) t.Assert(j.Get("n"), "123456789") t.Assert(j.Get("m"), g.Map{"k": "v"}) diff --git a/encoding/gjson/gjson_z_unit_new_test.go b/encoding/gjson/gjson_z_unit_new_test.go index b4f911379..359cafd64 100644 --- a/encoding/gjson/gjson_z_unit_new_test.go +++ b/encoding/gjson/gjson_z_unit_new_test.go @@ -98,7 +98,7 @@ func Test_New_HierarchicalStruct(t *testing.T) { }) } -func Test_NewWithOption(t *testing.T) { +func Test_NewWithOptions(t *testing.T) { gtest.C(t, func(t *gtest.T) { data := []byte("[9223372036854775807, 9223372036854775806]") array := gjson.New(data).Array() @@ -106,7 +106,7 @@ func Test_NewWithOption(t *testing.T) { }) gtest.C(t, func(t *gtest.T) { data := []byte("[9223372036854775807, 9223372036854775806]") - array := gjson.NewWithOption(data, gjson.Option{StrNumber: true}).Array() + array := gjson.NewWithOptions(data, gjson.Options{StrNumber: true}).Array() t.Assert(array, []uint64{9223372036854775807, 9223372036854775806}) }) } diff --git a/frame/g/g_object.go b/frame/g/g_object.go index 6e0dceb64..571d47247 100644 --- a/frame/g/g_object.go +++ b/frame/g/g_object.go @@ -18,9 +18,10 @@ import ( "github.com/gogf/gf/os/glog" "github.com/gogf/gf/os/gres" "github.com/gogf/gf/os/gview" + "github.com/gogf/gf/util/gvalid" ) -// Client is a convenience function, that creates and returns a new HTTP client. +// Client is a convenience function, which creates and returns a new HTTP client. func Client() *ghttp.Client { return ghttp.NewClient() } @@ -80,13 +81,6 @@ func Log(name ...string) *glog.Logger { return gins.Log(name...) } -// Database is alias of DB. -// See DB. -// Deprecated, use DB instead. -func Database(name ...string) gdb.DB { - return gins.Database(name...) -} - // DB returns an instance of database ORM object with specified configuration group name. func DB(name ...string) gdb.DB { return gins.Database(name...) @@ -97,21 +91,21 @@ func DB(name ...string) gdb.DB { // relational databases but also for NoSQL databases in the future. The name // "Table" is not proper for that purpose any more. // Deprecated, use Model instead. -func Table(tables ...string) *gdb.Model { - return DB().Model(tables...) +func Table(tableNameOrStruct ...interface{}) *gdb.Model { + return DB().Model(tableNameOrStruct...) } // Model creates and returns a model based on configuration of default database group. -func Model(tables ...string) *gdb.Model { - return DB().Model(tables...) -} - -// With creates and returns an ORM model based on meta data of given object. -func With(object interface{}) *gdb.Model { - return DB().With(object) +func Model(tableNameOrStruct ...interface{}) *gdb.Model { + return DB().Model(tableNameOrStruct...) } // Redis returns an instance of redis client with specified configuration group name. func Redis(name ...string) *gredis.Redis { return gins.Redis(name...) } + +// Validator is a convenience function, which creates and returns a new validation manager object. +func Validator() *gvalid.Validator { + return gvalid.New() +} diff --git a/frame/g/g_setting.go b/frame/g/g_setting.go index a8adff947..ddb9e966a 100644 --- a/frame/g/g_setting.go +++ b/frame/g/g_setting.go @@ -11,7 +11,7 @@ import ( "github.com/gogf/gf/net/ghttp" ) -// SetEnabled enables/disables the GoFrame internal logging manually. +// SetDebug enables/disables the GoFrame internal logging manually. // Note that this function is not concurrent safe, be aware of the DATA RACE, // which means you should call this function in your boot but not the runtime. func SetDebug(enabled bool) { diff --git a/frame/gins/gins_database.go b/frame/gins/gins_database.go index 2e92392ed..26e97b5fe 100644 --- a/frame/gins/gins_database.go +++ b/frame/gins/gins_database.go @@ -54,14 +54,14 @@ func Database(name ...string) gdb.DB { if exampleConfigFilePath, _ := Config().GetFilePath(exampleFileName); exampleConfigFilePath != "" { panic(gerror.Wrapf( err, - `configuration file "%s" not found, but found "%s", did you miss renaming the configuration example file?`, + `configuration file "%s" not found, but found "%s", did you miss renaming the example configuration file?`, Config().GetFileName(), exampleFileName, )) } else { panic(gerror.Wrapf( err, - `configuration file "%s" not found, did you miss the configuration file or the file name setting?`, + `configuration file "%s" not found, did you miss the configuration file or the misspell the configuration file name?`, Config().GetFileName(), )) } diff --git a/frame/gins/gins_z_unit_view_test.go b/frame/gins/gins_z_unit_view_test.go index bc8f2d409..084c09705 100644 --- a/frame/gins/gins_z_unit_view_test.go +++ b/frame/gins/gins_z_unit_view_test.go @@ -7,6 +7,7 @@ package gins import ( + "context" "fmt" "github.com/gogf/gf/debug/gdebug" "github.com/gogf/gf/os/gcfg" @@ -20,7 +21,7 @@ import ( func Test_View(t *testing.T) { gtest.C(t, func(t *gtest.T) { t.AssertNE(View(), nil) - b, e := View().ParseContent(`{{"我是中国人" | substr 2 -1}}`, nil) + b, e := View().ParseContent(context.TODO(), `{{"我是中国人" | substr 2 -1}}`, nil) t.Assert(e, nil) t.Assert(b, "中国人") }) @@ -30,7 +31,7 @@ func Test_View(t *testing.T) { t.Assert(err, nil) defer gfile.Remove(tpl) - b, e := View().Parse("t.tpl", nil) + b, e := View().Parse(context.TODO(), "t.tpl", nil) t.Assert(e, nil) t.Assert(b, "中国人") }) @@ -43,7 +44,7 @@ func Test_View(t *testing.T) { err = View().AddPath(path) t.Assert(err, nil) - b, e := View().Parse("t.tpl", nil) + b, e := View().Parse(context.TODO(), "t.tpl", nil) t.Assert(e, nil) t.Assert(b, "中国人") }) @@ -64,11 +65,11 @@ func Test_View_Config(t *testing.T) { str := `hello ${.name},version:${.version}` view.Assigns(map[string]interface{}{"version": "1.9.0"}) - result, err := view.ParseContent(str, nil) + result, err := view.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, "hello test1,version:1.9.0") - result, err = view.ParseDefault() + result, err = view.ParseDefault(context.TODO()) t.Assert(err, nil) t.Assert(result, "test1:test1") }) @@ -86,11 +87,11 @@ func Test_View_Config(t *testing.T) { str := `hello #{.name},version:#{.version}` view.Assigns(map[string]interface{}{"version": "1.9.0"}) - result, err := view.ParseContent(str, nil) + result, err := view.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, "hello test2,version:1.9.0") - result, err = view.ParseDefault() + result, err = view.ParseDefault(context.TODO()) t.Assert(err, nil) t.Assert(result, "test2:test2") }) @@ -108,11 +109,11 @@ func Test_View_Config(t *testing.T) { str := `hello {.name},version:{.version}` view.Assigns(map[string]interface{}{"version": "1.9.0"}) - result, err := view.ParseContent(str, nil) + result, err := view.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, "hello test,version:1.9.0") - result, err = view.ParseDefault() + result, err = view.ParseDefault(context.TODO()) t.Assert(err, nil) t.Assert(result, "test:test") }) @@ -130,11 +131,11 @@ func Test_View_Config(t *testing.T) { str := `hello {.name},version:{.version}` view.Assigns(map[string]interface{}{"version": "1.9.0"}) - result, err := view.ParseContent(str, nil) + result, err := view.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, "hello test,version:1.9.0") - result, err = view.ParseDefault() + result, err = view.ParseDefault(context.TODO()) t.Assert(err, nil) t.Assert(result, "test:test") }) diff --git a/go.mod b/go.mod index b27eb6035..5022b6160 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,12 @@ require ( github.com/BurntSushi/toml v0.3.1 github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28 github.com/fsnotify/fsnotify v1.4.9 - github.com/go-sql-driver/mysql v1.5.0 + github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579 github.com/gomodule/redigo v2.0.0+incompatible github.com/gorilla/websocket v1.4.1 github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf github.com/mattn/go-runewidth v0.0.10 // indirect - github.com/olekukonko/tablewriter v0.0.1 + github.com/olekukonko/tablewriter v0.0.5 go.opentelemetry.io/otel v0.19.0 go.opentelemetry.io/otel/oteltest v0.19.0 go.opentelemetry.io/otel/trace v0.19.0 diff --git a/go.sum b/go.sum index f936b51f4..582d62e0e 100644 --- a/go.sum +++ b/go.sum @@ -6,8 +6,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= -github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579 h1:pP/uEy52biKDytlgK/ug8kiYPAiYu6KajKVUHfGrtyw= +github.com/gogf/mysql v1.6.1-0.20210603073548-16164ae25579/go.mod h1:52e6mXyNnHAsFrXrSnj5JPRSKsZKpHylVtA3j4AtMz8= github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= @@ -16,10 +16,11 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf h1:wIOAyJMMen0ELGiFzlmqxdcV1yGbkyHBAB6PolcNbLA= github.com/grokify/html-strip-tags-go v0.0.0-20190921062105-daaa06bf1aaf/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= diff --git a/i18n/gi18n/gi18n.go b/i18n/gi18n/gi18n.go index b0a85400b..460239807 100644 --- a/i18n/gi18n/gi18n.go +++ b/i18n/gi18n/gi18n.go @@ -7,6 +7,8 @@ // Package gi18n implements internationalization and localization. package gi18n +import "context" + // SetPath sets the directory path storing i18n files. func SetPath(path string) error { return Instance().SetPath(path) @@ -23,42 +25,28 @@ func SetDelimiters(left, right string) { } // T is alias of Translate for convenience. -func T(content string, language ...string) string { - return Instance().T(content, language...) +func T(ctx context.Context, content string) string { + return Instance().T(ctx, content) } // Tf is alias of TranslateFormat for convenience. -func Tf(format string, values ...interface{}) string { - return Instance().TranslateFormat(format, values...) -} - -// Tfl is alias of TranslateFormatLang for convenience. -func Tfl(language string, format string, values ...interface{}) string { - return Instance().TranslateFormatLang(language, format, values...) +func Tf(ctx context.Context, format string, values ...interface{}) string { + return Instance().TranslateFormat(ctx, format, values...) } // TranslateFormat translates, formats and returns the with configured language // and given . -func TranslateFormat(format string, values ...interface{}) string { - return Instance().TranslateFormat(format, values...) -} - -// TranslateFormatLang translates, formats and returns the with configured language -// and given . The parameter specifies custom translation language ignoring -// configured language. If is given empty string, it uses the default configured -// language for the translation. -func TranslateFormatLang(language string, format string, values ...interface{}) string { - return Instance().TranslateFormatLang(language, format, values...) +func TranslateFormat(ctx context.Context, format string, values ...interface{}) string { + return Instance().TranslateFormat(ctx, format, values...) } // Translate translates with configured language and returns the translated content. -// The parameter specifies custom translation language ignoring configured language. -func Translate(content string, language ...string) string { - return Instance().Translate(content, language...) +func Translate(ctx context.Context, content string) string { + return Instance().Translate(ctx, content) } -// GetValue retrieves and returns the configured content for given key and specified language. +// GetContent retrieves and returns the configured content for given key and specified language. // It returns an empty string if not found. -func GetContent(key string, language ...string) string { - return Instance().GetContent(key, language...) +func GetContent(ctx context.Context, key string) string { + return Instance().GetContent(ctx, key) } diff --git a/i18n/gi18n/gi18n_ctx.go b/i18n/gi18n/gi18n_ctx.go new file mode 100644 index 000000000..a2c7293a7 --- /dev/null +++ b/i18n/gi18n/gi18n_ctx.go @@ -0,0 +1,35 @@ +// 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 gi18n implements internationalization and localization. +package gi18n + +import "context" + +const ( + ctxLanguage = "I18nLanguage" +) + +// WithLanguage append language setting to the context and returns a new context. +func WithLanguage(ctx context.Context, language string) context.Context { + if ctx == nil { + ctx = context.TODO() + } + return context.WithValue(ctx, ctxLanguage, language) +} + +// LanguageFromCtx retrieves and returns language name from context. +// It returns an empty string if it is not set previously. +func LanguageFromCtx(ctx context.Context) string { + if ctx == nil { + return "" + } + v := ctx.Value(ctxLanguage) + if v != nil { + return v.(string) + } + return "" +} diff --git a/i18n/gi18n/gi18n_instance.go b/i18n/gi18n/gi18n_instance.go index d9abdbde2..43af97cc1 100644 --- a/i18n/gi18n/gi18n_instance.go +++ b/i18n/gi18n/gi18n_instance.go @@ -9,7 +9,7 @@ package gi18n import "github.com/gogf/gf/container/gmap" const ( - // Default group name for instance usage. + // DefaultName is the default group name for instance usage. DefaultName = "default" ) diff --git a/i18n/gi18n/gi18n_manager.go b/i18n/gi18n/gi18n_manager.go index 6b737df94..0949c1f44 100644 --- a/i18n/gi18n/gi18n_manager.go +++ b/i18n/gi18n/gi18n_manager.go @@ -7,6 +7,7 @@ package gi18n import ( + "context" "errors" "fmt" "github.com/gogf/gf/internal/intlog" @@ -25,7 +26,7 @@ import ( "github.com/gogf/gf/os/gres" ) -// Manager, it is concurrent safe, supporting hot reload. +// Manager for i18n contents, it is concurrent safe, supporting hot reload. type Manager struct { mu sync.RWMutex data map[string]map[string]string // Translating map. @@ -36,13 +37,13 @@ type Manager struct { // Options is used for i18n object configuration. type Options struct { Path string // I18n files storage path. - Language string // Local language. + Language string // Default local language. Delimiters []string // Delimiters for variable parsing. } var ( - // defaultDelimiters defines the default key variable delimiters. - defaultDelimiters = []string{"{#", "}"} + defaultLanguage = "en" // defaultDelimiters defines the default language if user does not specified in options. + defaultDelimiters = []string{"{#", "}"} // defaultDelimiters defines the default key variable delimiters. ) // New creates and returns a new i18n manager. @@ -55,6 +56,9 @@ func New(options ...Options) *Manager { } else { opts = DefaultOptions() } + if len(opts.Language) == 0 { + opts.Language = defaultLanguage + } if len(opts.Delimiters) == 0 { opts.Delimiters = defaultDelimiters } @@ -118,45 +122,29 @@ func (m *Manager) SetDelimiters(left, right string) { } // T is alias of Translate for convenience. -func (m *Manager) T(content string, language ...string) string { - return m.Translate(content, language...) +func (m *Manager) T(ctx context.Context, content string) string { + return m.Translate(ctx, content) } // Tf is alias of TranslateFormat for convenience. -func (m *Manager) Tf(format string, values ...interface{}) string { - return m.TranslateFormat(format, values...) -} - -// Tfl is alias of TranslateFormatLang for convenience. -func (m *Manager) Tfl(language string, format string, values ...interface{}) string { - return m.TranslateFormatLang(language, format, values...) +func (m *Manager) Tf(ctx context.Context, format string, values ...interface{}) string { + return m.TranslateFormat(ctx, format, values...) } // TranslateFormat translates, formats and returns the with configured language // and given . -func (m *Manager) TranslateFormat(format string, values ...interface{}) string { - return fmt.Sprintf(m.Translate(format), values...) -} - -// TranslateFormatLang translates, formats and returns the with configured language -// and given . The parameter specifies custom translation language ignoring -// configured language. If is given empty string, it uses the default configured -// language for the translation. -func (m *Manager) TranslateFormatLang(language string, format string, values ...interface{}) string { - return fmt.Sprintf(m.Translate(format, language), values...) +func (m *Manager) TranslateFormat(ctx context.Context, format string, values ...interface{}) string { + return fmt.Sprintf(m.Translate(ctx, format), values...) } // Translate translates with configured language. -// The parameter specifies custom translation language ignoring configured language. -func (m *Manager) Translate(content string, language ...string) string { +func (m *Manager) Translate(ctx context.Context, content string) string { m.init() m.mu.RLock() defer m.mu.RUnlock() transLang := m.options.Language - if len(language) > 0 && language[0] != "" { - transLang = language[0] - } else { - transLang = m.options.Language + if lang := LanguageFromCtx(ctx); lang != "" { + transLang = lang } data := m.data[transLang] if data == nil { @@ -179,17 +167,15 @@ func (m *Manager) Translate(content string, language ...string) string { return result } -// GetValue retrieves and returns the configured content for given key and specified language. +// GetContent retrieves and returns the configured content for given key and specified language. // It returns an empty string if not found. -func (m *Manager) GetContent(key string, language ...string) string { +func (m *Manager) GetContent(ctx context.Context, key string) string { m.init() m.mu.RLock() defer m.mu.RUnlock() transLang := m.options.Language - if len(language) > 0 && language[0] != "" { - transLang = language[0] - } else { - transLang = m.options.Language + if lang := LanguageFromCtx(ctx); lang != "" { + transLang = lang } if data, ok := m.data[transLang]; ok { return data[key] diff --git a/i18n/gi18n/gi18n_unit_test.go b/i18n/gi18n/gi18n_unit_test.go index b0012c1c7..0579bce5c 100644 --- a/i18n/gi18n/gi18n_unit_test.go +++ b/i18n/gi18n/gi18n_unit_test.go @@ -7,6 +7,7 @@ package gi18n_test import ( + "context" "testing" "github.com/gogf/gf/os/gres" @@ -32,16 +33,16 @@ func Test_Basic(t *testing.T) { Path: gdebug.TestDataPath("i18n"), }) i18n.SetLanguage("none") - t.Assert(i18n.T("{#hello}{#world}"), "{#hello}{#world}") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") i18n.SetLanguage("ja") - t.Assert(i18n.T("{#hello}{#world}"), "こんにちは世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") i18n.SetLanguage("zh-CN") - t.Assert(i18n.T("{#hello}{#world}"), "你好世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") i18n.SetDelimiters("{$", "}") - t.Assert(i18n.T("{#hello}{#world}"), "{#hello}{#world}") - t.Assert(i18n.T("{$hello}{$world}"), "你好世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") + t.Assert(i18n.T(context.Background(), "{$hello}{$world}"), "你好世界") }) gtest.C(t, func(t *gtest.T) { @@ -49,13 +50,13 @@ func Test_Basic(t *testing.T) { Path: gdebug.TestDataPath("i18n-file"), }) i18n.SetLanguage("none") - t.Assert(i18n.T("{#hello}{#world}"), "{#hello}{#world}") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") i18n.SetLanguage("ja") - t.Assert(i18n.T("{#hello}{#world}"), "こんにちは世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") i18n.SetLanguage("zh-CN") - t.Assert(i18n.T("{#hello}{#world}"), "你好世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") }) gtest.C(t, func(t *gtest.T) { @@ -63,13 +64,13 @@ func Test_Basic(t *testing.T) { Path: gdebug.CallerDirectory() + gfile.Separator + "testdata" + gfile.Separator + "i18n-dir", }) i18n.SetLanguage("none") - t.Assert(i18n.T("{#hello}{#world}"), "{#hello}{#world}") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") i18n.SetLanguage("ja") - t.Assert(i18n.T("{#hello}{#world}"), "こんにちは世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") i18n.SetLanguage("zh-CN") - t.Assert(i18n.T("{#hello}{#world}"), "你好世界") + t.Assert(i18n.T(context.Background(), "{#hello}{#world}"), "你好世界") }) } @@ -80,18 +81,10 @@ func Test_TranslateFormat(t *testing.T) { Path: gdebug.TestDataPath("i18n"), }) i18n.SetLanguage("none") - t.Assert(i18n.Tf("{#hello}{#world} %d", 2020), "{#hello}{#world} 2020") + t.Assert(i18n.Tf(context.Background(), "{#hello}{#world} %d", 2020), "{#hello}{#world} 2020") i18n.SetLanguage("ja") - t.Assert(i18n.Tf("{#hello}{#world} %d", 2020), "こんにちは世界 2020") - }) - // Tfl - gtest.C(t, func(t *gtest.T) { - i18n := gi18n.New(gi18n.Options{ - Path: gdebug.TestDataPath("i18n"), - }) - t.Assert(i18n.Tfl("ja", "{#hello}{#world} %d", 2020), "こんにちは世界 2020") - t.Assert(i18n.Tfl("zh-CN", "{#hello}{#world} %d", 2020), "你好世界 2020") + t.Assert(i18n.Tf(context.Background(), "{#hello}{#world} %d", 2020), "こんにちは世界 2020") }) } @@ -101,13 +94,13 @@ func Test_DefaultManager(t *testing.T) { t.Assert(err, nil) gi18n.SetLanguage("none") - t.Assert(gi18n.T("{#hello}{#world}"), "{#hello}{#world}") + t.Assert(gi18n.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") gi18n.SetLanguage("ja") - t.Assert(gi18n.T("{#hello}{#world}"), "こんにちは世界") + t.Assert(gi18n.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") gi18n.SetLanguage("zh-CN") - t.Assert(gi18n.T("{#hello}{#world}"), "你好世界") + t.Assert(gi18n.T(context.Background(), "{#hello}{#world}"), "你好世界") }) gtest.C(t, func(t *gtest.T) { @@ -115,13 +108,13 @@ func Test_DefaultManager(t *testing.T) { t.Assert(err, nil) gi18n.SetLanguage("none") - t.Assert(gi18n.Translate("{#hello}{#world}"), "{#hello}{#world}") + t.Assert(gi18n.Translate(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") gi18n.SetLanguage("ja") - t.Assert(gi18n.Translate("{#hello}{#world}"), "こんにちは世界") + t.Assert(gi18n.Translate(context.Background(), "{#hello}{#world}"), "こんにちは世界") gi18n.SetLanguage("zh-CN") - t.Assert(gi18n.Translate("{#hello}{#world}"), "你好世界") + t.Assert(gi18n.Translate(context.Background(), "{#hello}{#world}"), "你好世界") }) } @@ -132,21 +125,21 @@ func Test_Instance(t *testing.T) { err := m.SetPath("i18n-dir") t.Assert(err, nil) m.SetLanguage("zh-CN") - t.Assert(m.T("{#hello}{#world}"), "你好世界") + t.Assert(m.T(context.Background(), "{#hello}{#world}"), "你好世界") }) gtest.C(t, func(t *gtest.T) { m := gi18n.Instance() - t.Assert(m.T("{#hello}{#world}"), "你好世界") + t.Assert(m.T(context.Background(), "{#hello}{#world}"), "你好世界") }) gtest.C(t, func(t *gtest.T) { - t.Assert(g.I18n().T("{#hello}{#world}"), "你好世界") + t.Assert(g.I18n().T(context.Background(), "{#hello}{#world}"), "你好世界") }) // Default language is: en gtest.C(t, func(t *gtest.T) { m := gi18n.Instance(gconv.String(gtime.TimestampNano())) - t.Assert(m.T("{#hello}{#world}"), "HelloWorld") + t.Assert(m.T(context.Background(), "{#hello}{#world}"), "HelloWorld") }) } @@ -157,12 +150,12 @@ func Test_Resource(t *testing.T) { t.Assert(err, nil) m.SetLanguage("none") - t.Assert(m.T("{#hello}{#world}"), "{#hello}{#world}") + t.Assert(m.T(context.Background(), "{#hello}{#world}"), "{#hello}{#world}") m.SetLanguage("ja") - t.Assert(m.T("{#hello}{#world}"), "こんにちは世界") + t.Assert(m.T(context.Background(), "{#hello}{#world}"), "こんにちは世界") m.SetLanguage("zh-CN") - t.Assert(m.T("{#hello}{#world}"), "你好世界") + t.Assert(m.T(context.Background(), "{#hello}{#world}"), "你好世界") }) } diff --git a/internal/empty/empty.go b/internal/empty/empty.go index 1d42441ff..023f759a9 100644 --- a/internal/empty/empty.go +++ b/internal/empty/empty.go @@ -85,7 +85,9 @@ func IsEmpty(value interface{}) bool { case map[string]interface{}: return len(value) == 0 default: + // ========================= // Common interfaces checks. + // ========================= if f, ok := value.(apiTime); ok { if f == nil { return true @@ -163,6 +165,126 @@ func IsEmpty(value interface{}) bool { return false } +// IsEmptyLength checks whether given `value` is empty length. +// It returns true if `value` is in: nil, "", len(slice/map/chan) == 0, +// or else it returns false. +//func IsEmptyLength(value interface{}) bool { +// if value == nil { +// return true +// } +// // It firstly checks the variable as common types using assertion to enhance the performance, +// // and then using reflection. +// switch value := value.(type) { +// case +// int, +// int8, +// int16, +// int32, +// int64, +// uint, +// uint8, +// uint16, +// uint32, +// uint64, +// float32, +// float64, +// bool: +// return false +// case string: +// return value == "" +// case []byte: +// return len(value) == 0 +// case []rune: +// return len(value) == 0 +// case []int: +// return len(value) == 0 +// case []string: +// return len(value) == 0 +// case []float32: +// return len(value) == 0 +// case []float64: +// return len(value) == 0 +// case map[string]interface{}: +// return len(value) == 0 +// default: +// // ========================= +// // Common interfaces checks. +// // ========================= +// if f, ok := value.(apiTime); ok { +// if f == nil { +// return true +// } +// return f.IsZero() +// } +// if f, ok := value.(apiString); ok { +// if f == nil { +// return true +// } +// return f.String() == "" +// } +// if f, ok := value.(apiInterfaces); ok { +// if f == nil { +// return true +// } +// return len(f.Interfaces()) == 0 +// } +// if f, ok := value.(apiMapStrAny); ok { +// if f == nil { +// return true +// } +// return len(f.MapStrAny()) == 0 +// } +// // Finally using reflect. +// var rv reflect.Value +// if v, ok := value.(reflect.Value); ok { +// rv = v +// } else { +// rv = reflect.ValueOf(value) +// } +// +// switch rv.Kind() { +// case +// reflect.Int, +// reflect.Int8, +// reflect.Int16, +// reflect.Int32, +// reflect.Int64, +// reflect.Uint, +// reflect.Uint8, +// reflect.Uint16, +// reflect.Uint32, +// reflect.Uint64, +// reflect.Uintptr, +// reflect.Float32, +// reflect.Float64, +// reflect.Bool: +// return false +// case reflect.String: +// return rv.Len() == 0 +// case reflect.Struct: +// for i := 0; i < rv.NumField(); i++ { +// if !IsEmpty(rv) { +// return false +// } +// } +// return true +// case reflect.Chan, +// reflect.Map, +// reflect.Slice, +// reflect.Array: +// return rv.Len() == 0 +// case reflect.Func, +// reflect.Ptr, +// reflect.Interface, +// reflect.UnsafePointer: +// if rv.IsNil() { +// return true +// } +// } +// } +// return false +//} + // IsNil checks whether given `value` is nil. // Parameter `traceSource` is used for tracing to the source variable if given `value` is type // of a pinter that also points to a pointer. It returns nil if the source is nil when `traceSource` diff --git a/internal/json/json.go b/internal/json/json.go index 1ed12ba79..ae6c38f13 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -8,6 +8,7 @@ package json import ( + "bytes" "encoding/json" "io" ) @@ -33,6 +34,13 @@ func Unmarshal(data []byte, v interface{}) error { return json.Unmarshal(data, v) } +// UnmarshalUseNumber decodes the json data bytes to target interface using number option. +func UnmarshalUseNumber(data []byte, v interface{}) error { + decoder := NewDecoder(bytes.NewReader(data)) + decoder.UseNumber() + return decoder.Decode(v) +} + // NewEncoder same as json.NewEncoder func NewEncoder(writer io.Writer) *json.Encoder { return json.NewEncoder(writer) diff --git a/internal/structs/structs_field.go b/internal/structs/structs_field.go index a0ad1a1cb..81ae1d960 100644 --- a/internal/structs/structs_field.go +++ b/internal/structs/structs_field.go @@ -14,6 +14,16 @@ func (f *Field) Tag(key string) string { return f.Field.Tag.Get(key) } +// TagLookup returns the value associated with key in the tag string. +// If the key is present in the tag the value (which may be empty) +// is returned. Otherwise the returned value will be the empty string. +// The ok return value reports whether the value was explicitly set in +// the tag string. If the tag does not have the conventional format, +// the value returned by Lookup is unspecified. +func (f *Field) TagLookup(key string) (value string, ok bool) { + return f.Field.Tag.Lookup(key) +} + // IsEmbedded returns true if the given field is an anonymous field (embedded) func (f *Field) IsEmbedded() bool { return f.Field.Anonymous @@ -61,8 +71,11 @@ func (f *Field) OriginalKind() reflect.Kind { // The parameter `priority` specifies the priority tag array for retrieving from high to low. // If it's given `nil`, it returns map[name]*Field, of which the `name` is attribute name. // +// The parameter `recursive` specifies the whether retrieving the fields recursively if the attribute +// is an embedded struct. +// // Note that it only retrieves the exported attributes with first letter up-case from struct. -func FieldMap(pointer interface{}, priority []string) (map[string]*Field, error) { +func FieldMap(pointer interface{}, priority []string, recursive bool) (map[string]*Field, error) { fields, err := getFieldValues(pointer) if err != nil { return nil, err @@ -88,8 +101,8 @@ func FieldMap(pointer interface{}, priority []string) (map[string]*Field, error) if tagValue != "" { mapField[tagValue] = tempField } else { - if field.IsEmbedded() { - m, err := FieldMap(field.Value, priority) + if recursive && field.IsEmbedded() { + m, err := FieldMap(field.Value, priority, recursive) if err != nil { return nil, err } diff --git a/internal/structs/structs_z_unit_test.go b/internal/structs/structs_z_unit_test.go index c4f31d1a2..4268f473a 100644 --- a/internal/structs/structs_z_unit_test.go +++ b/internal/structs/structs_z_unit_test.go @@ -110,7 +110,7 @@ func Test_FieldMap(t *testing.T) { Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"` } var user *User - m, _ := structs.FieldMap(user, []string{"params"}) + m, _ := structs.FieldMap(user, []string{"params"}, true) t.Assert(len(m), 3) _, ok := m["Id"] t.Assert(ok, true) @@ -130,7 +130,7 @@ func Test_FieldMap(t *testing.T) { Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"` } var user *User - m, _ := structs.FieldMap(user, nil) + m, _ := structs.FieldMap(user, nil, true) t.Assert(len(m), 3) _, ok := m["Id"] t.Assert(ok, true) diff --git a/net/ghttp/ghttp_client_websocket.go b/net/ghttp/ghttp_client_websocket.go new file mode 100644 index 000000000..1f1f228f7 --- /dev/null +++ b/net/ghttp/ghttp_client_websocket.go @@ -0,0 +1,29 @@ +// 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 ghttp + +import ( + "github.com/gorilla/websocket" + "net/http" + "time" +) + +// WebSocketClient wraps the underlying websocket client connection +// and provides convenient functions. +type WebSocketClient struct { + *websocket.Dialer +} + +// NewWebSocketClient New creates and returns a new WebSocketClient object. +func NewWebSocketClient() *WebSocketClient { + return &WebSocketClient{ + &websocket.Dialer{ + Proxy: http.ProxyFromEnvironment, + HandshakeTimeout: 45 * time.Second, + }, + } +} diff --git a/net/ghttp/ghttp_request_param.go b/net/ghttp/ghttp_request_param.go index f77eeeec3..1da2af60a 100644 --- a/net/ghttp/ghttp_request_param.go +++ b/net/ghttp/ghttp_request_param.go @@ -82,24 +82,27 @@ func (r *Request) doParse(pointer interface{}, requestType int) error { // 1. {"id":1, "name":"john"} // 2. ?id=1&name=john case reflect.Ptr, reflect.Struct: + var ( + err error + data map[string]interface{} + ) // Converting. switch requestType { case parseTypeQuery: - if err := r.GetQueryStruct(pointer); err != nil { + if data, err = r.doGetQueryStruct(pointer); err != nil { return err } case parseTypeForm: - if err := r.GetFormStruct(pointer); err != nil { + if data, err = r.doGetFormStruct(pointer); err != nil { return err } default: - if err := r.GetStruct(pointer); err != nil { + if data, err = r.doGetRequestStruct(pointer); err != nil { return err } } - // Validation. - if err := gvalid.CheckStruct(pointer, nil); err != nil { + if err := gvalid.CheckStructWithData(r.Context(), pointer, data, nil); err != nil { return err } @@ -107,7 +110,7 @@ func (r *Request) doParse(pointer interface{}, requestType int) error { // [{"id":1, "name":"john"}, {"id":, "name":"smith"}] case reflect.Array, reflect.Slice: // If struct slice conversion, it might post JSON/XML content, - // so it uses gjson for the conversion. + // so it uses `gjson` for the conversion. j, err := gjson.LoadContent(r.GetBody()) if err != nil { return err @@ -116,7 +119,12 @@ func (r *Request) doParse(pointer interface{}, requestType int) error { return err } for i := 0; i < reflectVal2.Len(); i++ { - if err := gvalid.CheckStruct(reflectVal2.Index(i), nil); err != nil { + if err := gvalid.CheckStructWithData( + r.Context(), + reflectVal2.Index(i), + j.GetMap(gconv.String(i)), + nil, + ); err != nil { return err } } @@ -312,7 +320,7 @@ func (r *Request) parseBody() { body = bytes.TrimSpace(body) // JSON format checks. if body[0] == '{' && body[len(body)-1] == '}' { - _ = json.Unmarshal(body, &r.bodyMap) + _ = json.UnmarshalUseNumber(body, &r.bodyMap) } // XML format checks. if len(body) > 5 && bytes.EqualFold(body[:5], xmlHeaderBytes) { diff --git a/net/ghttp/ghttp_request_param_form.go b/net/ghttp/ghttp_request_param_form.go index 630e9ea74..2e09b8b00 100644 --- a/net/ghttp/ghttp_request_param_form.go +++ b/net/ghttp/ghttp_request_param_form.go @@ -188,13 +188,18 @@ func (r *Request) GetFormMapStrVar(kvMap ...map[string]interface{}) map[string]* // given struct object. Note that the parameter is a pointer to the struct object. // The optional parameter is used to specify the key to attribute mapping. func (r *Request) GetFormStruct(pointer interface{}, mapping ...map[string]string) error { + _, err := r.doGetFormStruct(pointer, mapping...) + return err +} + +func (r *Request) doGetFormStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) { r.parseForm() - data := r.formMap + data = r.formMap if data == nil { data = map[string]interface{}{} } if err := r.mergeDefaultStructValue(data, pointer); err != nil { - return nil + return data, nil } - return gconv.Struct(data, pointer, mapping...) + return data, gconv.Struct(data, pointer, mapping...) } diff --git a/net/ghttp/ghttp_request_param_query.go b/net/ghttp/ghttp_request_param_query.go index a7257ecbf..b108b4b3c 100644 --- a/net/ghttp/ghttp_request_param_query.go +++ b/net/ghttp/ghttp_request_param_query.go @@ -196,13 +196,18 @@ func (r *Request) GetQueryMapStrVar(kvMap ...map[string]interface{}) map[string] // to the struct object. The optional parameter is used to specify the key to // attribute mapping. func (r *Request) GetQueryStruct(pointer interface{}, mapping ...map[string]string) error { + _, err := r.doGetQueryStruct(pointer, mapping...) + return err +} + +func (r *Request) doGetQueryStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) { r.parseQuery() - data := r.GetQueryMap() + data = r.GetQueryMap() if data == nil { data = map[string]interface{}{} } if err := r.mergeDefaultStructValue(data, pointer); err != nil { - return nil + return data, nil } - return gconv.Struct(data, pointer, mapping...) + return data, gconv.Struct(data, pointer, mapping...) } diff --git a/net/ghttp/ghttp_request_param_request.go b/net/ghttp/ghttp_request_param_request.go index 95a2be8e0..2e62a1992 100644 --- a/net/ghttp/ghttp_request_param_request.go +++ b/net/ghttp/ghttp_request_param_request.go @@ -270,14 +270,19 @@ func (r *Request) GetRequestMapStrVar(kvMap ...map[string]interface{}) map[strin // the parameter is a pointer to the struct object. // The optional parameter is used to specify the key to attribute mapping. func (r *Request) GetRequestStruct(pointer interface{}, mapping ...map[string]string) error { - data := r.GetRequestMap() + _, err := r.doGetRequestStruct(pointer, mapping...) + return err +} + +func (r *Request) doGetRequestStruct(pointer interface{}, mapping ...map[string]string) (data map[string]interface{}, err error) { + data = r.GetRequestMap() if data == nil { data = map[string]interface{}{} } if err := r.mergeDefaultStructValue(data, pointer); err != nil { - return nil + return data, nil } - return gconv.Struct(data, pointer, mapping...) + return data, gconv.Struct(data, pointer, mapping...) } // mergeDefaultStructValue merges the request parameters with default values from struct tag definition. diff --git a/net/ghttp/ghttp_response_view.go b/net/ghttp/ghttp_response_view.go index e3361197c..194d04c0d 100644 --- a/net/ghttp/ghttp_response_view.go +++ b/net/ghttp/ghttp_response_view.go @@ -59,18 +59,18 @@ func (r *Response) WriteTplContent(content string, params ...gview.Params) error // ParseTpl parses given template file with given template variables // and returns the parsed template content. func (r *Response) ParseTpl(tpl string, params ...gview.Params) (string, error) { - return r.Request.GetView().Parse(tpl, r.buildInVars(params...)) + return r.Request.GetView().Parse(r.Request.Context(), tpl, r.buildInVars(params...)) } // ParseDefault parses the default template file with params. func (r *Response) ParseTplDefault(params ...gview.Params) (string, error) { - return r.Request.GetView().ParseDefault(r.buildInVars(params...)) + return r.Request.GetView().ParseDefault(r.Request.Context(), r.buildInVars(params...)) } // ParseTplContent parses given template file with given template parameters // and returns the parsed template content. func (r *Response) ParseTplContent(content string, params ...gview.Params) (string, error) { - return r.Request.GetView().ParseContent(content, r.buildInVars(params...)) + return r.Request.GetView().ParseContent(r.Request.Context(), content, r.buildInVars(params...)) } // buildInVars merges build-in variables into and returns the new template variables. diff --git a/net/ghttp/ghttp_server.go b/net/ghttp/ghttp_server.go index af9edfd5c..5896ace8c 100644 --- a/net/ghttp/ghttp_server.go +++ b/net/ghttp/ghttp_server.go @@ -78,7 +78,7 @@ func serverProcessInit() { // It's an ugly calling for better initializing the main package path // in source development environment. It is useful only be used in main goroutine. - // It fails retrieving the main package path in asynchronized goroutines. + // It fails retrieving the main package path in asynchronous goroutines. gfile.MainPkgPath() } @@ -193,7 +193,7 @@ func (s *Server) Start() error { // If this is a child process, it then notifies its parent exit. if gproc.IsChild() { - gtimer.SetTimeout(2*time.Second, func() { + gtimer.SetTimeout(time.Duration(s.config.GracefulTimeout)*time.Second, func() { if err := gproc.Send(gproc.PPid(), []byte("exit"), adminGProcCommGroup); err != nil { //glog.Error("server error in process communication:", err) } @@ -268,7 +268,7 @@ func (s *Server) GetRouterArray() []RouterItem { item.Middleware += gdebug.FuncName(v) } } - // If the domain does not exist in the dump map, it create the map. + // If the domain does not exist in the dump map, it creates the map. // The value of the map is a custom sorted array. if _, ok := m[item.Domain]; !ok { // Sort in ASC order. @@ -416,7 +416,7 @@ func (s *Server) startServer(fdMap listenerFdMap) { s.servers = append(s.servers, s.newGracefulServer(itemFunc)) } } - // Start listening asynchronizedly. + // Start listening asynchronously. serverRunning.Add(1) for _, v := range s.servers { go func(server *gracefulServer) { diff --git a/net/ghttp/ghttp_server_admin.go b/net/ghttp/ghttp_server_admin.go index ee47fc2b5..dc4b107ea 100644 --- a/net/ghttp/ghttp_server_admin.go +++ b/net/ghttp/ghttp_server_admin.go @@ -26,7 +26,7 @@ func (p *utilAdmin) Index(r *Request) { "path": gfile.SelfPath(), "uri": strings.TrimRight(r.URL.Path, "/"), } - buffer, _ := gview.ParseContent(` + buffer, _ := gview.ParseContent(r.Context(), ` GoFrame Web Server Admin diff --git a/net/ghttp/ghttp_server_config.go b/net/ghttp/ghttp_server_config.go index 760b17b43..e7b19c926 100644 --- a/net/ghttp/ghttp_server_config.go +++ b/net/ghttp/ghttp_server_config.go @@ -218,6 +218,9 @@ type ServerConfig struct { // Graceful enables graceful reload feature for all servers of the process. Graceful bool `json:"graceful"` + + // GracefulTimeout set the maximum survival time (seconds) of the parent process. + GracefulTimeout uint8 `json:"gracefulTimeout"` } // Deprecated. Use NewConfig instead. @@ -265,6 +268,7 @@ func NewConfig() ServerConfig { FormParsingMemory: 1024 * 1024, // 1MB Rewrites: make(map[string]string), Graceful: false, + GracefulTimeout: 2, // seconds } } diff --git a/net/ghttp/ghttp_server_handler.go b/net/ghttp/ghttp_server_handler.go index 5b0331ad4..bc2bb05d9 100644 --- a/net/ghttp/ghttp_server_handler.go +++ b/net/ghttp/ghttp_server_handler.go @@ -80,9 +80,9 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Close the request and response body // to release the file descriptor in time. - request.Request.Body.Close() + _ = request.Request.Body.Close() if request.Request.Response != nil { - request.Request.Response.Body.Close() + _ = request.Request.Response.Body.Close() } }() @@ -168,7 +168,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Automatically set the session id to cookie - // if it creates a new session id in this request. + // if it creates a new session id in this request + // and SessionCookieOutput is enabled. if s.config.SessionCookieOutput && request.Session.IsDirty() && request.Session.Id() != request.GetSessionId() { diff --git a/net/ghttp/ghttp_server_pprof.go b/net/ghttp/ghttp_server_pprof.go index dc63a6a74..cef4f2d35 100644 --- a/net/ghttp/ghttp_server_pprof.go +++ b/net/ghttp/ghttp_server_pprof.go @@ -62,7 +62,7 @@ func (p *utilPProf) Index(r *Request) { "profiles": profiles, } if len(action) == 0 { - buffer, _ := gview.ParseContent(` + buffer, _ := gview.ParseContent(r.Context(), ` GoFrame PProf diff --git a/net/ghttp/ghttp_unit_request_json_test.go b/net/ghttp/ghttp_unit_request_json_test.go index 656ad6073..7feb32e52 100644 --- a/net/ghttp/ghttp_unit_request_json_test.go +++ b/net/ghttp/ghttp_unit_request_json_test.go @@ -23,7 +23,7 @@ func Test_Params_Json_Request(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` + Pass2 string `p:"password2" v:"password2@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` } p, _ := ports.PopRand() s := g.Server(p) @@ -144,7 +144,7 @@ func Test_Params_Json_Response(t *testing.T) { client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) map1 := make(map[string]interface{}) - err1 := json.Unmarshal([]byte(client.GetContent("/json1")), &map1) + err1 := json.UnmarshalUseNumber([]byte(client.GetContent("/json1")), &map1) t.Assert(err1, nil) t.Assert(len(map1), 4) t.Assert(map1["Name"], "john") @@ -153,7 +153,7 @@ func Test_Params_Json_Response(t *testing.T) { t.Assert(map1["password2"], "456") map2 := make(map[string]interface{}) - err2 := json.Unmarshal([]byte(client.GetContent("/json2")), &map2) + err2 := json.UnmarshalUseNumber([]byte(client.GetContent("/json2")), &map2) t.Assert(err2, nil) t.Assert(len(map2), 4) t.Assert(map2["Name"], "john") @@ -162,14 +162,14 @@ func Test_Params_Json_Response(t *testing.T) { t.Assert(map2["password2"], "456") map3 := make(map[string]interface{}) - err3 := json.Unmarshal([]byte(client.GetContent("/json3")), &map3) + err3 := json.UnmarshalUseNumber([]byte(client.GetContent("/json3")), &map3) t.Assert(err3, nil) t.Assert(len(map3), 2) t.Assert(map3["success"], "true") t.Assert(map3["message"], g.Map{"body": "测试", "code": 3, "error": "error"}) map4 := make(map[string]interface{}) - err4 := json.Unmarshal([]byte(client.GetContent("/json4")), &map4) + err4 := json.UnmarshalUseNumber([]byte(client.GetContent("/json4")), &map4) t.Assert(err4, nil) t.Assert(len(map4), 2) t.Assert(map4["success"], "true") diff --git a/net/ghttp/ghttp_unit_request_struct_test.go b/net/ghttp/ghttp_unit_request_struct_test.go index 4bdd503a8..5f4601744 100644 --- a/net/ghttp/ghttp_unit_request_struct_test.go +++ b/net/ghttp/ghttp_unit_request_struct_test.go @@ -395,7 +395,7 @@ func Test_Params_Struct(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"passwd1 @required|length:2,20|password3#||密码强度不足"` + Pass2 string `p:"password2" v:"password2 @required|length:2,20|password3#||密码强度不足"` } p, _ := ports.PopRand() s := g.Server(p) @@ -425,7 +425,7 @@ func Test_Params_Struct(t *testing.T) { if err := r.GetStruct(user); err != nil { r.Response.WriteExit(err) } - if err := gvalid.CheckStruct(user, nil); err != nil { + if err := gvalid.CheckStruct(r.Context(), user, nil); err != nil { r.Response.WriteExit(err) } } @@ -452,8 +452,8 @@ func Test_Params_Struct(t *testing.T) { t.Assert(client.PostContent("/struct1", `id=1&name=john&password1=123&password2=456`), `1john123456`) t.Assert(client.PostContent("/struct2", `id=1&name=john&password1=123&password2=456`), `1john123456`) t.Assert(client.PostContent("/struct2", ``), ``) - t.Assert(client.PostContent("/struct-valid", `id=1&name=john&password1=123&password2=0`), `The passwd1 value length must be between 2 and 20; 密码强度不足`) - t.Assert(client.PostContent("/parse", `id=1&name=john&password1=123&password2=0`), `The passwd1 value length must be between 2 and 20; 密码强度不足`) + t.Assert(client.PostContent("/struct-valid", `id=1&name=john&password1=123&password2=0`), `The password2 value length must be between 2 and 20; 密码强度不足`) + t.Assert(client.PostContent("/parse", `id=1&name=john&password1=123&password2=0`), `The password2 value length must be between 2 and 20; 密码强度不足`) t.Assert(client.PostContent("/parse", `{"id":1,"name":"john","password1":"123Abc!@#","password2":"123Abc!@#"}`), `1john123Abc!@#123Abc!@#`) }) } @@ -464,7 +464,7 @@ func Test_Params_Structs(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"passwd1 @required|length:2,20|password3#||密码强度不足"` + Pass2 string `p:"password2" v:"password2 @required|length:2,20|password3#||密码强度不足"` } p, _ := ports.PopRand() s := g.Server(p) @@ -491,3 +491,39 @@ func Test_Params_Structs(t *testing.T) { ) }) } + +func Test_Params_Struct_Validation(t *testing.T) { + type User struct { + Id int `v:"required"` + Name string `v:"name@required-with:id"` + } + p, _ := ports.PopRand() + s := g.Server(p) + s.Group("/", func(group *ghttp.RouterGroup) { + group.ALL("/", func(r *ghttp.Request) { + var ( + err error + user *User + ) + err = r.Parse(&user) + if err != nil { + r.Response.WriteExit(err) + } + r.Response.WriteExit(user.Id, user.Name) + }) + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + c := g.Client() + c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p)) + t.Assert(c.GetContent("/", ``), `The Id field is required`) + t.Assert(c.GetContent("/", `id=1&name=john`), `1john`) + t.Assert(c.PostContent("/", `id=1&name=john&password1=123&password2=456`), `1john`) + t.Assert(c.PostContent("/", `id=1`), `The name field is required`) + }) +} diff --git a/net/ghttp/ghttp_unit_request_test.go b/net/ghttp/ghttp_unit_request_test.go index f7b28618a..26880df6b 100644 --- a/net/ghttp/ghttp_unit_request_test.go +++ b/net/ghttp/ghttp_unit_request_test.go @@ -586,3 +586,38 @@ func Test_Params_Parse_DefaultValueTag(t *testing.T) { t.Assert(client.PostContent("/parse", `{"name":"smith", "score":100}`), `{"Name":"smith","Score":100}`) }) } + +func Test_Params_Parse_Validation(t *testing.T) { + type RegisterReq struct { + Name string `p:"username" v:"required|length:6,30#请输入账号|账号长度为:min到:max位"` + Pass string `p:"password1" v:"required|length:6,30#请输入密码|密码长度不够"` + Pass2 string `p:"password2" v:"required|length:6,30|same:password1#请确认密码|密码长度不够|两次密码不一致"` + } + + p, _ := ports.PopRand() + s := g.Server(p) + s.BindHandler("/parse", func(r *ghttp.Request) { + var req *RegisterReq + if err := r.Parse(&req); err != nil { + r.Response.Write(err) + } else { + r.Response.Write("ok") + } + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + prefix := fmt.Sprintf("http://127.0.0.1:%d", p) + client := g.Client() + client.SetPrefix(prefix) + + t.Assert(client.GetContent("/parse"), `请输入账号; 账号长度为6到30位; 请输入密码; 密码长度不够; 请确认密码; 密码长度不够; 两次密码不一致`) + t.Assert(client.GetContent("/parse?name=john11&password1=123456&password2=123"), `密码长度不够; 两次密码不一致`) + t.Assert(client.GetContent("/parse?name=john&password1=123456&password2=123456"), `账号长度为6到30位`) + t.Assert(client.GetContent("/parse?name=john11&password1=123456&password2=123456"), `ok`) + }) +} diff --git a/net/ghttp/ghttp_unit_request_xml_test.go b/net/ghttp/ghttp_unit_request_xml_test.go index 37252b9cc..6dd253d11 100644 --- a/net/ghttp/ghttp_unit_request_xml_test.go +++ b/net/ghttp/ghttp_unit_request_xml_test.go @@ -22,7 +22,7 @@ func Test_Params_Xml_Request(t *testing.T) { Name string Time *time.Time Pass1 string `p:"password1"` - Pass2 string `p:"password2" v:"required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` + Pass2 string `p:"password2" v:"password2@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致"` } p, _ := ports.PopRand() s := g.Server(p) diff --git a/net/ghttp/ghttp_unit_websocket_client_test.go b/net/ghttp/ghttp_unit_websocket_client_test.go new file mode 100644 index 000000000..823130ee1 --- /dev/null +++ b/net/ghttp/ghttp_unit_websocket_client_test.go @@ -0,0 +1,64 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package ghttp_test + +import ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/gorilla/websocket" + + "github.com/gogf/gf/frame/g" + "github.com/gogf/gf/net/ghttp" + "github.com/gogf/gf/test/gtest" +) + +func Test_WebSocketClient(t *testing.T) { + p, _ := ports.PopRand() + s := g.Server(p) + s.BindHandler("/ws", func(r *ghttp.Request) { + ws, err := r.WebSocket() + if err != nil { + r.Exit() + } + for { + msgType, msg, err := ws.ReadMessage() + if err != nil { + return + } + if err = ws.WriteMessage(msgType, msg); err != nil { + return + } + } + }) + s.SetPort(p) + s.SetDumpRouterMap(false) + s.Start() + defer s.Shutdown() + + time.Sleep(100 * time.Millisecond) + gtest.C(t, func(t *gtest.T) { + client := ghttp.NewWebSocketClient() + client.Proxy = http.ProxyFromEnvironment + client.HandshakeTimeout = time.Minute + + conn, _, err := client.Dial(fmt.Sprintf("ws://127.0.0.1:%d/ws", p), nil) + t.Assert(err, nil) + defer conn.Close() + + msg := []byte("hello") + err = conn.WriteMessage(websocket.TextMessage, msg) + t.Assert(err, nil) + + mt, data, err := conn.ReadMessage() + t.Assert(err, nil) + t.Assert(mt, websocket.TextMessage) + t.Assert(data, msg) + }) +} diff --git a/net/gtrace/gtrace_carrier.go b/net/gtrace/gtrace_carrier.go index c0cd41979..997f2ea91 100644 --- a/net/gtrace/gtrace_carrier.go +++ b/net/gtrace/gtrace_carrier.go @@ -55,5 +55,5 @@ func (c Carrier) String() string { func (c Carrier) UnmarshalJSON(b []byte) error { carrier := NewCarrier(nil) - return json.Unmarshal(b, carrier) + return json.UnmarshalUseNumber(b, carrier) } diff --git a/os/gbuild/gbuild.go b/os/gbuild/gbuild.go index 7f5cd6e99..5447c0cb7 100644 --- a/os/gbuild/gbuild.go +++ b/os/gbuild/gbuild.go @@ -24,7 +24,7 @@ var ( func init() { if builtInVarStr != "" { - err := json.Unmarshal(gbase64.MustDecodeString(builtInVarStr), &builtInVarMap) + err := json.UnmarshalUseNumber(gbase64.MustDecodeString(builtInVarStr), &builtInVarMap) if err != nil { intlog.Error(err) } diff --git a/os/gcfg/gcfg.go b/os/gcfg/gcfg.go index 76f5f7985..d9eac9f2b 100644 --- a/os/gcfg/gcfg.go +++ b/os/gcfg/gcfg.go @@ -8,32 +8,13 @@ package gcfg import ( - "bytes" - "errors" - "fmt" - "github.com/gogf/gf/errors/gerror" - "github.com/gogf/gf/internal/intlog" - "github.com/gogf/gf/os/gcmd" - "github.com/gogf/gf/text/gstr" - "github.com/gogf/gf/util/gmode" - - "github.com/gogf/gf/os/gres" - "github.com/gogf/gf/container/garray" "github.com/gogf/gf/container/gmap" - "github.com/gogf/gf/encoding/gjson" - "github.com/gogf/gf/os/gfile" - "github.com/gogf/gf/os/gfsnotify" - "github.com/gogf/gf/os/glog" - "github.com/gogf/gf/os/gspath" + "github.com/gogf/gf/internal/intlog" + "github.com/gogf/gf/os/gcmd" ) -const ( - DefaultConfigFile = "config.toml" // The default configuration file name. - cmdEnvKey = "gf.gcfg" // Configuration key for command argument or environment. -) - -// Configuration struct. +// Config is the configuration manager. type Config struct { defaultName string // Default configuration file name. searchPaths *garray.StrArray // Searching path array. @@ -41,374 +22,82 @@ type Config struct { violenceCheck bool // Whether do violence check in value index searching. It affects the performance when set true(false in default). } -var ( - supportedFileTypes = []string{"toml", "yaml", "yml", "json", "ini", "xml"} - resourceTryFiles = []string{"", "/", "config/", "config", "/config", "/config/"} +const ( + DefaultName = "config" // DefaultName is the default group name for instance usage. + DefaultConfigFile = "config.toml" // DefaultConfigFile is the default configuration file name. + cmdEnvKey = "gf.gcfg" // cmdEnvKey is the configuration key for command argument or environment. + errorPrintKey = "gf.gcfg.errorprint" // errorPrintKey is used to specify the key controlling error printing to stdout. ) -// New returns a new configuration management object. -// The parameter `file` specifies the default configuration file name for reading. -func New(file ...string) *Config { +var ( + supportedFileTypes = []string{"toml", "yaml", "yml", "json", "ini", "xml"} // All supported file types suffixes. + resourceTryFiles = []string{"", "/", "config/", "config", "/config", "/config/"} // Prefix array for trying searching in resource manager. + instances = gmap.NewStrAnyMap(true) // Instances map containing configuration instances. + customConfigContentMap = gmap.NewStrStrMap(true) // Customized configuration content. +) + +// SetContent sets customized configuration content for specified `file`. +// The `file` is unnecessary param, default is DefaultConfigFile. +func SetContent(content string, file ...string) { name := DefaultConfigFile if len(file) > 0 { name = file[0] - } else { - // Custom default configuration file name from command line or environment. - if customFile := gcmd.GetOptWithEnv(fmt.Sprintf("%s.file", cmdEnvKey)).String(); customFile != "" { - name = customFile - } } - c := &Config{ - defaultName: name, - searchPaths: garray.NewStrArray(true), - jsonMap: gmap.NewStrAnyMap(true), - } - // Customized dir path from env/cmd. - if customPath := gcmd.GetOptWithEnv(fmt.Sprintf("%s.path", cmdEnvKey)).String(); customPath != "" { - if gfile.Exists(customPath) { - _ = c.SetPath(customPath) - } else { - if errorPrint() { - glog.Errorf("[gcfg] Configuration directory path does not exist: %s", customPath) + // Clear file cache for instances which cached `name`. + instances.LockFunc(func(m map[string]interface{}) { + if customConfigContentMap.Contains(name) { + for _, v := range m { + v.(*Config).jsonMap.Remove(name) } } - } else { - // Dir path of working dir. - if err := c.AddPath(gfile.Pwd()); err != nil { - intlog.Error(err) - } - - // Dir path of main package. - if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) { - if err := c.AddPath(mainPath); err != nil { - intlog.Error(err) - } - } - - // Dir path of binary. - if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) { - if err := c.AddPath(selfPath); err != nil { - intlog.Error(err) - } - } - } - return c + customConfigContentMap.Set(name, content) + }) } -// SetPath sets the configuration directory path for file search. -// The parameter `path` can be absolute or relative path, -// but absolute path is strongly recommended. -func (c *Config) SetPath(path string) error { - var ( - isDir = false - realPath = "" - ) - if file := gres.Get(path); file != nil { - realPath = path - isDir = file.FileInfo().IsDir() - } else { - // Absolute path. - realPath = gfile.RealPath(path) - if realPath == "" { - // Relative path. - c.searchPaths.RLockFunc(func(array []string) { - for _, v := range array { - if path, _ := gspath.Search(v, path); path != "" { - realPath = path - break - } - } - }) - } - if realPath != "" { - isDir = gfile.IsDir(realPath) - } - } - // Path not exist. - if realPath == "" { - buffer := bytes.NewBuffer(nil) - if c.searchPaths.Len() > 0 { - buffer.WriteString(fmt.Sprintf("[gcfg] SetPath failed: cannot find directory \"%s\" in following paths:", path)) - c.searchPaths.RLockFunc(func(array []string) { - for k, v := range array { - buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v)) - } - }) - } else { - buffer.WriteString(fmt.Sprintf(`[gcfg] SetPath failed: path "%s" does not exist`, path)) - } - err := errors.New(buffer.String()) - if errorPrint() { - glog.Error(err) - } - return err - } - // Should be a directory. - if !isDir { - err := fmt.Errorf(`[gcfg] SetPath failed: path "%s" should be directory type`, path) - if errorPrint() { - glog.Error(err) - } - return err - } - // Repeated path check. - if c.searchPaths.Search(realPath) != -1 { - return nil - } - c.jsonMap.Clear() - c.searchPaths.Clear() - c.searchPaths.Append(realPath) - intlog.Print("SetPath:", realPath) - return nil -} - -// SetViolenceCheck sets whether to perform hierarchical conflict checking. -// This feature needs to be enabled when there is a level symbol in the key name. -// It is off in default. -// -// Note that, turning on this feature is quite expensive, and it is not recommended -// to allow separators in the key names. It is best to avoid this on the application side. -func (c *Config) SetViolenceCheck(check bool) { - c.violenceCheck = check - c.Clear() -} - -// AddPath adds a absolute or relative path to the search paths. -func (c *Config) AddPath(path string) error { - var ( - isDir = false - realPath = "" - ) - // It firstly checks the resource manager, - // and then checks the filesystem for the path. - if file := gres.Get(path); file != nil { - realPath = path - isDir = file.FileInfo().IsDir() - } else { - // Absolute path. - realPath = gfile.RealPath(path) - if realPath == "" { - // Relative path. - c.searchPaths.RLockFunc(func(array []string) { - for _, v := range array { - if path, _ := gspath.Search(v, path); path != "" { - realPath = path - break - } - } - }) - } - if realPath != "" { - isDir = gfile.IsDir(realPath) - } - } - if realPath == "" { - buffer := bytes.NewBuffer(nil) - if c.searchPaths.Len() > 0 { - buffer.WriteString(fmt.Sprintf("[gcfg] AddPath failed: cannot find directory \"%s\" in following paths:", path)) - c.searchPaths.RLockFunc(func(array []string) { - for k, v := range array { - buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v)) - } - }) - } else { - buffer.WriteString(fmt.Sprintf(`[gcfg] AddPath failed: path "%s" does not exist`, path)) - } - err := gerror.New(buffer.String()) - if errorPrint() { - glog.Error(err) - } - return err - } - if !isDir { - err := gerror.Newf(`[gcfg] AddPath failed: path "%s" should be directory type`, path) - if errorPrint() { - glog.Error(err) - } - return err - } - // Repeated path check. - if c.searchPaths.Search(realPath) != -1 { - return nil - } - c.searchPaths.Append(realPath) - intlog.Print("AddPath:", realPath) - return nil -} - -// SetFileName sets the default configuration file name. -func (c *Config) SetFileName(name string) *Config { - c.defaultName = name - return c -} - -// GetFileName returns the default configuration file name. -func (c *Config) GetFileName() string { - return c.defaultName -} - -// Available checks and returns whether configuration of given `file` is available. -func (c *Config) Available(file ...string) bool { - var name string - if len(file) > 0 && file[0] != "" { - name = file[0] - } else { - name = c.defaultName - } - if path, _ := c.GetFilePath(name); path != "" { - return true - } - if GetContent(name) != "" { - return true - } - return false -} - -// GetFilePath returns the absolute configuration file path for the given filename by `file`. -// If `file` is not passed, it returns the configuration file path of the default name. -// It returns an empty `path` string and an error if the given `file` does not exist. -func (c *Config) GetFilePath(file ...string) (path string, err error) { - name := c.defaultName +// GetContent returns customized configuration content for specified `file`. +// The `file` is unnecessary param, default is DefaultConfigFile. +func GetContent(file ...string) string { + name := DefaultConfigFile if len(file) > 0 { name = file[0] } - // Searching resource manager. - if !gres.IsEmpty() { - for _, v := range resourceTryFiles { - if file := gres.Get(v + name); file != nil { - path = file.Name() - return - } - } - c.searchPaths.RLockFunc(func(array []string) { - for _, prefix := range array { - for _, v := range resourceTryFiles { - if file := gres.Get(prefix + v + name); file != nil { - path = file.Name() - return - } - } - } - }) - } - c.autoCheckAndAddMainPkgPathToSearchPaths() - // Searching the file system. - c.searchPaths.RLockFunc(func(array []string) { - for _, prefix := range array { - prefix = gstr.TrimRight(prefix, `\/`) - if path, _ = gspath.Search(prefix, name); path != "" { - return - } - if path, _ = gspath.Search(prefix+gfile.Separator+"config", name); path != "" { - return - } - } - }) - // If it cannot find the path of `file`, it formats and returns a detailed error. - if path == "" { - var ( - buffer = bytes.NewBuffer(nil) - ) - if c.searchPaths.Len() > 0 { - buffer.WriteString(fmt.Sprintf(`[gcfg] cannot find config file "%s" in resource manager or the following paths:`, name)) - c.searchPaths.RLockFunc(func(array []string) { - index := 1 - for _, v := range array { - v = gstr.TrimRight(v, `\/`) - buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v)) - index++ - buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v+gfile.Separator+"config")) - index++ - } - }) - } else { - buffer.WriteString(fmt.Sprintf("[gcfg] cannot find config file \"%s\" with no path configured", name)) - } - err = gerror.New(buffer.String()) - } - return + return customConfigContentMap.Get(name) } -// autoCheckAndAddMainPkgPathToSearchPaths automatically checks and adds directory path of package main -// to the searching path list if it's currently in development environment. -func (c *Config) autoCheckAndAddMainPkgPathToSearchPaths() { - if gmode.IsDevelop() { - mainPkgPath := gfile.MainPkgPath() - if mainPkgPath != "" { - if !c.searchPaths.Contains(mainPkgPath) { - c.searchPaths.Append(mainPkgPath) - } - } - } -} - -// getJson returns a *gjson.Json object for the specified `file` content. -// It would print error if file reading fails. It return nil if any error occurs. -func (c *Config) getJson(file ...string) *gjson.Json { - var name string - if len(file) > 0 && file[0] != "" { +// RemoveContent removes the global configuration with specified `file`. +// If `name` is not passed, it removes configuration of the default group name. +func RemoveContent(file ...string) { + name := DefaultConfigFile + if len(file) > 0 { name = file[0] - } else { - name = c.defaultName } - r := c.jsonMap.GetOrSetFuncLock(name, func() interface{} { - var ( - err error - content string - filePath string - ) - // The configured content can be any kind of data type different from its file type. - isFromConfigContent := true - if content = GetContent(name); content == "" { - isFromConfigContent = false - filePath, err = c.GetFilePath(name) - if err != nil && errorPrint() { - glog.Error(err) - } - if filePath == "" { - return nil - } - if file := gres.Get(filePath); file != nil { - content = string(file.Content()) - } else { - content = gfile.GetContents(filePath) + // Clear file cache for instances which cached `name`. + instances.LockFunc(func(m map[string]interface{}) { + if customConfigContentMap.Contains(name) { + for _, v := range m { + v.(*Config).jsonMap.Remove(name) } + customConfigContentMap.Remove(name) } - // Note that the underlying configuration json object operations are concurrent safe. - var ( - j *gjson.Json - ) - dataType := gfile.ExtName(name) - if gjson.IsValidDataType(dataType) && !isFromConfigContent { - j, err = gjson.LoadContentType(dataType, content, true) - } else { - j, err = gjson.LoadContent(content, true) - } - if err == nil { - j.SetViolenceCheck(c.violenceCheck) - // Add monitor for this configuration file, - // any changes of this file will refresh its cache in Config object. - if filePath != "" && !gres.Contains(filePath) { - _, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) { - c.jsonMap.Remove(name) - }) - if err != nil && errorPrint() { - glog.Error(err) - } - } - return j - } - if errorPrint() { - if filePath != "" { - glog.Criticalf(`[gcfg] load config file "%s" failed: %s`, filePath, err.Error()) - } else { - glog.Criticalf(`[gcfg] load configuration failed: %s`, err.Error()) - } - } - return nil }) - if r != nil { - return r.(*gjson.Json) - } - return nil + + intlog.Printf(`RemoveContent: %s`, name) +} + +// ClearContent removes all global configuration contents. +func ClearContent() { + customConfigContentMap.Clear() + // Clear cache for all instances. + instances.LockFunc(func(m map[string]interface{}) { + for _, v := range m { + v.(*Config).jsonMap.Clear() + } + }) + + intlog.Print(`RemoveConfig`) +} + +// errorPrint checks whether printing error to stdout. +func errorPrint() bool { + return gcmd.GetOptWithEnv(errorPrintKey, true).Bool() } diff --git a/os/gcfg/gcfg_config.go b/os/gcfg/gcfg_config.go index 68adf275a..6034f1f88 100644 --- a/os/gcfg/gcfg_config.go +++ b/os/gcfg/gcfg_config.go @@ -7,72 +7,413 @@ package gcfg import ( + "bytes" + "errors" + "fmt" + "github.com/gogf/gf/container/garray" "github.com/gogf/gf/container/gmap" + "github.com/gogf/gf/encoding/gjson" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/intlog" + "github.com/gogf/gf/os/gcmd" + "github.com/gogf/gf/os/gfile" + "github.com/gogf/gf/os/gfsnotify" + "github.com/gogf/gf/os/glog" + "github.com/gogf/gf/os/gres" + "github.com/gogf/gf/os/gspath" + "github.com/gogf/gf/text/gstr" + "github.com/gogf/gf/util/gmode" ) -var ( - // Customized configuration content. - configs = gmap.NewStrStrMap(true) -) - -// SetContent sets customized configuration content for specified `file`. -// The `file` is unnecessary param, default is DefaultConfigFile. -func SetContent(content string, file ...string) { +// New returns a new configuration management object. +// The parameter `file` specifies the default configuration file name for reading. +func New(file ...string) *Config { name := DefaultConfigFile if len(file) > 0 { name = file[0] + } else { + // Custom default configuration file name from command line or environment. + if customFile := gcmd.GetOptWithEnv(fmt.Sprintf("%s.file", cmdEnvKey)).String(); customFile != "" { + name = customFile + } } - // Clear file cache for instances which cached `name`. - instances.LockFunc(func(m map[string]interface{}) { - if configs.Contains(name) { - for _, v := range m { - v.(*Config).jsonMap.Remove(name) + c := &Config{ + defaultName: name, + searchPaths: garray.NewStrArray(true), + jsonMap: gmap.NewStrAnyMap(true), + } + // Customized dir path from env/cmd. + if customPath := gcmd.GetOptWithEnv(fmt.Sprintf("%s.path", cmdEnvKey)).String(); customPath != "" { + if gfile.Exists(customPath) { + _ = c.SetPath(customPath) + } else { + if errorPrint() { + glog.Errorf("[gcfg] Configuration directory path does not exist: %s", customPath) } } - configs.Set(name, content) - }) -} + } else { + // Dir path of working dir. + if err := c.AddPath(gfile.Pwd()); err != nil { + intlog.Error(err) + } -// GetContent returns customized configuration content for specified `file`. -// The `file` is unnecessary param, default is DefaultConfigFile. -func GetContent(file ...string) string { - name := DefaultConfigFile - if len(file) > 0 { - name = file[0] - } - return configs.Get(name) -} - -// RemoveContent removes the global configuration with specified `file`. -// If `name` is not passed, it removes configuration of the default group name. -func RemoveContent(file ...string) { - name := DefaultConfigFile - if len(file) > 0 { - name = file[0] - } - // Clear file cache for instances which cached `name`. - instances.LockFunc(func(m map[string]interface{}) { - if configs.Contains(name) { - for _, v := range m { - v.(*Config).jsonMap.Remove(name) + // Dir path of main package. + if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) { + if err := c.AddPath(mainPath); err != nil { + intlog.Error(err) } - configs.Remove(name) } - }) - intlog.Printf(`RemoveContent: %s`, name) + // Dir path of binary. + if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) { + if err := c.AddPath(selfPath); err != nil { + intlog.Error(err) + } + } + } + return c } -// ClearContent removes all global configuration contents. -func ClearContent() { - configs.Clear() - // Clear cache for all instances. - instances.LockFunc(func(m map[string]interface{}) { - for _, v := range m { - v.(*Config).jsonMap.Clear() +// Instance returns an instance of Config with default settings. +// The parameter `name` is the name for the instance. But very note that, if the file "name.toml" +// exists in the configuration directory, it then sets it as the default configuration file. The +// toml file type is the default configuration file type. +func Instance(name ...string) *Config { + key := DefaultName + if len(name) > 0 && name[0] != "" { + key = name[0] + } + return instances.GetOrSetFuncLock(key, func() interface{} { + c := New() + // If it's not using default configuration or its configuration file is not available, + // it searches the possible configuration file according to the name and all supported + // file types. + if key != DefaultName || !c.Available() { + for _, fileType := range supportedFileTypes { + if file := fmt.Sprintf(`%s.%s`, key, fileType); c.Available(file) { + c.SetFileName(file) + break + } + } + } + return c + }).(*Config) +} + +// SetPath sets the configuration directory path for file search. +// The parameter `path` can be absolute or relative path, +// but absolute path is strongly recommended. +func (c *Config) SetPath(path string) error { + var ( + isDir = false + realPath = "" + ) + if file := gres.Get(path); file != nil { + realPath = path + isDir = file.FileInfo().IsDir() + } else { + // Absolute path. + realPath = gfile.RealPath(path) + if realPath == "" { + // Relative path. + c.searchPaths.RLockFunc(func(array []string) { + for _, v := range array { + if path, _ := gspath.Search(v, path); path != "" { + realPath = path + break + } + } + }) + } + if realPath != "" { + isDir = gfile.IsDir(realPath) + } + } + // Path not exist. + if realPath == "" { + buffer := bytes.NewBuffer(nil) + if c.searchPaths.Len() > 0 { + buffer.WriteString(fmt.Sprintf("[gcfg] SetPath failed: cannot find directory \"%s\" in following paths:", path)) + c.searchPaths.RLockFunc(func(array []string) { + for k, v := range array { + buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v)) + } + }) + } else { + buffer.WriteString(fmt.Sprintf(`[gcfg] SetPath failed: path "%s" does not exist`, path)) + } + err := errors.New(buffer.String()) + if errorPrint() { + glog.Error(err) + } + return err + } + // Should be a directory. + if !isDir { + err := fmt.Errorf(`[gcfg] SetPath failed: path "%s" should be directory type`, path) + if errorPrint() { + glog.Error(err) + } + return err + } + // Repeated path check. + if c.searchPaths.Search(realPath) != -1 { + return nil + } + c.jsonMap.Clear() + c.searchPaths.Clear() + c.searchPaths.Append(realPath) + intlog.Print("SetPath:", realPath) + return nil +} + +// SetViolenceCheck sets whether to perform hierarchical conflict checking. +// This feature needs to be enabled when there is a level symbol in the key name. +// It is off in default. +// +// Note that, turning on this feature is quite expensive, and it is not recommended +// to allow separators in the key names. It is best to avoid this on the application side. +func (c *Config) SetViolenceCheck(check bool) { + c.violenceCheck = check + c.Clear() +} + +// AddPath adds a absolute or relative path to the search paths. +func (c *Config) AddPath(path string) error { + var ( + isDir = false + realPath = "" + ) + // It firstly checks the resource manager, + // and then checks the filesystem for the path. + if file := gres.Get(path); file != nil { + realPath = path + isDir = file.FileInfo().IsDir() + } else { + // Absolute path. + realPath = gfile.RealPath(path) + if realPath == "" { + // Relative path. + c.searchPaths.RLockFunc(func(array []string) { + for _, v := range array { + if path, _ := gspath.Search(v, path); path != "" { + realPath = path + break + } + } + }) + } + if realPath != "" { + isDir = gfile.IsDir(realPath) + } + } + if realPath == "" { + buffer := bytes.NewBuffer(nil) + if c.searchPaths.Len() > 0 { + buffer.WriteString(fmt.Sprintf("[gcfg] AddPath failed: cannot find directory \"%s\" in following paths:", path)) + c.searchPaths.RLockFunc(func(array []string) { + for k, v := range array { + buffer.WriteString(fmt.Sprintf("\n%d. %s", k+1, v)) + } + }) + } else { + buffer.WriteString(fmt.Sprintf(`[gcfg] AddPath failed: path "%s" does not exist`, path)) + } + err := gerror.New(buffer.String()) + if errorPrint() { + glog.Error(err) + } + return err + } + if !isDir { + err := gerror.Newf(`[gcfg] AddPath failed: path "%s" should be directory type`, path) + if errorPrint() { + glog.Error(err) + } + return err + } + // Repeated path check. + if c.searchPaths.Search(realPath) != -1 { + return nil + } + c.searchPaths.Append(realPath) + intlog.Print("AddPath:", realPath) + return nil +} + +// SetFileName sets the default configuration file name. +func (c *Config) SetFileName(name string) *Config { + c.defaultName = name + return c +} + +// GetFileName returns the default configuration file name. +func (c *Config) GetFileName() string { + return c.defaultName +} + +// Available checks and returns whether configuration of given `file` is available. +func (c *Config) Available(file ...string) bool { + var name string + if len(file) > 0 && file[0] != "" { + name = file[0] + } else { + name = c.defaultName + } + if path, _ := c.GetFilePath(name); path != "" { + return true + } + if GetContent(name) != "" { + return true + } + return false +} + +// GetFilePath returns the absolute configuration file path for the given filename by `file`. +// If `file` is not passed, it returns the configuration file path of the default name. +// It returns an empty `path` string and an error if the given `file` does not exist. +func (c *Config) GetFilePath(file ...string) (path string, err error) { + name := c.defaultName + if len(file) > 0 { + name = file[0] + } + // Searching resource manager. + if !gres.IsEmpty() { + for _, v := range resourceTryFiles { + if file := gres.Get(v + name); file != nil { + path = file.Name() + return + } + } + c.searchPaths.RLockFunc(func(array []string) { + for _, prefix := range array { + for _, v := range resourceTryFiles { + if file := gres.Get(prefix + v + name); file != nil { + path = file.Name() + return + } + } + } + }) + } + c.autoCheckAndAddMainPkgPathToSearchPaths() + // Searching the file system. + c.searchPaths.RLockFunc(func(array []string) { + for _, prefix := range array { + prefix = gstr.TrimRight(prefix, `\/`) + if path, _ = gspath.Search(prefix, name); path != "" { + return + } + if path, _ = gspath.Search(prefix+gfile.Separator+"config", name); path != "" { + return + } } }) - - intlog.Print(`RemoveConfig`) + // If it cannot find the path of `file`, it formats and returns a detailed error. + if path == "" { + var ( + buffer = bytes.NewBuffer(nil) + ) + if c.searchPaths.Len() > 0 { + buffer.WriteString(fmt.Sprintf(`[gcfg] cannot find config file "%s" in resource manager or the following paths:`, name)) + c.searchPaths.RLockFunc(func(array []string) { + index := 1 + for _, v := range array { + v = gstr.TrimRight(v, `\/`) + buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v)) + index++ + buffer.WriteString(fmt.Sprintf("\n%d. %s", index, v+gfile.Separator+"config")) + index++ + } + }) + } else { + buffer.WriteString(fmt.Sprintf("[gcfg] cannot find config file \"%s\" with no path configured", name)) + } + err = gerror.New(buffer.String()) + } + return +} + +// autoCheckAndAddMainPkgPathToSearchPaths automatically checks and adds directory path of package main +// to the searching path list if it's currently in development environment. +func (c *Config) autoCheckAndAddMainPkgPathToSearchPaths() { + if gmode.IsDevelop() { + mainPkgPath := gfile.MainPkgPath() + if mainPkgPath != "" { + if !c.searchPaths.Contains(mainPkgPath) { + c.searchPaths.Append(mainPkgPath) + } + } + } +} + +// getJson returns a *gjson.Json object for the specified `file` content. +// It would print error if file reading fails. It return nil if any error occurs. +func (c *Config) getJson(file ...string) *gjson.Json { + var name string + if len(file) > 0 && file[0] != "" { + name = file[0] + } else { + name = c.defaultName + } + r := c.jsonMap.GetOrSetFuncLock(name, func() interface{} { + var ( + err error + content string + filePath string + ) + // The configured content can be any kind of data type different from its file type. + isFromConfigContent := true + if content = GetContent(name); content == "" { + isFromConfigContent = false + filePath, err = c.GetFilePath(name) + if err != nil && errorPrint() { + glog.Error(err) + } + if filePath == "" { + return nil + } + if file := gres.Get(filePath); file != nil { + content = string(file.Content()) + } else { + content = gfile.GetContents(filePath) + } + } + // Note that the underlying configuration json object operations are concurrent safe. + var ( + j *gjson.Json + ) + dataType := gfile.ExtName(name) + if gjson.IsValidDataType(dataType) && !isFromConfigContent { + j, err = gjson.LoadContentType(dataType, content, true) + } else { + j, err = gjson.LoadContent(content, true) + } + if err == nil { + j.SetViolenceCheck(c.violenceCheck) + // Add monitor for this configuration file, + // any changes of this file will refresh its cache in Config object. + if filePath != "" && !gres.Contains(filePath) { + _, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) { + c.jsonMap.Remove(name) + }) + if err != nil && errorPrint() { + glog.Error(err) + } + } + return j + } + if errorPrint() { + if filePath != "" { + glog.Criticalf(`[gcfg] load config file "%s" failed: %s`, filePath, err.Error()) + } else { + glog.Criticalf(`[gcfg] load configuration failed: %s`, err.Error()) + } + } + return nil + }) + if r != nil { + return r.(*gjson.Json) + } + return nil } diff --git a/os/gcfg/gcfg_api.go b/os/gcfg/gcfg_config_api.go similarity index 100% rename from os/gcfg/gcfg_api.go rename to os/gcfg/gcfg_config_api.go diff --git a/os/gcfg/gcfg_error.go b/os/gcfg/gcfg_error.go deleted file mode 100644 index fd7df93b3..000000000 --- a/os/gcfg/gcfg_error.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gcfg - -import ( - "github.com/gogf/gf/os/gcmd" -) - -const ( - // errorPrintKey is used to specify the key controlling error printing to stdout. - // This error is designed not to be returned by functions. - errorPrintKey = "gf.gcfg.errorprint" -) - -// errorPrint checks whether printing error to stdout. -func errorPrint() bool { - return gcmd.GetOptWithEnv(errorPrintKey, true).Bool() -} diff --git a/os/gcfg/gcfg_instance.go b/os/gcfg/gcfg_instance.go deleted file mode 100644 index 4ef396801..000000000 --- a/os/gcfg/gcfg_instance.go +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gcfg - -import ( - "fmt" - "github.com/gogf/gf/container/gmap" -) - -const ( - // Default group name for instance usage. - DefaultName = "config" -) - -var ( - // Instances map containing configuration instances. - instances = gmap.NewStrAnyMap(true) -) - -// Instance returns an instance of Config with default settings. -// The parameter `name` is the name for the instance. But very note that, if the file "name.toml" -// exists in the configuration directory, it then sets it as the default configuration file. The -// toml file type is the default configuration file type. -func Instance(name ...string) *Config { - key := DefaultName - if len(name) > 0 && name[0] != "" { - key = name[0] - } - return instances.GetOrSetFuncLock(key, func() interface{} { - c := New() - // If it's not using default configuration or its configuration file is not available, - // it searches the possible configuration file according to the name and all supported - // file types. - if key != DefaultName || !c.Available() { - for _, fileType := range supportedFileTypes { - if file := fmt.Sprintf(`%s.%s`, key, fileType); c.Available(file) { - c.SetFileName(file) - break - } - } - } - return c - }).(*Config) -} diff --git a/os/gcron/gcron_entry.go b/os/gcron/gcron_entry.go index 94ff02f43..68fd6d14c 100644 --- a/os/gcron/gcron_entry.go +++ b/os/gcron/gcron_entry.go @@ -17,7 +17,7 @@ import ( "github.com/gogf/gf/util/gconv" ) -// Timed task entry. +// Entry is timing task entry. type Entry struct { cron *Cron // Cron object belonged to. entry *gtimer.Entry // Associated gtimer.Entry. @@ -104,7 +104,7 @@ func (entry *Entry) Close() { entry.entry.Close() } -// Timed task check execution. +// Timing task check execution. // The running times limits feature is implemented by gcron.Entry and cannot be implemented by gtimer.Entry. // gcron.Entry relies on gtimer to implement a scheduled task check for gcron.Entry per second. func (entry *Entry) check() { diff --git a/os/gfsnotify/gfsnotify.go b/os/gfsnotify/gfsnotify.go index 07817a8e5..619454189 100644 --- a/os/gfsnotify/gfsnotify.go +++ b/os/gfsnotify/gfsnotify.go @@ -91,8 +91,8 @@ func New() (*Watcher, error) { intlog.Printf("New watcher failed: %v", err) return nil, err } - w.startWatchLoop() - w.startEventLoop() + w.watchLoop() + w.eventLoop() return w, nil } diff --git a/os/gfsnotify/gfsnotify_watcher.go b/os/gfsnotify/gfsnotify_watcher.go index 421b40a59..37067a988 100644 --- a/os/gfsnotify/gfsnotify_watcher.go +++ b/os/gfsnotify/gfsnotify_watcher.go @@ -61,6 +61,7 @@ func (w *Watcher) AddOnce(name, path string, callbackFunc func(event *Event), re } // addWithCallbackFunc adds the path to underlying monitor, creates and returns a callback object. +// Very note that if it calls multiple times with the same `path`, the latest one will overwrite the previous one. func (w *Watcher) addWithCallbackFunc(name, path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) { // Check and convert the given path to absolute path. if t := fileRealPath(path); t == "" { diff --git a/os/gfsnotify/gfsnotify_watcher_loop.go b/os/gfsnotify/gfsnotify_watcher_loop.go index a05c663de..531225051 100644 --- a/os/gfsnotify/gfsnotify_watcher_loop.go +++ b/os/gfsnotify/gfsnotify_watcher_loop.go @@ -11,8 +11,8 @@ import ( "github.com/gogf/gf/internal/intlog" ) -// startWatchLoop starts the loop for event listening fro underlying inotify monitor. -func (w *Watcher) startWatchLoop() { +// watchLoop starts the loop for event listening fro underlying inotify monitor. +func (w *Watcher) watchLoop() { go func() { for { select { @@ -40,47 +40,8 @@ func (w *Watcher) startWatchLoop() { }() } -// getCallbacks searches and returns all callbacks with given . -// It also searches its parent for callbacks if they're recursive. -func (w *Watcher) getCallbacks(path string) (callbacks []*Callback) { - // Firstly add the callbacks of itself. - if v := w.callbacks.Get(path); v != nil { - for _, v := range v.(*glist.List).FrontAll() { - callback := v.(*Callback) - callbacks = append(callbacks, callback) - } - } - // Secondly searches its parent for callbacks. - dirPath := fileDir(path) - if v := w.callbacks.Get(dirPath); v != nil { - for _, v := range v.(*glist.List).FrontAll() { - callback := v.(*Callback) - if callback.recursive { - callbacks = append(callbacks, callback) - } - } - } - // Lastly searches the parent recursively for callbacks. - for { - parentDirPath := fileDir(dirPath) - if parentDirPath == dirPath { - break - } - if v := w.callbacks.Get(parentDirPath); v != nil { - for _, v := range v.(*glist.List).FrontAll() { - callback := v.(*Callback) - if callback.recursive { - callbacks = append(callbacks, callback) - } - } - } - dirPath = parentDirPath - } - return -} - -// startEventLoop is the core event handler. -func (w *Watcher) startEventLoop() { +// eventLoop is the core event handler. +func (w *Watcher) eventLoop() { go func() { for { if v := w.events.Pop(); v != nil { @@ -171,3 +132,42 @@ func (w *Watcher) startEventLoop() { } }() } + +// getCallbacks searches and returns all callbacks with given . +// It also searches its parents for callbacks if they're recursive. +func (w *Watcher) getCallbacks(path string) (callbacks []*Callback) { + // Firstly add the callbacks of itself. + if v := w.callbacks.Get(path); v != nil { + for _, v := range v.(*glist.List).FrontAll() { + callback := v.(*Callback) + callbacks = append(callbacks, callback) + } + } + // Secondly searches its direct parent for callbacks. + // It is special handling here, which is the different between `recursive` and `not recursive` logic + // for direct parent folder of `path` that events are from. + dirPath := fileDir(path) + if v := w.callbacks.Get(dirPath); v != nil { + for _, v := range v.(*glist.List).FrontAll() { + callback := v.(*Callback) + callbacks = append(callbacks, callback) + } + } + // Lastly searches all the parents of directory of `path` recursively for callbacks. + for { + parentDirPath := fileDir(dirPath) + if parentDirPath == dirPath { + break + } + if v := w.callbacks.Get(parentDirPath); v != nil { + for _, v := range v.(*glist.List).FrontAll() { + callback := v.(*Callback) + if callback.recursive { + callbacks = append(callbacks, callback) + } + } + } + dirPath = parentDirPath + } + return +} diff --git a/os/gfsnotify/gfsnotify_z_unit_test.go b/os/gfsnotify/gfsnotify_z_unit_test.go index a1f195dc1..e1e5b1f95 100644 --- a/os/gfsnotify/gfsnotify_z_unit_test.go +++ b/os/gfsnotify/gfsnotify_z_unit_test.go @@ -7,6 +7,7 @@ package gfsnotify_test import ( + "github.com/gogf/gf/container/garray" "testing" "time" @@ -191,3 +192,29 @@ func TestWatcher_Callback2(t *testing.T) { t.Assert(v2.Val(), 2) }) } + +func TestWatcher_WatchFolderWithoutRecursively(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + var ( + err error + array = garray.New(true) + dirPath = gfile.TempDir(gtime.TimestampNanoStr()) + ) + err = gfile.Mkdir(dirPath) + t.AssertNil(err) + + _, err = gfsnotify.Add(dirPath, func(event *gfsnotify.Event) { + //fmt.Println(event.String()) + array.Append(1) + }, false) + t.AssertNil(err) + time.Sleep(time.Millisecond * 100) + t.Assert(array.Len(), 0) + + f, err := gfile.Create(gfile.Join(dirPath, "1")) + t.AssertNil(err) + t.AssertNil(f.Close()) + time.Sleep(time.Millisecond * 100) + t.Assert(array.Len(), 1) + }) +} diff --git a/os/glog/glog.go b/os/glog/glog.go index 089f853f7..a465a5b43 100644 --- a/os/glog/glog.go +++ b/os/glog/glog.go @@ -30,7 +30,7 @@ func init() { SetDebug(defaultDebug) } -// Default returns the default logger. +// DefaultLogger returns the default logger. func DefaultLogger() *Logger { return logger } diff --git a/os/glog/glog_api.go b/os/glog/glog_api.go index 830d6f64f..243a7b993 100644 --- a/os/glog/glog_api.go +++ b/os/glog/glog_api.go @@ -18,6 +18,7 @@ func Printf(format string, v ...interface{}) { logger.Printf(format, v...) } +// Println is alias of Print. // See Print. func Println(v ...interface{}) { logger.Println(v...) diff --git a/os/glog/glog_config.go b/os/glog/glog_config.go index c38454495..6e4402dee 100644 --- a/os/glog/glog_config.go +++ b/os/glog/glog_config.go @@ -148,3 +148,8 @@ func SetLevelPrefixes(prefixes map[int]string) { func GetLevelPrefix(level int) string { return logger.GetLevelPrefix(level) } + +// SetHandlers sets the logging handlers for default logger. +func SetHandlers(handlers ...Handler) { + logger.SetHandlers(handlers...) +} diff --git a/os/glog/glog_logger.go b/os/glog/glog_logger.go index 59d3d4f88..7f4dcbc91 100644 --- a/os/glog/glog_logger.go +++ b/os/glog/glog_logger.go @@ -62,6 +62,7 @@ func New() *Logger { init: gtype.NewBool(), config: DefaultConfig(), } + logger.config.Handlers = []Handler{defaultHandler} return logger } @@ -94,7 +95,7 @@ func (l *Logger) getFilePath(now time.Time) string { } // print prints to defined writer, logging file or passed . -func (l *Logger) print(std io.Writer, lead string, values ...interface{}) { +func (l *Logger) print(ctx context.Context, level int, values ...interface{}) { // Lazy initialize for rotation feature. // It uses atomic reading operation to enhance the performance checking. // It here uses CAP for performance and concurrent safety. @@ -111,69 +112,70 @@ func (l *Logger) print(std io.Writer, lead string, values ...interface{}) { } var ( - now = time.Now() - buffer = bytes.NewBuffer(nil) + now = time.Now() + input = &HandlerInput{ + logger: l, + index: -1, + Ctx: ctx, + Time: now, + Level: level, + } ) if l.config.HeaderPrint { // Time. timeFormat := "" if l.config.Flags&F_TIME_DATE > 0 { - timeFormat += "2006-01-02 " + timeFormat += "2006-01-02" } if l.config.Flags&F_TIME_TIME > 0 { - timeFormat += "15:04:05 " + if timeFormat != "" { + timeFormat += " " + } + timeFormat += "15:04:05" } if l.config.Flags&F_TIME_MILLI > 0 { - timeFormat += "15:04:05.000 " + if timeFormat != "" { + timeFormat += " " + } + timeFormat += "15:04:05.000" } if len(timeFormat) > 0 { - buffer.WriteString(now.Format(timeFormat)) - } - // Lead string. - if len(lead) > 0 { - buffer.WriteString(lead) - if len(values) > 0 { - buffer.WriteByte(' ') - } + input.TimeFormat = now.Format(timeFormat) } + + // Level string. + input.LevelFormat = l.getLevelPrefixWithBrackets(level) + // Caller path and Fn name. if l.config.Flags&(F_FILE_LONG|F_FILE_SHORT|F_CALLER_FN) > 0 { - callerPath := "" callerFnName, path, line := gdebug.CallerWithFilter(pathFilterKey, l.config.StSkip) if l.config.Flags&F_CALLER_FN > 0 { - buffer.WriteString(fmt.Sprintf(`[%s] `, callerFnName)) + input.CallerFunc = fmt.Sprintf(`[%s]`, callerFnName) } if l.config.Flags&F_FILE_LONG > 0 { - callerPath = fmt.Sprintf(`%s:%d: `, path, line) + input.CallerPath = fmt.Sprintf(`%s:%d:`, path, line) } if l.config.Flags&F_FILE_SHORT > 0 { - callerPath = fmt.Sprintf(`%s:%d: `, gfile.Basename(path), line) + input.CallerPath = fmt.Sprintf(`%s:%d:`, gfile.Basename(path), line) } - buffer.WriteString(callerPath) - } // Prefix. if len(l.config.Prefix) > 0 { - buffer.WriteString(l.config.Prefix + " ") + input.Prefix = l.config.Prefix } } // Convert value to string. - var ( - tempStr = "" - valueStr = "" - ) - - if l.ctx != nil { + if ctx != nil { // Tracing values. - spanCtx := trace.SpanContextFromContext(l.ctx) + spanCtx := trace.SpanContextFromContext(ctx) if traceId := spanCtx.TraceID(); traceId.IsValid() { - buffer.WriteString(fmt.Sprintf("{TraceID:%s} ", traceId.String())) + input.CtxStr = "{TraceID:" + traceId.String() + "}" } // Context values. if len(l.config.CtxKeys) > 0 { ctxStr := "" for _, key := range l.config.CtxKeys { - if v := l.ctx.Value(key); v != nil { + if v := ctx.Value(key); v != nil { if ctxStr != "" { ctxStr += ", " } @@ -181,51 +183,52 @@ func (l *Logger) print(std io.Writer, lead string, values ...interface{}) { } } if ctxStr != "" { - buffer.WriteString(fmt.Sprintf("{%s} ", ctxStr)) + input.CtxStr += "{" + ctxStr + "}" } } } - + var tempStr string for _, v := range values { tempStr = gconv.String(v) - if len(valueStr) > 0 { - if valueStr[len(valueStr)-1] == '\n' { + if len(input.Content) > 0 { + if input.Content[len(input.Content)-1] == '\n' { // Remove one blank line(\n\n). if tempStr[0] == '\n' { - valueStr += tempStr[1:] + input.Content += tempStr[1:] } else { - valueStr += tempStr + input.Content += tempStr } } else { - valueStr += " " + tempStr + input.Content += " " + tempStr } } else { - valueStr = tempStr + input.Content = tempStr } } - buffer.WriteString(valueStr + "\n") if l.config.Flags&F_ASYNC > 0 { + input.IsAsync = true err := asyncPool.Add(func() { - l.printToWriter(now, std, buffer) + input.Next() }) if err != nil { intlog.Error(err) } } else { - l.printToWriter(now, std, buffer) + input.Next() } } // printToWriter writes buffer to writer. -func (l *Logger) printToWriter(now time.Time, std io.Writer, buffer *bytes.Buffer) { +func (l *Logger) printToWriter(ctx context.Context, input *HandlerInput) { + buffer := input.Buffer() if l.config.Writer == nil { // Output content to disk file. if l.config.Path != "" { - l.printToFile(now, buffer) + l.printToFile(input.Time, buffer) } // Allow output to stdout? if l.config.StdoutPrint { - if _, err := std.Write(buffer.Bytes()); err != nil { + if _, err := os.Stdout.Write(buffer.Bytes()); err != nil { intlog.Error(err) } } @@ -238,9 +241,9 @@ func (l *Logger) printToWriter(now time.Time, std io.Writer, buffer *bytes.Buffe } // printToFile outputs logging content to disk file. -func (l *Logger) printToFile(now time.Time, buffer *bytes.Buffer) { +func (l *Logger) printToFile(t time.Time, buffer *bytes.Buffer) { var ( - logFilePath = l.getFilePath(now) + logFilePath = l.getFilePath(t) memoryLockKey = "glog.printToFile:" + logFilePath ) gmlock.Lock(memoryLockKey) @@ -249,7 +252,7 @@ func (l *Logger) printToFile(now time.Time, buffer *bytes.Buffer) { // Rotation file size checks. if l.config.RotateSize > 0 { if gfile.Size(logFilePath) > l.config.RotateSize { - l.rotateFileBySize(now) + l.rotateFileBySize(t) } } // Logging content outputting to disk file. @@ -280,20 +283,29 @@ func (l *Logger) getFilePointer(path string) *gfpool.File { return file } +// getCtx returns the context which is set through chaining operations. +// It returns an empty context if no context set previously. +func (l *Logger) getCtx() context.Context { + if l.ctx != nil { + return l.ctx + } + return context.TODO() +} + // printStd prints content without stack. -func (l *Logger) printStd(lead string, value ...interface{}) { - l.print(os.Stdout, lead, value...) +func (l *Logger) printStd(level int, value ...interface{}) { + l.print(l.getCtx(), level, value...) } // printStd prints content with stack check. -func (l *Logger) printErr(lead string, value ...interface{}) { +func (l *Logger) printErr(level int, value ...interface{}) { if l.config.StStatus == 1 { if s := l.GetStack(); s != "" { value = append(value, "\nStack:\n"+s) } } // In matter of sequence, do not use stderr here, but use the same stdout. - l.print(os.Stdout, lead, value...) + l.print(l.getCtx(), level, value...) } // format formats using fmt.Sprintf. diff --git a/os/glog/glog_logger_api.go b/os/glog/glog_logger_api.go index f7769630d..321da0752 100644 --- a/os/glog/glog_logger_api.go +++ b/os/glog/glog_logger_api.go @@ -14,13 +14,13 @@ import ( // Print prints with newline using fmt.Sprintln. // The parameter can be multiple variables. func (l *Logger) Print(v ...interface{}) { - l.printStd("", v...) + l.printStd(LEVEL_NONE, v...) } // Printf prints with format using fmt.Sprintf. // The parameter can be multiple variables. func (l *Logger) Printf(format string, v ...interface{}) { - l.printStd("", l.format(format, v...)) + l.printStd(LEVEL_NONE, l.format(format, v...)) } // Println is alias of Print. @@ -31,53 +31,53 @@ func (l *Logger) Println(v ...interface{}) { // Fatal prints the logging content with [FATA] header and newline, then exit the current process. func (l *Logger) Fatal(v ...interface{}) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_FATA), v...) + l.printErr(LEVEL_FATA, v...) os.Exit(1) } // Fatalf prints the logging content with [FATA] header, custom format and newline, then exit the current process. func (l *Logger) Fatalf(format string, v ...interface{}) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_FATA), l.format(format, v...)) + l.printErr(LEVEL_FATA, l.format(format, v...)) os.Exit(1) } // Panic prints the logging content with [PANI] header and newline, then panics. func (l *Logger) Panic(v ...interface{}) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_PANI), v...) + l.printErr(LEVEL_PANI, v...) panic(fmt.Sprint(v...)) } // Panicf prints the logging content with [PANI] header, custom format and newline, then panics. func (l *Logger) Panicf(format string, v ...interface{}) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_PANI), l.format(format, v...)) + l.printErr(LEVEL_PANI, l.format(format, v...)) panic(l.format(format, v...)) } // Info prints the logging content with [INFO] header and newline. func (l *Logger) Info(v ...interface{}) { if l.checkLevel(LEVEL_INFO) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_INFO), v...) + l.printStd(LEVEL_INFO, v...) } } // Infof prints the logging content with [INFO] header, custom format and newline. func (l *Logger) Infof(format string, v ...interface{}) { if l.checkLevel(LEVEL_INFO) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_INFO), l.format(format, v...)) + l.printStd(LEVEL_INFO, l.format(format, v...)) } } // Debug prints the logging content with [DEBU] header and newline. func (l *Logger) Debug(v ...interface{}) { if l.checkLevel(LEVEL_DEBU) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_DEBU), v...) + l.printStd(LEVEL_DEBU, v...) } } // Debugf prints the logging content with [DEBU] header, custom format and newline. func (l *Logger) Debugf(format string, v ...interface{}) { if l.checkLevel(LEVEL_DEBU) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_DEBU), l.format(format, v...)) + l.printStd(LEVEL_DEBU, l.format(format, v...)) } } @@ -85,7 +85,7 @@ func (l *Logger) Debugf(format string, v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Notice(v ...interface{}) { if l.checkLevel(LEVEL_NOTI) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_NOTI), v...) + l.printStd(LEVEL_NOTI, v...) } } @@ -93,7 +93,7 @@ func (l *Logger) Notice(v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Noticef(format string, v ...interface{}) { if l.checkLevel(LEVEL_NOTI) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_NOTI), l.format(format, v...)) + l.printStd(LEVEL_NOTI, l.format(format, v...)) } } @@ -101,7 +101,7 @@ func (l *Logger) Noticef(format string, v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Warning(v ...interface{}) { if l.checkLevel(LEVEL_WARN) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_WARN), v...) + l.printStd(LEVEL_WARN, v...) } } @@ -109,7 +109,7 @@ func (l *Logger) Warning(v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Warningf(format string, v ...interface{}) { if l.checkLevel(LEVEL_WARN) { - l.printStd(l.getLevelPrefixWithBrackets(LEVEL_WARN), l.format(format, v...)) + l.printStd(LEVEL_WARN, l.format(format, v...)) } } @@ -117,7 +117,7 @@ func (l *Logger) Warningf(format string, v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Error(v ...interface{}) { if l.checkLevel(LEVEL_ERRO) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_ERRO), v...) + l.printErr(LEVEL_ERRO, v...) } } @@ -125,7 +125,7 @@ func (l *Logger) Error(v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Errorf(format string, v ...interface{}) { if l.checkLevel(LEVEL_ERRO) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_ERRO), l.format(format, v...)) + l.printErr(LEVEL_ERRO, l.format(format, v...)) } } @@ -133,7 +133,7 @@ func (l *Logger) Errorf(format string, v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Critical(v ...interface{}) { if l.checkLevel(LEVEL_CRIT) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_CRIT), v...) + l.printErr(LEVEL_CRIT, v...) } } @@ -141,7 +141,7 @@ func (l *Logger) Critical(v ...interface{}) { // It also prints caller stack info if stack feature is enabled. func (l *Logger) Criticalf(format string, v ...interface{}) { if l.checkLevel(LEVEL_CRIT) { - l.printErr(l.getLevelPrefixWithBrackets(LEVEL_CRIT), l.format(format, v...)) + l.printErr(LEVEL_CRIT, l.format(format, v...)) } } diff --git a/os/glog/glog_logger_config.go b/os/glog/glog_logger_config.go index dd619d20e..6b00a6afd 100644 --- a/os/glog/glog_logger_config.go +++ b/os/glog/glog_logger_config.go @@ -9,18 +9,20 @@ package glog import ( "errors" "fmt" + "io" + "strings" + "time" + "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/internal/intlog" "github.com/gogf/gf/os/gfile" "github.com/gogf/gf/util/gconv" "github.com/gogf/gf/util/gutil" - "io" - "strings" - "time" ) // Config is the configuration object for logger. type Config struct { + Handlers []Handler `json:"-"` // Logger handlers which implement feature similar as middleware. Writer io.Writer `json:"-"` // Customized io.Writer. Flags int `json:"flags"` // Extra flags for logging output features. Path string `json:"path"` // Logging directory path. @@ -165,6 +167,24 @@ func (l *Logger) SetCtxKeys(keys ...interface{}) { l.config.CtxKeys = keys } +// AppendCtxKeys appends extra keys to logger. +// It ignores the key if it is already appended to the logger previously. +func (l *Logger) AppendCtxKeys(keys ...interface{}) { + var isExist bool + for _, key := range keys { + isExist = false + for _, ctxKey := range l.config.CtxKeys { + if ctxKey == key { + isExist = true + break + } + } + if !isExist { + l.config.CtxKeys = append(l.config.CtxKeys, key) + } + } +} + // GetCtxKeys retrieves and returns the context keys for logging. func (l *Logger) GetCtxKeys() []interface{} { return l.config.CtxKeys @@ -227,3 +247,8 @@ func (l *Logger) SetHeaderPrint(enabled bool) { func (l *Logger) SetPrefix(prefix string) { l.config.Prefix = prefix } + +// SetHandlers sets the logging handlers for current logger. +func (l *Logger) SetHandlers(handlers ...Handler) { + l.config.Handlers = append(handlers, defaultHandler) +} diff --git a/os/glog/glog_logger_handler.go b/os/glog/glog_logger_handler.go new file mode 100644 index 000000000..69a703992 --- /dev/null +++ b/os/glog/glog_logger_handler.go @@ -0,0 +1,79 @@ +// 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 glog + +import ( + "bytes" + "context" + "time" +) + +type Handler func(ctx context.Context, input *HandlerInput) + +type HandlerInput struct { + logger *Logger + index int + Ctx context.Context + Time time.Time + TimeFormat string + Level int + LevelFormat string + CallerFunc string + CallerPath string + CtxStr string + Prefix string + Content string + IsAsync bool +} + +// defaultHandler is the default handler for logger. +func defaultHandler(ctx context.Context, input *HandlerInput) { + input.logger.printToWriter(ctx, input) +} + +func (i *HandlerInput) addStringToBuffer(buffer *bytes.Buffer, s string) { + if buffer.Len() > 0 { + buffer.WriteByte(' ') + } + buffer.WriteString(s) +} + +func (i *HandlerInput) Buffer() *bytes.Buffer { + buffer := bytes.NewBuffer(nil) + buffer.WriteString(i.TimeFormat) + if i.LevelFormat != "" { + i.addStringToBuffer(buffer, i.LevelFormat) + } + if i.CallerFunc != "" { + i.addStringToBuffer(buffer, i.CallerFunc) + } + if i.CallerPath != "" { + i.addStringToBuffer(buffer, i.CallerPath) + } + if i.Prefix != "" { + i.addStringToBuffer(buffer, i.Prefix) + } + if i.CtxStr != "" { + i.addStringToBuffer(buffer, i.CtxStr) + } + if i.Content != "" { + i.addStringToBuffer(buffer, i.Content) + } + i.addStringToBuffer(buffer, "\n") + return buffer +} + +func (i *HandlerInput) String() string { + return i.Buffer().String() +} + +func (i *HandlerInput) Next() { + if len(i.logger.config.Handlers)-1 > i.index { + i.index++ + i.logger.config.Handlers[i.index](i.Ctx, i) + } +} diff --git a/os/glog/glog_logger_level.go b/os/glog/glog_logger_level.go index f9b5c28ed..a756bc6f8 100644 --- a/os/glog/glog_logger_level.go +++ b/os/glog/glog_logger_level.go @@ -18,6 +18,7 @@ const ( LEVEL_ALL = LEVEL_DEBU | LEVEL_INFO | LEVEL_NOTI | LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT LEVEL_DEV = LEVEL_ALL LEVEL_PROD = LEVEL_WARN | LEVEL_ERRO | LEVEL_CRIT + LEVEL_NONE = 0 LEVEL_DEBU = 1 << iota // 8 LEVEL_INFO // 16 LEVEL_NOTI // 32 diff --git a/os/glog/glog_z_unit_handler_test.go b/os/glog/glog_z_unit_handler_test.go new file mode 100644 index 000000000..70acc0f5c --- /dev/null +++ b/os/glog/glog_z_unit_handler_test.go @@ -0,0 +1,80 @@ +// 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 glog_test + +import ( + "bytes" + "context" + "github.com/gogf/gf/container/garray" + "github.com/gogf/gf/os/glog" + "github.com/gogf/gf/test/gtest" + "github.com/gogf/gf/text/gstr" + "testing" +) + +var arrayForHandlerTest1 = garray.NewStrArray() + +func customHandler1(ctx context.Context, input *glog.HandlerInput) { + arrayForHandlerTest1.Append(input.String()) + input.Next() +} + +func TestLogger_SetHandlers1(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + w := bytes.NewBuffer(nil) + l := glog.NewWithWriter(w) + l.SetHandlers(customHandler1) + l.SetCtxKeys("Trace-Id", "Span-Id", "Test") + ctx := context.WithValue(context.Background(), "Trace-Id", "1234567890") + ctx = context.WithValue(ctx, "Span-Id", "abcdefg") + + l.Ctx(ctx).Print(1, 2, 3) + t.Assert(gstr.Count(w.String(), "Trace-Id"), 1) + t.Assert(gstr.Count(w.String(), "1234567890"), 1) + t.Assert(gstr.Count(w.String(), "Span-Id"), 1) + t.Assert(gstr.Count(w.String(), "abcdefg"), 1) + t.Assert(gstr.Count(w.String(), "1 2 3"), 1) + + t.Assert(arrayForHandlerTest1.Len(), 1) + t.Assert(gstr.Count(arrayForHandlerTest1.At(0), "Trace-Id"), 1) + t.Assert(gstr.Count(arrayForHandlerTest1.At(0), "1234567890"), 1) + t.Assert(gstr.Count(arrayForHandlerTest1.At(0), "Span-Id"), 1) + t.Assert(gstr.Count(arrayForHandlerTest1.At(0), "abcdefg"), 1) + t.Assert(gstr.Count(arrayForHandlerTest1.At(0), "1 2 3"), 1) + }) +} + +var arrayForHandlerTest2 = garray.NewStrArray() + +func customHandler2(ctx context.Context, input *glog.HandlerInput) { + arrayForHandlerTest2.Append(input.String()) +} + +func TestLogger_SetHandlers2(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + w := bytes.NewBuffer(nil) + l := glog.NewWithWriter(w) + l.SetHandlers(customHandler2) + l.SetCtxKeys("Trace-Id", "Span-Id", "Test") + ctx := context.WithValue(context.Background(), "Trace-Id", "1234567890") + ctx = context.WithValue(ctx, "Span-Id", "abcdefg") + + l.Ctx(ctx).Print(1, 2, 3) + t.Assert(gstr.Count(w.String(), "Trace-Id"), 0) + t.Assert(gstr.Count(w.String(), "1234567890"), 0) + t.Assert(gstr.Count(w.String(), "Span-Id"), 0) + t.Assert(gstr.Count(w.String(), "abcdefg"), 0) + t.Assert(gstr.Count(w.String(), "1 2 3"), 0) + + t.Assert(arrayForHandlerTest2.Len(), 1) + t.Assert(gstr.Count(arrayForHandlerTest2.At(0), "Trace-Id"), 1) + t.Assert(gstr.Count(arrayForHandlerTest2.At(0), "1234567890"), 1) + t.Assert(gstr.Count(arrayForHandlerTest2.At(0), "Span-Id"), 1) + t.Assert(gstr.Count(arrayForHandlerTest2.At(0), "abcdefg"), 1) + t.Assert(gstr.Count(arrayForHandlerTest2.At(0), "1 2 3"), 1) + }) +} diff --git a/os/gproc/gproc_comm_receive.go b/os/gproc/gproc_comm_receive.go index b0bd9f39a..18a815553 100644 --- a/os/gproc/gproc_comm_receive.go +++ b/os/gproc/gproc_comm_receive.go @@ -89,7 +89,7 @@ func receiveTcpHandler(conn *gtcp.Conn) { if len(buffer) > 0 { // Package decoding. msg := new(MsgRequest) - if err := json.Unmarshal(buffer, msg); err != nil { + if err := json.UnmarshalUseNumber(buffer, msg); err != nil { //glog.Error(err) continue } diff --git a/os/gproc/gproc_comm_send.go b/os/gproc/gproc_comm_send.go index 25af7c453..b53c5132e 100644 --- a/os/gproc/gproc_comm_send.go +++ b/os/gproc/gproc_comm_send.go @@ -43,7 +43,7 @@ func Send(pid int, data []byte, group ...string) error { }) if len(result) > 0 { response := new(MsgResponse) - err = json.Unmarshal(result, response) + err = json.UnmarshalUseNumber(result, response) if err == nil { if response.Code != 1 { err = errors.New(response.Message) diff --git a/os/gproc/gproc_manager.go b/os/gproc/gproc_manager.go index 8f3cea114..cc448eb65 100644 --- a/os/gproc/gproc_manager.go +++ b/os/gproc/gproc_manager.go @@ -12,26 +12,27 @@ import ( "github.com/gogf/gf/container/gmap" ) -// 进程管理器 +// Manager is a process manager maintaining multiple processes. type Manager struct { - processes *gmap.IntAnyMap // 所管理的子进程map + processes *gmap.IntAnyMap // Process id to Process object mapping. } -// 创建一个进程管理器 +// NewManager creates and returns a new process manager. func NewManager() *Manager { return &Manager{ processes: gmap.NewIntAnyMap(true), } } -// 创建一个进程(不执行) +// NewProcess creates and returns a Process object. func (m *Manager) NewProcess(path string, args []string, environment []string) *Process { p := NewProcess(path, args, environment) p.Manager = m return p } -// 获取当前进程管理器中的一个进程 +// GetProcess retrieves and returns a Process object. +// It returns nil if it does not find the process with given `pid`. func (m *Manager) GetProcess(pid int) *Process { if v := m.processes.Get(pid); v != nil { return v.(*Process) @@ -39,7 +40,8 @@ func (m *Manager) GetProcess(pid int) *Process { return nil } -// 添加一个已存在进程到进程管理器中 +// AddProcess adds a process to current manager. +// It does nothing if the process with given `pid` does not exist. func (m *Manager) AddProcess(pid int) { if m.processes.Get(pid) == nil { if process, err := os.FindProcess(pid); err == nil { @@ -50,12 +52,12 @@ func (m *Manager) AddProcess(pid int) { } } -// 移除进程管理器中的指定进程 +// RemoveProcess removes a process from current manager. func (m *Manager) RemoveProcess(pid int) { m.processes.Remove(pid) } -// 获取所有的进程对象,构成列表返回 +// Processes retrieves and returns all processes in current manager. func (m *Manager) Processes() []*Process { processes := make([]*Process, 0) m.processes.RLockFunc(func(m map[int]interface{}) { @@ -66,12 +68,12 @@ func (m *Manager) Processes() []*Process { return processes } -// 获取所有的进程pid,构成列表返回 +// Pids retrieves and returns all process id array in current manager. func (m *Manager) Pids() []int { return m.processes.Keys() } -// 等待所有子进程结束 +// WaitAll waits until all process exit. func (m *Manager) WaitAll() { processes := m.Processes() if len(processes) > 0 { @@ -81,7 +83,7 @@ func (m *Manager) WaitAll() { } } -// 关闭所有的进程 +// KillAll kills all processes in current manager. func (m *Manager) KillAll() error { for _, p := range m.Processes() { if err := p.Kill(); err != nil { @@ -91,7 +93,7 @@ func (m *Manager) KillAll() error { return nil } -// 向所有进程发送信号量 +// SignalAll sends a signal `sig` to all processes in current manager. func (m *Manager) SignalAll(sig os.Signal) error { for _, p := range m.Processes() { if err := p.Signal(sig); err != nil { @@ -101,24 +103,24 @@ func (m *Manager) SignalAll(sig os.Signal) error { return nil } -// 向所有进程发送消息 +// Send sends data bytes to all processes in current manager. func (m *Manager) Send(data []byte) { for _, p := range m.Processes() { p.Send(data) } } -// 向指定进程发送消息 +// SendTo sneds data bytes to specified processe in current manager. func (m *Manager) SendTo(pid int, data []byte) error { return Send(pid, data) } -// 清空管理器 +// Clear removes all processes in current manager. func (m *Manager) Clear() { m.processes.Clear() } -// 当前进程总数 +// Size returns the size of processes in current manager. func (m *Manager) Size() int { return m.processes.Size() } diff --git a/os/gproc/gproc_signal.go b/os/gproc/gproc_signal.go index 40ad5464b..08d059e00 100644 --- a/os/gproc/gproc_signal.go +++ b/os/gproc/gproc_signal.go @@ -47,13 +47,15 @@ func AddSigHandler(handler SigHandler, signals ...os.Signal) { // syscall.SIGKILL, // syscall.SIGTERM, // syscall.SIGABRT. -func AddSigHandlerShutdown(handler SigHandler) { - for sig, _ := range shutdownSignalMap { - signalHandlerMap[sig] = append(signalHandlerMap[sig], handler) +func AddSigHandlerShutdown(handler ...SigHandler) { + for _, h := range handler { + for sig, _ := range shutdownSignalMap { + signalHandlerMap[sig] = append(signalHandlerMap[sig], h) + } } } -// ListenSignal blocks and does signal listening and handling. +// Listen blocks and does signal listening and handling. func Listen() { signals := make([]os.Signal, 0) for sig, _ := range signalHandlerMap { diff --git a/os/gres/testdata/data/data.go b/os/gres/testdata/data/data.go index 099d514e3..ff96670de 100644 --- a/os/gres/testdata/data/data.go +++ b/os/gres/testdata/data/data.go @@ -3,7 +3,7 @@ package data import "github.com/gogf/gf/os/gres" func init() { - if err := gres.Add("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"); err != nil { + if err := gres.Add("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"); err != nil { panic("add binary content to resource manager failed: " + err.Error()) } } diff --git a/os/gres/testdata/testdata.go b/os/gres/testdata/testdata.go index b228bde1c..37c21f2a6 100644 --- a/os/gres/testdata/testdata.go +++ b/os/gres/testdata/testdata.go @@ -3,7 +3,7 @@ package testdata import "github.com/gogf/gf/os/gres" func init() { - if err := gres.Add("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"); err != nil { + if err := gres.Add("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"); err != nil { panic("add binary content to resource manager failed: " + err.Error()) } } diff --git a/os/gsession/gsession_manager.go b/os/gsession/gsession_manager.go index dc83ae50d..c52095146 100644 --- a/os/gsession/gsession_manager.go +++ b/os/gsession/gsession_manager.go @@ -15,9 +15,13 @@ import ( // Manager for sessions. type Manager struct { - ttl time.Duration // TTL for sessions. - storage Storage // Storage interface for session storage. - sessionData *gcache.Cache // Session data cache for session TTL. + ttl time.Duration // TTL for sessions. + storage Storage // Storage interface for session storage. + + // sessionData is the memory data cache for session TTL, + // which is available only if the Storage does not stores any session data in synchronizing. + // Please refer to the implements of StorageFile, StorageMemory and StorageRedis. + sessionData *gcache.Cache } // New creates and returns a new session manager. diff --git a/os/gsession/gsession_storage_file.go b/os/gsession/gsession_storage_file.go index 51936a499..fb0bc7ca6 100644 --- a/os/gsession/gsession_storage_file.go +++ b/os/gsession/gsession_storage_file.go @@ -175,7 +175,7 @@ func (s *StorageFile) GetSession(id string, ttl time.Duration, data *gmap.StrAny } } var m map[string]interface{} - if err = json.Unmarshal(content, &m); err != nil { + if err = json.UnmarshalUseNumber(content, &m); err != nil { return nil, err } if m == nil { diff --git a/os/gsession/gsession_storage_redis.go b/os/gsession/gsession_storage_redis.go index bd5714e66..8d26e2db6 100644 --- a/os/gsession/gsession_storage_redis.go +++ b/os/gsession/gsession_storage_redis.go @@ -126,7 +126,7 @@ func (s *StorageRedis) GetSession(id string, ttl time.Duration, data *gmap.StrAn return nil, nil } var m map[string]interface{} - if err = json.Unmarshal(content, &m); err != nil { + if err = json.UnmarshalUseNumber(content, &m); err != nil { return nil, err } if m == nil { diff --git a/os/gtime/gtime_sql.go b/os/gtime/gtime_sql.go new file mode 100644 index 000000000..e10822c43 --- /dev/null +++ b/os/gtime/gtime_sql.go @@ -0,0 +1,28 @@ +package gtime + +import ( + "database/sql/driver" +) + +// Scanner is an interface used by Scan in package database/sql for Scanning value +// from database to local golang variable. +func (t *Time) Scan(value interface{}) error { + if t == nil { + return nil + } + newTime := New(value) + *t = *newTime + return nil +} + +// Value is the interface providing the Value method for package database/sql/driver +// for retrieving value from golang variable to database. +func (t *Time) Value() (driver.Value, error) { + if t == nil { + return nil, nil + } + if t.IsZero() { + return nil, nil + } + return t.Time, nil +} diff --git a/os/gtime/gtime_time.go b/os/gtime/gtime_time.go index 871013ed4..d3a02594a 100644 --- a/os/gtime/gtime_time.go +++ b/os/gtime/gtime_time.go @@ -464,3 +464,8 @@ func (t *Time) UnmarshalText(data []byte) error { } return gerror.Newf(`invalid time value: %s`, data) } + +// NoValidation marks this struct object will not be validated by package gvalid. +func (t *Time) NoValidation() { + +} diff --git a/os/gtime/gtime_z_unit_json_test.go b/os/gtime/gtime_z_unit_json_test.go index b79fd3230..781da86d6 100644 --- a/os/gtime/gtime_z_unit_json_test.go +++ b/os/gtime/gtime_z_unit_json_test.go @@ -50,7 +50,7 @@ func Test_Json_Pointer(t *testing.T) { gtest.C(t, func(t *gtest.T) { var t1 gtime.Time s := []byte(`"2006-01-02 15:04:05"`) - err := json.Unmarshal(s, &t1) + err := json.UnmarshalUseNumber(s, &t1) t.Assert(err, nil) t.Assert(t1.String(), "2006-01-02 15:04:05") }) diff --git a/os/gtime/gtime_z_unit_sql_test.go b/os/gtime/gtime_z_unit_sql_test.go new file mode 100644 index 000000000..fa40e71fd --- /dev/null +++ b/os/gtime/gtime_z_unit_sql_test.go @@ -0,0 +1,41 @@ +package gtime_test + +import ( + "github.com/gogf/gf/os/gtime" + "github.com/gogf/gf/test/gtest" + "testing" +) + +func TestTime_Scan(t1 *testing.T) { + gtest.C(t1, func(t *gtest.T) { + tt := gtime.Time{} + //test string + s := gtime.Now().String() + t.Assert(tt.Scan(s), nil) + t.Assert(tt.String(), s) + //test nano + n := gtime.TimestampNano() + t.Assert(tt.Scan(n), nil) + t.Assert(tt.TimestampNano(), n) + //test nil + none := (*gtime.Time)(nil) + t.Assert(none.Scan(nil), nil) + t.Assert(none, nil) + }) + +} + +func TestTime_Value(t1 *testing.T) { + gtest.C(t1, func(t *gtest.T) { + tt := gtime.Now() + s, err := tt.Value() + t.Assert(err, nil) + t.Assert(s, tt.Time) + //test nil + none := (*gtime.Time)(nil) + s, err = none.Value() + t.Assert(err, nil) + t.Assert(s, nil) + + }) +} diff --git a/os/gtimer/gtimer.go b/os/gtimer/gtimer.go index 80840682c..3ffd36326 100644 --- a/os/gtimer/gtimer.go +++ b/os/gtimer/gtimer.go @@ -4,8 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Package gtimer implements Hierarchical Timing Wheel for interval/delayed jobs -// running and management. +// Package gtimer implements timer for interval/delayed jobs running and management. // // This package is designed for management for millions of timing jobs. The differences // between gtimer and gcron are as follows: @@ -21,33 +20,53 @@ package gtimer import ( "fmt" + "github.com/gogf/gf/container/gtype" "math" + "sync" "time" "github.com/gogf/gf/os/gcmd" ) +// Timer is the timer manager, which uses ticks to calculate the timing interval. +type Timer struct { + mu sync.RWMutex + queue *priorityQueue // queue is a priority queue based on heap structure. + status *gtype.Int // status is the current timer status. + ticks *gtype.Int64 // ticks is the proceeded interval number by the timer. + options TimerOptions // timer options is used for timer configuration. +} + +// TimerOptions is the configuration object for Timer. +type TimerOptions struct { + Interval time.Duration // Interval is the interval escaped of the timer. +} + const ( - StatusReady = 0 // Job is ready for running. - StatusRunning = 1 // Job is already running. - StatusStopped = 2 // Job is stopped. - StatusReset = 3 // Job is reset. - StatusClosed = -1 // Job is closed and waiting to be deleted. - panicExit = "exit" // Internal usage for custom job exit function with panic. - defaultTimes = math.MaxInt32 // Default limit running times, a big number. - defaultSlotNumber = 10 // Default slot number. - defaultWheelInterval = 60 // Default wheel interval, for better manually reading. - defaultWheelLevel = 5 // Default wheel level. + StatusReady = 0 // Job or Timer is ready for running. + StatusRunning = 1 // Job or Timer is already running. + StatusStopped = 2 // Job or Timer is stopped. + StatusClosed = -1 // Job or Timer is closed and waiting to be deleted. + panicExit = "exit" // panicExit is used for custom job exit with panic. + defaultTimes = math.MaxInt32 // defaultTimes is the default limit running times, a big number. + defaultTimerInterval = 100 // defaultTimerInterval is the default timer interval in milliseconds. cmdEnvKey = "gf.gtimer" // Configuration key for command argument or environment. ) var ( - defaultSlots = gcmd.GetOptWithEnv(fmt.Sprintf("%s.slots", cmdEnvKey), defaultSlotNumber).Int() - defaultLevel = gcmd.GetOptWithEnv(fmt.Sprintf("%s.level", cmdEnvKey), defaultWheelLevel).Int() - defaultInterval = gcmd.GetOptWithEnv(fmt.Sprintf("%s.interval", cmdEnvKey), defaultWheelInterval).Duration() * time.Millisecond - defaultTimer = New(defaultSlots, defaultInterval, defaultLevel) + defaultTimer = New() + defaultInterval = gcmd.GetOptWithEnv( + fmt.Sprintf("%s.interval", cmdEnvKey), defaultTimerInterval, + ).Duration() * time.Millisecond ) +// DefaultOptions creates and returns a default options object for Timer creation. +func DefaultOptions() TimerOptions { + return TimerOptions{ + Interval: defaultInterval, + } +} + // SetTimeout runs the job once after duration of . // It is like the one in javascript. func SetTimeout(delay time.Duration, job JobFunc) { diff --git a/os/gtimer/gtimer_entry.go b/os/gtimer/gtimer_entry.go index f413646c4..beebf21a6 100644 --- a/os/gtimer/gtimer_entry.go +++ b/os/gtimer/gtimer_entry.go @@ -7,101 +7,87 @@ package gtimer import ( - "time" - "github.com/gogf/gf/container/gtype" + "math" ) -// Entry is the timing job entry to wheel. +// Entry is the timing job. type Entry struct { - name string - wheel *wheel // Belonged wheel. - job JobFunc // The job function. - singleton *gtype.Bool // Singleton mode. - status *gtype.Int // Job status. - times *gtype.Int // Limit running times. - intervalTicks int64 // The interval ticks of the job. - createTicks int64 // Timer ticks when the job installed. - createMs int64 // The timestamp in milliseconds when job installed. - intervalMs int64 // The interval milliseconds of the job. - installIntervalMs int64 // Interval when first installation in milliseconds. + job JobFunc // The job function. + timer *Timer // Belonged timer. + ticks int64 // The job runs every ticks. + times *gtype.Int // Limit running times. + status *gtype.Int // Job status. + singleton *gtype.Bool // Singleton mode. + nextTicks *gtype.Int64 // Next run ticks of the job. } // JobFunc is the job function. type JobFunc = func() -// addEntry adds a timing job to the wheel. -func (w *wheel) addEntry(interval time.Duration, job JobFunc, singleton bool, times int, status int) *Entry { - if times <= 0 { - times = defaultTimes - } - var ( - intervalMs = interval.Nanoseconds() / 1e6 - intervalTicks = intervalMs / w.intervalMs - ) - if intervalTicks == 0 { - // If the given interval is lesser than the one of the wheel, - // then sets it to one tick, which means it will be run in one interval. - intervalTicks = 1 - } - var ( - nowMs = time.Now().UnixNano() / 1e6 - nowTicks = w.ticks.Val() - entry = &Entry{ - wheel: w, - job: job, - times: gtype.NewInt(times), - status: gtype.NewInt(status), - createTicks: nowTicks, - intervalTicks: intervalTicks, - singleton: gtype.NewBool(singleton), - createMs: nowMs, - intervalMs: intervalMs, - installIntervalMs: intervalMs, - } - ) - // Install the job to the list of the slot. - w.slots[(nowTicks+intervalTicks)%w.number].PushBack(entry) - return entry -} - -// addEntryByParent adds a timing job with parent entry. -// The parameter `rollOn` specifies if just rolling on the entry, which was not met the runnable requirement -// and not executed previously. This is true often when the job internal is too long. -func (w *wheel) addEntryByParent(rollOn bool, nowMs, interval int64, parent *Entry) *Entry { - intervalTicks := interval / w.intervalMs - if intervalTicks == 0 { - intervalTicks = 1 - } - nowTicks := w.ticks.Val() - entry := &Entry{ - name: parent.name, - wheel: w, - job: parent.job, - times: parent.times, - status: parent.status, - intervalTicks: intervalTicks, - singleton: parent.singleton, - createTicks: nowTicks, - createMs: nowMs, - intervalMs: interval, - installIntervalMs: parent.installIntervalMs, - } - if rollOn { - entry.createMs = parent.createMs - if parent.wheel.level == w.level { - entry.createTicks = parent.createTicks - } - } - w.slots[(nowTicks+intervalTicks)%w.number].PushBack(entry) - return entry -} - // Status returns the status of the job. func (entry *Entry) Status() int { return entry.status.Val() } +// Run runs the timer job asynchronously. +func (entry *Entry) Run() { + leftRunningTimes := entry.times.Add(-1) + // Running times exceeding checks. + if leftRunningTimes < 0 { + entry.status.Set(StatusClosed) + return + } + // This means it does not limit the running times. + if leftRunningTimes == math.MaxInt32-1 { + entry.times.Set(math.MaxInt32) + } + go func() { + defer func() { + if err := recover(); err != nil { + if err != panicExit { + panic(err) + } else { + entry.Close() + return + } + } + if entry.Status() == StatusRunning { + entry.SetStatus(StatusReady) + } + }() + entry.job() + }() +} + +// doCheckAndRunByTicks checks the if job can run in given timer ticks, +// it runs asynchronously if the given `currentTimerTicks` meets or else +// it increments its ticks and waits for next running check. +func (entry *Entry) doCheckAndRunByTicks(currentTimerTicks int64) { + // Ticks check. + if currentTimerTicks < entry.nextTicks.Val() { + return + } + entry.nextTicks.Set(currentTimerTicks + entry.ticks) + // Perform job checking. + switch entry.status.Val() { + case StatusRunning: + if entry.IsSingleton() { + return + } + case StatusReady: + if !entry.status.Cas(StatusReady, StatusRunning) { + return + } + case StatusStopped: + return + case StatusClosed: + return + } + // Perform job running. + entry.Run() +} + // SetStatus custom sets the status for the job. func (entry *Entry) SetStatus(status int) int { return entry.status.Set(status) @@ -117,16 +103,16 @@ func (entry *Entry) Stop() { entry.status.Set(StatusStopped) } -//Reset reset the job. -func (entry *Entry) Reset() { - entry.status.Set(StatusReset) -} - // Close closes the job, and then it will be removed from the timer. func (entry *Entry) Close() { entry.status.Set(StatusClosed) } +// Reset reset the job, which resets its ticks for next running. +func (entry *Entry) Reset() { + entry.nextTicks.Set(entry.timer.ticks.Val() + entry.ticks) +} + // IsSingleton checks and returns whether the job in singleton mode. func (entry *Entry) IsSingleton() bool { return entry.singleton.Val() @@ -137,75 +123,12 @@ func (entry *Entry) SetSingleton(enabled bool) { entry.singleton.Set(enabled) } +// Job returns the job function of this job. +func (entry *Entry) Job() JobFunc { + return entry.job +} + // SetTimes sets the limit running times for the job. func (entry *Entry) SetTimes(times int) { entry.times.Set(times) } - -// Run runs the job. -func (entry *Entry) Run() { - entry.job() -} - -// check checks if the job should be run in given ticks and timestamp milliseconds. -func (entry *Entry) check(nowTicks int64, nowMs int64) (runnable, addable bool) { - switch entry.status.Val() { - case StatusStopped: - return false, true - case StatusClosed: - return false, false - case StatusReset: - return false, true - } - // Firstly checks using the ticks, this may be low precision as one tick is a little bit long. - //if entry.name == "1" { - // intlog.Print("check:", nowTicks-entry.createTicks, nowTicks, entry.createTicks, entry.intervalTicks) - //} - if diff := nowTicks - entry.createTicks; diff > 0 && diff%entry.intervalTicks == 0 { - // If not the lowest level wheel. - if entry.wheel.level > 0 { - diffMs := nowMs - entry.createMs - switch { - // Add it to the next slot, which means it will run on next interval. - case diffMs < entry.wheel.timer.intervalMs: - entry.wheel.slots[(nowTicks+entry.intervalTicks)%entry.wheel.number].PushBack(entry) - return false, false - - // Normal rolls on the job. - case diffMs >= entry.wheel.timer.intervalMs: - // Calculate the leftover milliseconds, - // if it is greater than the minimum interval, then re-install it. - if leftMs := entry.intervalMs - diffMs; leftMs > entry.wheel.timer.intervalMs { - // Re-calculate and re-installs the job proper slot. - entry.wheel.timer.doAddEntryByParent(false, nowMs, leftMs, entry) - return false, false - } - } - } - // Singleton mode check. - if entry.IsSingleton() { - // Note that it is atomic operation to ensure concurrent safety. - if entry.status.Set(StatusRunning) == StatusRunning { - return false, true - } - } - // Limit running times. - times := entry.times.Add(-1) - if times <= 0 { - // Note that it is atomic operation to ensure concurrent safety. - if entry.status.Set(StatusClosed) == StatusClosed || times < 0 { - return false, false - } - } - // This means it does not limit the running times. - // I know it's ugly, but it is surely high performance for running times limit. - if times < 2000000000 && times > 1000000000 { - entry.times.Set(defaultTimes) - } - //if entry.name == "1" { - // intlog.Print("runnable:", nowTicks-entry.createTicks, nowTicks, entry.createTicks, entry.createTicks, entry.interval) - //} - return true, true - } - return false, true -} diff --git a/os/gtimer/gtimer_loop.go b/os/gtimer/gtimer_loop.go deleted file mode 100644 index 725f6de46..000000000 --- a/os/gtimer/gtimer_loop.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. -// -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, -// You can obtain one at https://github.com/gogf/gf. - -package gtimer - -import ( - "time" - - "github.com/gogf/gf/container/glist" -) - -// start starts the ticker using a standalone goroutine. -func (w *wheel) start() { - go func() { - var ( - tickDuration = time.Duration(w.intervalMs) * time.Millisecond - ticker = time.NewTicker(tickDuration) - ) - for { - select { - case <-ticker.C: - switch w.timer.status.Val() { - case StatusRunning: - w.proceed() - - case StatusStopped: - // Do nothing. - - case StatusClosed: - ticker.Stop() - return - } - - } - } - }() -} - -// proceed checks and rolls on the job. -// If a timing job is time for running, it runs in an asynchronous goroutine, -// or else it removes from current slot and re-installs the job to another wheel and slot -// according to its leftover interval in milliseconds. -func (w *wheel) proceed() { - var ( - nowTicks = w.ticks.Add(1) - list = w.slots[int(nowTicks%w.number)] - length = list.Len() - nowMs = w.timer.nowFunc().UnixNano() / 1e6 - ) - if length > 0 { - go func(l *glist.List, nowTicks int64) { - var entry *Entry - for i := length; i > 0; i-- { - if v := l.PopFront(); v == nil { - break - } else { - entry = v.(*Entry) - } - // Checks whether the time for running. - runnable, addable := entry.check(nowTicks, nowMs) - if runnable { - // Just run it in another goroutine. - go func(entry *Entry) { - defer func() { - if err := recover(); err != nil { - if err != panicExit { - panic(err) - } else { - entry.Close() - } - } - if entry.Status() == StatusRunning { - entry.SetStatus(StatusReady) - } - }() - entry.job() - }(entry) - } - // Add job again, which make the job continuous running. - if addable { - // If StatusReset, reset to runnable state. - if entry.Status() == StatusReset { - entry.SetStatus(StatusReady) - } - entry.wheel.timer.doAddEntryByParent(!runnable, nowMs, entry.installIntervalMs, entry) - } - } - }(list, nowTicks) - } -} diff --git a/os/gtimer/gtimer_queue.go b/os/gtimer/gtimer_queue.go new file mode 100644 index 000000000..9fb6af73d --- /dev/null +++ b/os/gtimer/gtimer_queue.go @@ -0,0 +1,104 @@ +// 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 gtimer + +import ( + "container/heap" + "github.com/gogf/gf/container/gtype" + "math" + "sync" +) + +// priorityQueue is an abstract data type similar to a regular queue or stack data structure in which +// each element additionally has a "priority" associated with it. In a priority queue, an element with +// high priority is served before an element with low priority. +// priorityQueue is based on heap structure. +type priorityQueue struct { + mu sync.RWMutex + heap *priorityQueueHeap // the underlying queue items manager using heap. + latestPriority *gtype.Int64 // latestPriority stores the most priority value of the heap, which is used to check if necessary to call the Pop of heap by Timer. +} + +// priorityQueueHeap is a heap manager, of which the underlying `array` is a array implementing a heap structure. +type priorityQueueHeap struct { + array []priorityQueueItem +} + +// priorityQueueItem stores the queue item which has a `priority` attribute to sort itself in heap. +type priorityQueueItem struct { + value interface{} + priority int64 +} + +// newPriorityQueue creates and returns a priority queue. +func newPriorityQueue() *priorityQueue { + queue := &priorityQueue{ + heap: &priorityQueueHeap{ + array: make([]priorityQueueItem, 0), + }, + latestPriority: gtype.NewInt64(math.MaxInt64), + } + heap.Init(queue.heap) + return queue +} + +// Len retrieves and returns the length of the queue. +func (q *priorityQueue) Len() int { + q.mu.RLock() + defer q.mu.RUnlock() + return q.heap.Len() +} + +// LatestPriority retrieves and returns the minimum and the most priority value of the queue. +func (q *priorityQueue) LatestPriority() int64 { + return q.latestPriority.Val() +} + +// Push pushes a value to the queue. +// The `priority` specifies the priority of the value. +// The lesser the `priority` value the higher priority of the `value`. +func (q *priorityQueue) Push(value interface{}, priority int64) { + q.mu.Lock() + heap.Push(q.heap, priorityQueueItem{ + value: value, + priority: priority, + }) + q.mu.Unlock() + // Update the minimum priority using atomic operation. + for { + latestPriority := q.latestPriority.Val() + if priority >= latestPriority { + break + } + if q.latestPriority.Cas(latestPriority, priority) { + break + } + } +} + +// Pop retrieves, removes and returns the most high priority value from the queue. +func (q *priorityQueue) Pop() interface{} { + q.mu.Lock() + if v := heap.Pop(q.heap); v != nil { + item := v.(priorityQueueItem) + q.mu.Unlock() + // Update the minimum priority using atomic operation. + for { + latestPriority := q.latestPriority.Val() + if item.priority >= latestPriority { + break + } + if q.latestPriority.Cas(latestPriority, item.priority) { + break + } + } + return item.value + } else { + q.mu.Unlock() + } + return nil +} diff --git a/os/gtimer/gtimer_queue_heap.go b/os/gtimer/gtimer_queue_heap.go new file mode 100644 index 000000000..30879f770 --- /dev/null +++ b/os/gtimer/gtimer_queue_heap.go @@ -0,0 +1,41 @@ +// 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 gtimer + +// Len is used to implement the interface of sort.Interface. +func (h *priorityQueueHeap) Len() int { + return len(h.array) +} + +// Less is used to implement the interface of sort.Interface. +func (h *priorityQueueHeap) Less(i, j int) bool { + return h.array[i].priority < h.array[j].priority +} + +// Swap is used to implement the interface of sort.Interface. +func (h *priorityQueueHeap) Swap(i, j int) { + if len(h.array) == 0 { + return + } + h.array[i], h.array[j] = h.array[j], h.array[i] +} + +// Push pushes an item to the heap. +func (h *priorityQueueHeap) Push(x interface{}) { + h.array = append(h.array, x.(priorityQueueItem)) +} + +// Pop retrieves, removes and returns the most high priority item from the heap. +func (h *priorityQueueHeap) Pop() interface{} { + length := len(h.array) + if length == 0 { + return nil + } + item := h.array[length-1] + h.array = h.array[0 : length-1] + return item +} diff --git a/os/gtimer/gtimer_timer.go b/os/gtimer/gtimer_timer.go index 0280f0680..21bf19112 100644 --- a/os/gtimer/gtimer_timer.go +++ b/os/gtimer/gtimer_timer.go @@ -7,104 +7,28 @@ package gtimer import ( - "fmt" - "time" - - "github.com/gogf/gf/container/glist" "github.com/gogf/gf/container/gtype" + "time" ) -// Timer is a Hierarchical Timing Wheel manager for timing jobs. -type Timer struct { - status *gtype.Int // Timer status. - wheels []*wheel // The underlying wheels. - length int // Max level of the wheels. - number int // Slot Number of each wheel. - intervalMs int64 // Interval of the slot in milliseconds. - nowFunc func() time.Time // nowFunc returns the current time, which can be custom. -} - -// Wheel is a slot wrapper for timing job install and uninstall. -type wheel struct { - timer *Timer // Belonged timer. - level int // The level in the timer. - slots []*glist.List // Slot array. - number int64 // Slot Number=len(slots). - ticks *gtype.Int64 // Ticked count of the wheel, one tick is one of its interval passed. - totalMs int64 // Total duration in milliseconds=number*interval. - createMs int64 // Created timestamp in milliseconds. - intervalMs int64 // Interval in milliseconds, which is the duration of one slot. -} - -// New creates and returns a Hierarchical Timing Wheel designed timer. -// The parameter specifies the interval of the timer. -// The optional parameter specifies the wheels count of the timer, -// which is defaultWheelLevel in default. -func New(slot int, interval time.Duration, level ...int) *Timer { - t := doNewWithoutAutoStart(slot, interval, level...) - t.wheels[0].start() - return t -} - -func doNewWithoutAutoStart(slot int, interval time.Duration, level ...int) *Timer { - if slot <= 0 { - panic(fmt.Sprintf("invalid slot number: %d", slot)) - } - length := defaultWheelLevel - if len(level) > 0 { - length = level[0] - } +func New(options ...TimerOptions) *Timer { t := &Timer{ - status: gtype.NewInt(StatusRunning), - wheels: make([]*wheel, length), - length: length, - number: slot, - intervalMs: interval.Nanoseconds() / 1e6, - nowFunc: func() time.Time { - return time.Now() - }, + queue: newPriorityQueue(), + status: gtype.NewInt(StatusRunning), + ticks: gtype.NewInt64(), } - for i := 0; i < length; i++ { - if i > 0 { - n := time.Duration(t.wheels[i-1].totalMs) * time.Millisecond - if n <= 0 { - panic(fmt.Sprintf(`inteval is too large with level: %dms x %d`, interval, length)) - } - w := t.newWheel(i, slot, n) - t.wheels[i] = w - t.wheels[i-1].addEntry(n, w.proceed, false, defaultTimes, StatusReady) - if i == length-1 { - t.wheels[i].addEntry(n, w.proceed, false, defaultTimes, StatusReady) - } - } else { - w := t.newWheel(i, slot, interval) - t.wheels[i] = w - } + if len(options) > 0 { + t.options = options[0] + } else { + t.options = DefaultOptions() } + go t.loop() return t } -// newWheel creates and returns a single wheel. -func (t *Timer) newWheel(level int, slot int, interval time.Duration) *wheel { - w := &wheel{ - timer: t, - level: level, - slots: make([]*glist.List, slot), - number: int64(slot), - ticks: gtype.NewInt64(), - totalMs: int64(slot) * interval.Nanoseconds() / 1e6, - createMs: time.Now().UnixNano() / 1e6, - intervalMs: interval.Nanoseconds() / 1e6, - } - for i := int64(0); i < w.number; i++ { - w.slots[i] = glist.New(true) - } - return w -} - // Add adds a timing job to the timer, which runs in interval of . func (t *Timer) Add(interval time.Duration, job JobFunc) *Entry { - return t.doAddEntry(interval, job, false, defaultTimes, StatusReady) + return t.createEntry(interval, job, false, defaultTimes, StatusReady) } // AddEntry adds a timing job to the timer with detailed parameters. @@ -119,22 +43,22 @@ func (t *Timer) Add(interval time.Duration, job JobFunc) *Entry { // // The parameter specifies the job status when it's firstly added to the timer. func (t *Timer) AddEntry(interval time.Duration, job JobFunc, singleton bool, times int, status int) *Entry { - return t.doAddEntry(interval, job, singleton, times, status) + return t.createEntry(interval, job, singleton, times, status) } // AddSingleton is a convenience function for add singleton mode job. func (t *Timer) AddSingleton(interval time.Duration, job JobFunc) *Entry { - return t.doAddEntry(interval, job, true, defaultTimes, StatusReady) + return t.createEntry(interval, job, true, defaultTimes, StatusReady) } // AddOnce is a convenience function for adding a job which only runs once and then exits. func (t *Timer) AddOnce(interval time.Duration, job JobFunc) *Entry { - return t.doAddEntry(interval, job, true, 1, StatusReady) + return t.createEntry(interval, job, true, 1, StatusReady) } // AddTimes is a convenience function for adding a job which is limited running times. func (t *Timer) AddTimes(interval time.Duration, times int, job JobFunc) *Entry { - return t.doAddEntry(interval, job, true, times, StatusReady) + return t.createEntry(interval, job, true, times, StatusReady) } // DelayAdd adds a timing job after delay of duration. @@ -192,77 +116,29 @@ func (t *Timer) Close() { t.status.Set(StatusClosed) } -// doAddEntry adds a timing job to timer for internal usage. -func (t *Timer) doAddEntry(interval time.Duration, job JobFunc, singleton bool, times int, status int) *Entry { - return t.wheels[t.getLevelByIntervalMs(interval.Nanoseconds()/1e6)].addEntry(interval, job, singleton, times, status) -} - -// doAddEntryByParent adds a timing job to timer with parent entry for internal usage. -func (t *Timer) doAddEntryByParent(rollOn bool, nowMs, interval int64, parent *Entry) *Entry { - return t.wheels[t.getLevelByIntervalMs(interval)].addEntryByParent(rollOn, nowMs, interval, parent) -} - -// getLevelByIntervalMs calculates and returns the level of timer wheel with given milliseconds. -func (t *Timer) getLevelByIntervalMs(intervalMs int64) int { - pos, cmp := t.binSearchIndex(intervalMs) - switch cmp { - // If equals to the last comparison value, do not add it directly to this wheel, - // but loop and continue comparison from the index to the first level, - // and add it to the proper level wheel. - case 0: - fallthrough - // If lesser than the last comparison value, - // loop and continue comparison from the index to the first level, - // and add it to the proper level wheel. - case -1: - i := pos - for ; i > 0; i-- { - if intervalMs > t.wheels[i].intervalMs && intervalMs <= t.wheels[i].totalMs { - return i - } - } - return i - - // If greater than the last comparison value, - // loop and continue comparison from the index to the last level, - // and add it to the proper level wheel. - case 1: - i := pos - for ; i < t.length-1; i++ { - if intervalMs > t.wheels[i].intervalMs && intervalMs <= t.wheels[i].totalMs { - return i - } - } - return i +// createEntry creates and adds a timing job to the timer. +func (t *Timer) createEntry(interval time.Duration, job JobFunc, singleton bool, times int, status int) *Entry { + if times <= 0 { + times = defaultTimes } - return 0 -} - -// binSearchIndex uses binary search algorithm for finding the possible level of the wheel -// for the interval value. -func (t *Timer) binSearchIndex(n int64) (index int, result int) { - min := 0 - max := t.length - 1 - mid := 0 - cmp := -2 - for min <= max { - mid = min + int((max-min)/2) - switch { - case t.wheels[mid].intervalMs == n: - cmp = 0 - case t.wheels[mid].intervalMs > n: - cmp = -1 - case t.wheels[mid].intervalMs < n: - cmp = 1 - } - switch cmp { - case -1: - max = mid - 1 - case 1: - min = mid + 1 - case 0: - return mid, cmp - } + var ( + intervalTicksOfJob = int64(interval / t.options.Interval) + ) + if intervalTicksOfJob == 0 { + // If the given interval is lesser than the one of the wheel, + // then sets it to one tick, which means it will be run in one interval. + intervalTicksOfJob = 1 } - return mid, cmp + nextTicks := t.ticks.Val() + intervalTicksOfJob + entry := &Entry{ + job: job, + timer: t, + ticks: intervalTicksOfJob, + times: gtype.NewInt(times), + status: gtype.NewInt(status), + singleton: gtype.NewBool(singleton), + nextTicks: gtype.NewInt64(nextTicks), + } + t.queue.Push(entry, nextTicks) + return entry } diff --git a/os/gtimer/gtimer_timer_loop.go b/os/gtimer/gtimer_timer_loop.go new file mode 100644 index 000000000..61e2ee672 --- /dev/null +++ b/os/gtimer/gtimer_timer_loop.go @@ -0,0 +1,68 @@ +// 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 gtimer + +import "time" + +// loop starts the ticker using a standalone goroutine. +func (t *Timer) loop() { + go func() { + var ( + currentTimerTicks int64 + timerIntervalTicker = time.NewTicker(t.options.Interval) + ) + defer timerIntervalTicker.Stop() + for { + select { + case <-timerIntervalTicker.C: + // Check the timer status. + switch t.status.Val() { + case StatusRunning: + // Timer proceeding. + currentTimerTicks = t.ticks.Add(1) + if currentTimerTicks >= t.queue.LatestPriority() { + t.proceed(currentTimerTicks) + } + + case StatusStopped: + // Do nothing. + + case StatusClosed: + // Timer exits. + return + } + } + } + }() +} + +// proceed proceeds the timer job checking and running logic. +func (t *Timer) proceed(currentTimerTicks int64) { + var ( + value interface{} + ) + for { + value = t.queue.Pop() + if value == nil { + break + } + entry := value.(*Entry) + // It checks if it meets the ticks requirement. + if jobNextTicks := entry.nextTicks.Val(); currentTimerTicks < jobNextTicks { + // It push the job back if current ticks does not meet its running ticks requirement. + t.queue.Push(entry, entry.nextTicks.Val()) + break + } + // It checks the job running requirements and then does asynchronous running. + entry.doCheckAndRunByTicks(currentTimerTicks) + // Status check: push back or ignore it. + if entry.Status() != StatusClosed { + // It pushes the job back to queue for next running. + t.queue.Push(entry, entry.nextTicks.Val()) + } + } +} diff --git a/os/gtimer/gtimer_z_bench_test.go b/os/gtimer/gtimer_z_bench_test.go index 1f5eea33c..139bf1c4a 100644 --- a/os/gtimer/gtimer_z_bench_test.go +++ b/os/gtimer/gtimer_z_bench_test.go @@ -4,17 +4,15 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -package gtimer_test +package gtimer import ( "testing" "time" - - "github.com/gogf/gf/os/gtimer" ) var ( - timer = gtimer.New(5, 30*time.Millisecond) + timer = New() ) func Benchmark_Add(b *testing.B) { @@ -25,6 +23,12 @@ func Benchmark_Add(b *testing.B) { } } +func Benchmark_PriorityQueue_Pop(b *testing.B) { + for i := 0; i < b.N; i++ { + timer.queue.Pop() + } +} + func Benchmark_StartStop(b *testing.B) { for i := 0; i < b.N; i++ { timer.Start() diff --git a/os/gtimer/gtimer_z_unit_entry_test.go b/os/gtimer/gtimer_z_unit_entry_test.go index ba0d2d421..a542111b7 100644 --- a/os/gtimer/gtimer_z_unit_entry_test.go +++ b/os/gtimer/gtimer_z_unit_entry_test.go @@ -4,7 +4,7 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Entry Operations +// Job Operations package gtimer_test @@ -17,40 +17,40 @@ import ( "github.com/gogf/gf/test/gtest" ) -func TestEntry_Start_Stop_Close(t *testing.T) { +func TestJob_Start_Stop_Close(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) - entry := timer.Add(200*time.Millisecond, func() { + job := timer.Add(200*time.Millisecond, func() { array.Append(1) }) time.Sleep(250 * time.Millisecond) t.Assert(array.Len(), 1) - entry.Stop() + job.Stop() time.Sleep(250 * time.Millisecond) t.Assert(array.Len(), 1) - entry.Start() + job.Start() time.Sleep(250 * time.Millisecond) t.Assert(array.Len(), 2) - entry.Close() + job.Close() time.Sleep(250 * time.Millisecond) t.Assert(array.Len(), 2) - t.Assert(entry.Status(), gtimer.StatusClosed) + t.Assert(job.Status(), gtimer.StatusClosed) }) } -func TestEntry_Singleton(t *testing.T) { +func TestJob_Singleton(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) - entry := timer.Add(200*time.Millisecond, func() { + job := timer.Add(200*time.Millisecond, func() { array.Append(1) time.Sleep(10 * time.Second) }) - t.Assert(entry.IsSingleton(), false) - entry.SetSingleton(true) - t.Assert(entry.IsSingleton(), true) + t.Assert(job.IsSingleton(), false) + job.SetSingleton(true) + t.Assert(job.IsSingleton(), true) time.Sleep(250 * time.Millisecond) t.Assert(array.Len(), 1) @@ -59,27 +59,28 @@ func TestEntry_Singleton(t *testing.T) { }) } -func TestEntry_SetTimes(t *testing.T) { +func TestJob_SetTimes(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) - entry := timer.Add(200*time.Millisecond, func() { + job := timer.Add(200*time.Millisecond, func() { array.Append(1) }) - entry.SetTimes(2) + job.SetTimes(2) + //job.IsSingleton() time.Sleep(1200 * time.Millisecond) t.Assert(array.Len(), 2) }) } -func TestEntry_Run(t *testing.T) { +func TestJob_Run(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) - entry := timer.Add(1000*time.Millisecond, func() { + job := timer.Add(1000*time.Millisecond, func() { array.Append(1) }) - entry.Run() + job.Job()() t.Assert(array.Len(), 1) }) } diff --git a/os/gtimer/gtimer_z_unit_timer_internal_test.go b/os/gtimer/gtimer_z_unit_timer_internal_test.go index 5d144fb9a..9882d21c3 100644 --- a/os/gtimer/gtimer_z_unit_timer_internal_test.go +++ b/os/gtimer/gtimer_z_unit_timer_internal_test.go @@ -8,7 +8,6 @@ package gtimer import ( "github.com/gogf/gf/container/garray" - "github.com/gogf/gf/container/gtype" "github.com/gogf/gf/test/gtest" "testing" "time" @@ -16,35 +15,34 @@ import ( func TestTimer_Proceed(t *testing.T) { gtest.C(t, func(t *gtest.T) { - index := gtype.NewInt() array := garray.New(true) - timer := doNewWithoutAutoStart(10, 60*time.Millisecond, 6) - timer.nowFunc = func() time.Time { - return time.Now().Add(time.Duration(index.Add(1)) * time.Millisecond * 60) - } - timer.AddOnce(2*time.Second, func() { + timer := New(TimerOptions{ + Interval: time.Hour, + }) + timer.Add(10000*time.Hour, func() { array.Append(1) }) - timer.AddOnce(1*time.Minute, func() { - array.Append(2) + timer.proceed(10001) + time.Sleep(10 * time.Millisecond) + t.Assert(array.Len(), 1) + timer.proceed(20001) + time.Sleep(10 * time.Millisecond) + t.Assert(array.Len(), 2) + }) + gtest.C(t, func(t *gtest.T) { + array := garray.New(true) + timer := New(TimerOptions{ + Interval: time.Millisecond * 100, }) - timer.AddOnce(5*time.Minute, func() { - array.Append(3) + timer.Add(10000*time.Hour, func() { + array.Append(1) }) - timer.AddOnce(1*time.Hour, func() { - array.Append(4) - }) - timer.AddOnce(100*time.Minute, func() { - array.Append(5) - }) - timer.AddOnce(2*time.Hour, func() { - array.Append(6) - }) - for i := 0; i < 500000; i++ { - timer.wheels[0].proceed() - time.Sleep(10 * time.Microsecond) - } - time.Sleep(time.Second) - t.Assert(array.Slice(), []int{1, 2, 3, 4, 5, 6}) + ticks := int64((10000 * time.Hour) / (time.Millisecond * 100)) + timer.proceed(ticks + 1) + time.Sleep(10 * time.Millisecond) + t.Assert(array.Len(), 1) + timer.proceed(2*ticks + 1) + time.Sleep(10 * time.Millisecond) + t.Assert(array.Len(), 2) }) } diff --git a/os/gtimer/gtimer_z_unit_timer_test.go b/os/gtimer/gtimer_z_unit_timer_test.go index d6da5a8b4..3faf7e908 100644 --- a/os/gtimer/gtimer_z_unit_timer_test.go +++ b/os/gtimer/gtimer_z_unit_timer_test.go @@ -9,7 +9,6 @@ package gtimer_test import ( - "github.com/gogf/gf/os/glog" "testing" "time" @@ -19,7 +18,7 @@ import ( ) func New() *gtimer.Timer { - return gtimer.New(10, 10*time.Millisecond) + return gtimer.New() } func TestTimer_Add_Close(t *testing.T) { @@ -28,15 +27,15 @@ func TestTimer_Add_Close(t *testing.T) { array := garray.New(true) //fmt.Println("start", time.Now()) timer.Add(200*time.Millisecond, func() { - //fmt.Println("entry1", time.Now()) + //fmt.Println("job1", time.Now()) array.Append(1) }) timer.Add(200*time.Millisecond, func() { - //fmt.Println("entry2", time.Now()) + //fmt.Println("job2", time.Now()) array.Append(1) }) timer.Add(400*time.Millisecond, func() { - //fmt.Println("entry3", time.Now()) + //fmt.Println("job3", time.Now()) array.Append(1) }) time.Sleep(250 * time.Millisecond) @@ -55,40 +54,39 @@ func TestTimer_Start_Stop_Close(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) - timer.Add(200*time.Millisecond, func() { - //glog.Println("add...") + timer.Add(1000*time.Millisecond, func() { array.Append(1) }) t.Assert(array.Len(), 0) - time.Sleep(300 * time.Millisecond) + time.Sleep(1200 * time.Millisecond) t.Assert(array.Len(), 1) timer.Stop() - time.Sleep(1000 * time.Millisecond) + time.Sleep(1200 * time.Millisecond) t.Assert(array.Len(), 1) timer.Start() - time.Sleep(200 * time.Millisecond) + time.Sleep(1200 * time.Millisecond) t.Assert(array.Len(), 2) timer.Close() - time.Sleep(1000 * time.Millisecond) + time.Sleep(1200 * time.Millisecond) t.Assert(array.Len(), 2) }) } -func TestTimer_Reset(t *testing.T) { +func TestJob_Reset(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) - glog.Printf("start time:%d", time.Now().Unix()) - singleton := timer.AddSingleton(2*time.Second, func() { - timestamp := time.Now().Unix() - glog.Println(timestamp) - array.Append(timestamp) + job := timer.AddSingleton(500*time.Millisecond, func() { + array.Append(1) }) - time.Sleep(5 * time.Second) - glog.Printf("reset time:%d", time.Now().Unix()) - singleton.Reset() - time.Sleep(10 * time.Second) - t.Assert(array.Len(), 6) + time.Sleep(300 * time.Millisecond) + job.Reset() + time.Sleep(300 * time.Millisecond) + job.Reset() + time.Sleep(300 * time.Millisecond) + job.Reset() + time.Sleep(600 * time.Millisecond) + t.Assert(array.Len(), 1) }) } @@ -156,7 +154,7 @@ func TestTimer_DelayAdd(t *testing.T) { }) } -func TestTimer_DelayAddEntry(t *testing.T) { +func TestTimer_DelayAddJob(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) @@ -227,7 +225,9 @@ func TestTimer_DelayAddTimes(t *testing.T) { func TestTimer_AddLessThanInterval(t *testing.T) { gtest.C(t, func(t *gtest.T) { - timer := gtimer.New(10, 100*time.Millisecond) + timer := gtimer.New(gtimer.TimerOptions{ + Interval: 100 * time.Millisecond, + }) array := garray.New(true) timer.Add(20*time.Millisecond, func() { array.Append(1) @@ -243,7 +243,7 @@ func TestTimer_AddLessThanInterval(t *testing.T) { }) } -func TestTimer_AddLeveledEntry1(t *testing.T) { +func TestTimer_AddLeveledJob1(t *testing.T) { gtest.C(t, func(t *gtest.T) { timer := New() array := garray.New(true) diff --git a/os/gview/gview.go b/os/gview/gview.go index 54813b9ab..dc0013add 100644 --- a/os/gview/gview.go +++ b/os/gview/gview.go @@ -11,6 +11,7 @@ package gview import ( + "context" "github.com/gogf/gf/container/gmap" "github.com/gogf/gf/internal/intlog" @@ -50,9 +51,9 @@ func checkAndInitDefaultView() { // ParseContent parses the template content directly using the default view object // and returns the parsed content. -func ParseContent(content string, params ...Params) (string, error) { +func ParseContent(ctx context.Context, content string, params ...Params) (string, error) { checkAndInitDefaultView() - return defaultViewObj.ParseContent(content, params...) + return defaultViewObj.ParseContent(ctx, content, params...) } // New returns a new view object. diff --git a/os/gview/gview_buildin.go b/os/gview/gview_buildin.go index 064903897..c838750ee 100644 --- a/os/gview/gview_buildin.go +++ b/os/gview/gview_buildin.go @@ -7,6 +7,7 @@ package gview import ( + "context" "fmt" "github.com/gogf/gf/internal/json" "github.com/gogf/gf/util/gutil" @@ -115,7 +116,7 @@ func (view *View) buildInFuncInclude(file interface{}, data ...map[string]interf return "" } // It will search the file internally. - content, err := view.Parse(path, m) + content, err := view.Parse(context.TODO(), path, m) if err != nil { return htmltpl.HTML(err.Error()) } diff --git a/os/gview/gview_i18n.go b/os/gview/gview_i18n.go index df8ca6584..554f2f567 100644 --- a/os/gview/gview_i18n.go +++ b/os/gview/gview_i18n.go @@ -6,18 +6,33 @@ package gview -import "github.com/gogf/gf/util/gconv" +import ( + "context" + "github.com/gogf/gf/i18n/gi18n" + "github.com/gogf/gf/util/gconv" +) + +const ( + i18nLanguageVariableName = "I18nLanguage" +) // i18nTranslate translate the content with i18n feature. -func (view *View) i18nTranslate(content string, params Params) string { +func (view *View) i18nTranslate(ctx context.Context, content string, variables Params) string { if view.config.I18nManager != nil { - if v, ok := params["I18nLanguage"]; ok { - language := gconv.String(v) - if language != "" { - return view.config.I18nManager.T(content, language) - } + // Compatible with old version. + if language, ok := variables[i18nLanguageVariableName]; ok { + ctx = gi18n.WithLanguage(ctx, gconv.String(language)) } - return view.config.I18nManager.T(content) + return view.config.I18nManager.T(ctx, content) } return content } + +// setI18nLanguageFromCtx retrieves language name from context and sets it to template variables map. +func (view *View) setI18nLanguageFromCtx(ctx context.Context, variables map[string]interface{}) { + if language, ok := variables[i18nLanguageVariableName]; !ok { + if language = gi18n.LanguageFromCtx(ctx); language != "" { + variables[i18nLanguageVariableName] = language + } + } +} diff --git a/os/gview/gview_parse.go b/os/gview/gview_parse.go index e02d9dd75..f69d7e08b 100644 --- a/os/gview/gview_parse.go +++ b/os/gview/gview_parse.go @@ -8,6 +8,7 @@ package gview import ( "bytes" + "context" "errors" "fmt" "github.com/gogf/gf/encoding/ghash" @@ -54,7 +55,7 @@ var ( // Parse parses given template file with given template variables // and returns the parsed template content. -func (view *View) Parse(file string, params ...Params) (result string, err error) { +func (view *View) Parse(ctx context.Context, file string, params ...Params) (result string, err error) { var tpl interface{} // It caches the file, folder and its content to enhance performance. r := view.fileCacheMap.GetOrSetFuncLock(file, func() interface{} { @@ -125,6 +126,8 @@ func (view *View) Parse(file string, params ...Params) (result string, err error if len(view.data) > 0 { gutil.MapMerge(variables, view.data) } + view.setI18nLanguageFromCtx(ctx, variables) + buffer := bytes.NewBuffer(nil) if view.config.AutoEncode { newTpl, err := tpl.(*htmltpl.Template).Clone() @@ -142,18 +145,18 @@ func (view *View) Parse(file string, params ...Params) (result string, err error // TODO any graceful plan to replace ""? result = gstr.Replace(buffer.String(), "", "") - result = view.i18nTranslate(result, variables) + result = view.i18nTranslate(ctx, result, variables) return result, nil } // ParseDefault parses the default template file with params. -func (view *View) ParseDefault(params ...Params) (result string, err error) { - return view.Parse(view.config.DefaultFile, params...) +func (view *View) ParseDefault(ctx context.Context, params ...Params) (result string, err error) { + return view.Parse(ctx, view.config.DefaultFile, params...) } // ParseContent parses given template content with template variables // and returns the parsed content in []byte. -func (view *View) ParseContent(content string, params ...Params) (string, error) { +func (view *View) ParseContent(ctx context.Context, content string, params ...Params) (string, error) { // It's not necessary continuing parsing if template content is empty. if content == "" { return "", nil @@ -191,6 +194,8 @@ func (view *View) ParseContent(content string, params ...Params) (string, error) if len(view.data) > 0 { gutil.MapMerge(variables, view.data) } + view.setI18nLanguageFromCtx(ctx, variables) + buffer := bytes.NewBuffer(nil) if view.config.AutoEncode { newTpl, err := tpl.(*htmltpl.Template).Clone() @@ -207,7 +212,7 @@ func (view *View) ParseContent(content string, params ...Params) (string, error) } // TODO any graceful plan to replace ""? result := gstr.Replace(buffer.String(), "", "") - result = view.i18nTranslate(result, variables) + result = view.i18nTranslate(ctx, result, variables) return result, nil } diff --git a/os/gview/gview_unit_basic_test.go b/os/gview/gview_unit_basic_test.go index 53caf1b8c..4cad9247c 100644 --- a/os/gview/gview_unit_basic_test.go +++ b/os/gview/gview_unit_basic_test.go @@ -7,6 +7,7 @@ package gview_test import ( + "context" "github.com/gogf/gf/encoding/ghtml" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/util/gconv" @@ -39,18 +40,18 @@ func Test_Basic(t *testing.T) { view.Assigns(g.Map{"version": "1.7.0"}) view.BindFunc("GetName", func() string { return "gf" }) view.BindFuncMap(gview.FuncMap{"GetVersion": func() string { return "1.7.0" }}) - result, err := view.ParseContent(str, g.Map{"other": "that's all"}) + result, err := view.ParseContent(context.TODO(), str, g.Map{"other": "that's all"}) t.Assert(err != nil, false) t.Assert(result, "hello gf,version:1.7.0;hello gf,version:1.7.0;that's all") //测试api方法 str = `hello {{.name}}` - result, err = gview.ParseContent(str, g.Map{"name": "gf"}) + result, err = gview.ParseContent(context.TODO(), str, g.Map{"name": "gf"}) t.Assert(err != nil, false) t.Assert(result, "hello gf") //测试instance方法 - result, err = gview.Instance().ParseContent(str, g.Map{"name": "gf"}) + result, err = gview.Instance().ParseContent(context.TODO(), str, g.Map{"name": "gf"}) t.Assert(err != nil, false) t.Assert(result, "hello gf") }) @@ -59,132 +60,132 @@ func Test_Basic(t *testing.T) { func Test_Func(t *testing.T) { gtest.C(t, func(t *gtest.T) { str := `{{eq 1 1}};{{eq 1 2}};{{eq "A" "B"}}` - result, err := gview.ParseContent(str, nil) + result, err := gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `true;false;false`) str = `{{ne 1 2}};{{ne 1 1}};{{ne "A" "B"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `true;false;true`) str = `{{lt 1 2}};{{lt 1 1}};{{lt 1 0}};{{lt "A" "B"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `true;false;false;true`) str = `{{le 1 2}};{{le 1 1}};{{le 1 0}};{{le "A" "B"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `true;true;false;true`) str = `{{gt 1 2}};{{gt 1 1}};{{gt 1 0}};{{gt "A" "B"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `false;false;true;false`) str = `{{ge 1 2}};{{ge 1 1}};{{ge 1 0}};{{ge "A" "B"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `false;true;true;false`) str = `{{"
测试
"|text}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `测试`) str = `{{"
测试
"|html}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `<div>测试</div>`) str = `{{"
测试
"|htmlencode}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `<div>测试</div>`) str = `{{"<div>测试</div>"|htmldecode}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `
测试
`) str = `{{"https://goframe.org"|url}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `https%3A%2F%2Fgoframe.org`) str = `{{"https://goframe.org"|urlencode}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `https%3A%2F%2Fgoframe.org`) str = `{{"https%3A%2F%2Fgoframe.org"|urldecode}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `https://goframe.org`) str = `{{"https%3NA%2F%2Fgoframe.org"|urldecode}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(gstr.Contains(result, "invalid URL escape"), true) str = `{{1540822968 | date "Y-m-d"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `2018-10-29`) str = `{{date "Y-m-d"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) str = `{{"我是中国人" | substr 2 -1}};{{"我是中国人" | substr 2 2}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `中国人;中国`) str = `{{"我是中国人" | strlimit 2 "..."}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `我是...`) str = `{{"I'm中国人" | replace "I'm" "我是"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `我是中国人`) str = `{{compare "A" "B"}};{{compare "1" "2"}};{{compare 2 1}};{{compare 1 1}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `-1;-1;1;0`) str = `{{"热爱GF热爱生活" | hidestr 20 "*"}};{{"热爱GF热爱生活" | hidestr 50 "*"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `热爱GF*爱生活;热爱****生活`) str = `{{"热爱GF热爱生活" | highlight "GF" "red"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `热爱GF热爱生活`) str = `{{"gf" | toupper}};{{"GF" | tolower}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `GF;gf`) str = `{{concat "I" "Love" "GoFrame"}}` - result, err = gview.ParseContent(str, nil) + result, err = gview.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, `ILoveGoFrame`) }) // eq: multiple values. gtest.C(t, func(t *gtest.T) { str := `{{eq 1 2 1 3 4 5}}` - result, err := gview.ParseContent(str, nil) + result, err := gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `true`) }) gtest.C(t, func(t *gtest.T) { str := `{{eq 6 2 1 3 4 5}}` - result, err := gview.ParseContent(str, nil) + result, err := gview.ParseContent(context.TODO(), str, nil) t.Assert(err != nil, false) t.Assert(result, `false`) }) @@ -193,7 +194,7 @@ func Test_Func(t *testing.T) { func Test_FuncNl2Br(t *testing.T) { gtest.C(t, func(t *gtest.T) { str := `{{"Go\nFrame" | nl2br}}` - result, err := gview.ParseContent(str, nil) + result, err := gview.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, `Go
Frame`) }) @@ -203,7 +204,7 @@ func Test_FuncNl2Br(t *testing.T) { s += "Go\nFrame\n中文" } str := `{{.content | nl2br}}` - result, err := gview.ParseContent(str, g.Map{ + result, err := gview.ParseContent(context.TODO(), str, g.Map{ "content": s, }) t.Assert(err, nil) @@ -231,10 +232,10 @@ func Test_FuncInclude(t *testing.T) { ioutil.WriteFile(templatePath+gfile.Separator+"footer.html", []byte(footer), 0644) ioutil.WriteFile(templatePath+gfile.Separator+"layout.html", []byte(layout), 0644) view := gview.New(templatePath) - result, err := view.Parse("notfound.html") + result, err := view.Parse(context.TODO(), "notfound.html") t.Assert(err != nil, true) t.Assert(result, ``) - result, err = view.Parse("layout.html") + result, err = view.Parse(context.TODO(), "layout.html") t.Assert(err != nil, false) t.Assert(result, `

HEADER

hello gf

@@ -243,7 +244,7 @@ func Test_FuncInclude(t *testing.T) { gfile.Mkdir(templatePath + gfile.Separator + "template") gfile.Create(notfoundPath) ioutil.WriteFile(notfoundPath, []byte("notfound"), 0644) - result, err = view.Parse("notfound.html") + result, err = view.Parse(context.TODO(), "notfound.html") t.Assert(err != nil, true) t.Assert(result, ``) }) @@ -283,7 +284,7 @@ func Test_ParseContent(t *testing.T) { gtest.C(t, func(t *gtest.T) { str := `{{.name}}` view := gview.New() - result, err := view.ParseContent(str, g.Map{"name": func() {}}) + result, err := view.ParseContent(context.TODO(), str, g.Map{"name": func() {}}) t.Assert(err != nil, true) t.Assert(result, ``) }) @@ -306,7 +307,7 @@ func Test_HotReload(t *testing.T) { view := gview.New(dirPath) time.Sleep(100 * time.Millisecond) - result, err := view.Parse("test.html", g.Map{ + result, err := view.Parse(context.TODO(), "test.html", g.Map{ "var": "1", }) t.Assert(err, nil) @@ -317,7 +318,7 @@ func Test_HotReload(t *testing.T) { t.Assert(err, nil) time.Sleep(100 * time.Millisecond) - result, err = view.Parse("test.html", g.Map{ + result, err = view.Parse(context.TODO(), "test.html", g.Map{ "var": "2", }) t.Assert(err, nil) @@ -329,7 +330,7 @@ func Test_XSS(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() s := "
" - r, err := v.ParseContent("{{.v}}", g.Map{ + r, err := v.ParseContent(context.TODO(), "{{.v}}", g.Map{ "v": s, }) t.Assert(err, nil) @@ -339,7 +340,7 @@ func Test_XSS(t *testing.T) { v := gview.New() v.SetAutoEncode(true) s := "
" - r, err := v.ParseContent("{{.v}}", g.Map{ + r, err := v.ParseContent(context.TODO(), "{{.v}}", g.Map{ "v": s, }) t.Assert(err, nil) @@ -350,7 +351,7 @@ func Test_XSS(t *testing.T) { v := gview.New() v.SetAutoEncode(true) s := "
" - r, err := v.ParseContent("{{if eq 1 1}}{{.v}}{{end}}", g.Map{ + r, err := v.ParseContent(context.TODO(), "{{if eq 1 1}}{{.v}}{{end}}", g.Map{ "v": s, }) t.Assert(err, nil) @@ -371,7 +372,7 @@ func Test_BuildInFuncMap(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() v.Assign("v", new(TypeForBuildInFuncMap)) - r, err := v.ParseContent("{{range $k, $v := map .v.Test}} {{$k}}:{{$v}} {{end}}") + r, err := v.ParseContent(context.TODO(), "{{range $k, $v := map .v.Test}} {{$k}}:{{$v}} {{end}}") t.Assert(err, nil) t.Assert(gstr.Contains(r, "Name:john"), true) t.Assert(gstr.Contains(r, "Score:99.9"), true) @@ -394,7 +395,7 @@ func Test_BuildInFuncMaps(t *testing.T) { gtest.C(t, func(t *gtest.T) { v := gview.New() v.Assign("v", new(TypeForBuildInFuncMaps)) - r, err := v.ParseContent("{{range $k, $v := maps .v.Test}} {{$k}}:{{$v.Name}} {{$v.Score}} {{end}}") + r, err := v.ParseContent(context.TODO(), "{{range $k, $v := maps .v.Test}} {{$k}}:{{$v.Name}} {{$v.Score}} {{end}}") t.Assert(err, nil) t.Assert(r, ` 0:john 99.9 1:smith 100 `) }) @@ -407,7 +408,7 @@ func Test_BuildInFuncDump(t *testing.T) { "name": "john", "score": 100, }) - r, err := v.ParseContent("{{dump .}}") + r, err := v.ParseContent(context.TODO(), "{{dump .}}") t.Assert(err, nil) t.Assert(gstr.Contains(r, `"name": "john"`), true) t.Assert(gstr.Contains(r, `"score": 100`), true) @@ -420,7 +421,7 @@ func Test_BuildInFuncJson(t *testing.T) { v.Assign("v", g.Map{ "name": "john", }) - r, err := v.ParseContent("{{json .v}}") + r, err := v.ParseContent(context.TODO(), "{{json .v}}") t.Assert(err, nil) t.Assert(r, `{"name":"john"}`) }) diff --git a/os/gview/gview_unit_config_test.go b/os/gview/gview_unit_config_test.go index 314b4be48..899245157 100644 --- a/os/gview/gview_unit_config_test.go +++ b/os/gview/gview_unit_config_test.go @@ -7,6 +7,7 @@ package gview_test import ( + "context" "github.com/gogf/gf/debug/gdebug" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/os/gview" @@ -30,11 +31,11 @@ func Test_Config(t *testing.T) { str := `hello ${.name},version:${.version}` view.Assigns(g.Map{"version": "1.7.0"}) - result, err := view.ParseContent(str, nil) + result, err := view.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, "hello gf,version:1.7.0") - result, err = view.ParseDefault() + result, err = view.ParseDefault(context.TODO()) t.Assert(err, nil) t.Assert(result, "name:gf") }) @@ -55,11 +56,11 @@ func Test_ConfigWithMap(t *testing.T) { str := `hello ${.name},version:${.version}` view.Assigns(g.Map{"version": "1.7.0"}) - result, err := view.ParseContent(str, nil) + result, err := view.ParseContent(context.TODO(), str, nil) t.Assert(err, nil) t.Assert(result, "hello gf,version:1.7.0") - result, err = view.ParseDefault() + result, err = view.ParseDefault(context.TODO()) t.Assert(err, nil) t.Assert(result, "name:gf") }) diff --git a/os/gview/gview_unit_encode_test.go b/os/gview/gview_unit_encode_test.go index 1aad3616d..892be705b 100644 --- a/os/gview/gview_unit_encode_test.go +++ b/os/gview/gview_unit_encode_test.go @@ -7,6 +7,7 @@ package gview_test import ( + "context" "github.com/gogf/gf/debug/gdebug" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/os/gfile" @@ -20,7 +21,7 @@ func Test_Encode_Parse(t *testing.T) { v := gview.New() v.SetPath(gdebug.TestDataPath("tpl")) v.SetAutoEncode(true) - result, err := v.Parse("encode.tpl", g.Map{ + result, err := v.Parse(context.TODO(), "encode.tpl", g.Map{ "title": "my title", }) t.Assert(err, nil) @@ -33,7 +34,7 @@ func Test_Encode_ParseContent(t *testing.T) { v := gview.New() tplContent := gfile.GetContents(gdebug.TestDataPath("tpl", "encode.tpl")) v.SetAutoEncode(true) - result, err := v.ParseContent(tplContent, g.Map{ + result, err := v.ParseContent(context.TODO(), tplContent, g.Map{ "title": "my title", }) t.Assert(err, nil) diff --git a/os/gview/gview_unit_i18n_test.go b/os/gview/gview_unit_i18n_test.go index b04ba2bbd..23550e66c 100644 --- a/os/gview/gview_unit_i18n_test.go +++ b/os/gview/gview_unit_i18n_test.go @@ -7,6 +7,7 @@ package gview_test import ( + "context" "testing" "github.com/gogf/gf/debug/gdebug" @@ -26,21 +27,21 @@ func Test_I18n(t *testing.T) { g.I18n().SetPath(gdebug.TestDataPath("i18n")) g.I18n().SetLanguage("zh-CN") - result1, err := g.View().ParseContent(content, g.Map{ + result1, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", }) t.Assert(err, nil) t.Assert(result1, expect1) g.I18n().SetLanguage("ja") - result2, err := g.View().ParseContent(content, g.Map{ + result2, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", }) t.Assert(err, nil) t.Assert(result2, expect2) g.I18n().SetLanguage("none") - result3, err := g.View().ParseContent(content, g.Map{ + result3, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", }) t.Assert(err, nil) @@ -54,21 +55,21 @@ func Test_I18n(t *testing.T) { g.I18n().SetPath(gdebug.CallerDirectory() + gfile.Separator + "testdata" + gfile.Separator + "i18n") - result1, err := g.View().ParseContent(content, g.Map{ + result1, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", "I18nLanguage": "zh-CN", }) t.Assert(err, nil) t.Assert(result1, expect1) - result2, err := g.View().ParseContent(content, g.Map{ + result2, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", "I18nLanguage": "ja", }) t.Assert(err, nil) t.Assert(result2, expect2) - result3, err := g.View().ParseContent(content, g.Map{ + result3, err := g.View().ParseContent(context.TODO(), content, g.Map{ "name": "john", "I18nLanguage": "none", }) diff --git a/util/gconv/gconv.go b/util/gconv/gconv.go index 271c23cb7..be6b027ac 100644 --- a/util/gconv/gconv.go +++ b/util/gconv/gconv.go @@ -13,6 +13,7 @@ import ( "fmt" "github.com/gogf/gf/internal/json" "github.com/gogf/gf/os/gtime" + "math" "reflect" "strconv" "strings" @@ -39,13 +40,13 @@ var ( "false": {}, } - // Priority tags for Map*/Struct* functions. + // StructTagPriority defines the default priority tags for Map*/Struct* functions. // Note, the "gconv", "param", "params" tags are used by old version of package. // It is strongly recommended using short tag "c" or "p" instead in the future. StructTagPriority = []string{"gconv", "param", "params", "c", "p", "json"} ) -// Convert converts the variable `i` to the type `t`, the type `t` is specified by string. +// Convert converts the variable `any` to the type `t`, the type `t` is specified by string. // The optional parameter `params` is used for additional necessary parameter for this conversion. // It supports common types conversion as its conversion based on type name string. func Convert(any interface{}, t string, params ...interface{}) interface{} { @@ -277,7 +278,7 @@ func Convert(any interface{}, t string, params ...interface{}) interface{} { } } -// Byte converts `i` to byte. +// Byte converts `any` to byte. func Byte(any interface{}) byte { if v, ok := any.(byte); ok { return v @@ -285,7 +286,7 @@ func Byte(any interface{}) byte { return Uint8(any) } -// Bytes converts `i` to []byte. +// Bytes converts `any` to []byte. func Bytes(any interface{}) []byte { if any == nil { return nil @@ -299,19 +300,45 @@ func Bytes(any interface{}) []byte { if f, ok := value.(apiBytes); ok { return f.Bytes() } + var ( + reflectValue = reflect.ValueOf(any) + reflectKind = reflectValue.Kind() + ) + for reflectKind == reflect.Ptr { + reflectValue = reflectValue.Elem() + reflectKind = reflectValue.Kind() + } + switch reflectKind { + case reflect.Array, reflect.Slice: + var ( + ok = true + bytes = make([]byte, reflectValue.Len()) + ) + for i, _ := range bytes { + int32Value := Int32(reflectValue.Index(i).Interface()) + if int32Value < 0 || int32Value > math.MaxUint8 { + ok = false + break + } + bytes[i] = byte(int32Value) + } + if ok { + return bytes + } + } return gbinary.Encode(any) } } -// Rune converts `i` to rune. +// Rune converts `any` to rune. func Rune(any interface{}) rune { if v, ok := any.(rune); ok { return v } - return rune(Int32(any)) + return Int32(any) } -// Runes converts `i` to []rune. +// Runes converts `any` to []rune. func Runes(any interface{}) []rune { if v, ok := any.([]rune); ok { return v @@ -319,7 +346,7 @@ func Runes(any interface{}) []rune { return []rune(String(any)) } -// String converts `i` to string. +// String converts `any` to string. // It's most common used converting function. func String(any interface{}) string { if any == nil { @@ -422,8 +449,8 @@ func String(any interface{}) string { } } -// Bool converts `i` to bool. -// It returns false if `i` is: false, "", 0, "false", "off", "no", empty slice/map. +// Bool converts `any` to bool. +// It returns false if `any` is: false, "", 0, "false", "off", "no", empty slice/map. func Bool(any interface{}) bool { if any == nil { return false @@ -467,7 +494,7 @@ func Bool(any interface{}) bool { } } -// Int converts `i` to int. +// Int converts `any` to int. func Int(any interface{}) int { if any == nil { return 0 @@ -478,7 +505,7 @@ func Int(any interface{}) int { return int(Int64(any)) } -// Int8 converts `i` to int8. +// Int8 converts `any` to int8. func Int8(any interface{}) int8 { if any == nil { return 0 @@ -489,7 +516,7 @@ func Int8(any interface{}) int8 { return int8(Int64(any)) } -// Int16 converts `i` to int16. +// Int16 converts `any` to int16. func Int16(any interface{}) int16 { if any == nil { return 0 @@ -500,7 +527,7 @@ func Int16(any interface{}) int16 { return int16(Int64(any)) } -// Int32 converts `i` to int32. +// Int32 converts `any` to int32. func Int32(any interface{}) int32 { if any == nil { return 0 @@ -511,7 +538,7 @@ func Int32(any interface{}) int32 { return int32(Int64(any)) } -// Int64 converts `i` to int64. +// Int64 converts `any` to int64. func Int64(any interface{}) int64 { if any == nil { return 0 @@ -592,7 +619,7 @@ func Int64(any interface{}) int64 { } } -// Uint converts `i` to uint. +// Uint converts `any` to uint. func Uint(any interface{}) uint { if any == nil { return 0 @@ -603,7 +630,7 @@ func Uint(any interface{}) uint { return uint(Uint64(any)) } -// Uint8 converts `i` to uint8. +// Uint8 converts `any` to uint8. func Uint8(any interface{}) uint8 { if any == nil { return 0 @@ -614,7 +641,7 @@ func Uint8(any interface{}) uint8 { return uint8(Uint64(any)) } -// Uint16 converts `i` to uint16. +// Uint16 converts `any` to uint16. func Uint16(any interface{}) uint16 { if any == nil { return 0 @@ -625,7 +652,7 @@ func Uint16(any interface{}) uint16 { return uint16(Uint64(any)) } -// Uint32 converts `i` to uint32. +// Uint32 converts `any` to uint32. func Uint32(any interface{}) uint32 { if any == nil { return 0 @@ -636,7 +663,7 @@ func Uint32(any interface{}) uint32 { return uint32(Uint64(any)) } -// Uint64 converts `i` to uint64. +// Uint64 converts `any` to uint64. func Uint64(any interface{}) uint64 { if any == nil { return 0 @@ -699,7 +726,7 @@ func Uint64(any interface{}) uint64 { } } -// Float32 converts `i` to float32. +// Float32 converts `any` to float32. func Float32(any interface{}) float32 { if any == nil { return 0 @@ -720,7 +747,7 @@ func Float32(any interface{}) float32 { } } -// Float64 converts `i` to float64. +// Float64 converts `any` to float64. func Float64(any interface{}) float64 { if any == nil { return 0 diff --git a/util/gconv/gconv_interface.go b/util/gconv/gconv_interface.go index 8ddbe7c3b..cce763cc0 100644 --- a/util/gconv/gconv_interface.go +++ b/util/gconv/gconv_interface.go @@ -6,6 +6,8 @@ package gconv +import "github.com/gogf/gf/os/gtime" + // apiString is used for type assert api for String(). type apiString interface { String() string @@ -92,3 +94,8 @@ type apiUnmarshalText interface { type apiSet interface { Set(value interface{}) (old interface{}) } + +// apiGTime is the interface for gtime.Time converting. +type apiGTime interface { + GTime(format ...string) *gtime.Time +} diff --git a/util/gconv/gconv_map.go b/util/gconv/gconv_map.go index 5563ddda8..6685433b2 100644 --- a/util/gconv/gconv_map.go +++ b/util/gconv/gconv_map.go @@ -56,7 +56,7 @@ func doMapConvert(value interface{}, recursive bool, tags ...string) map[string] case string: // If it is a JSON string, automatically unmarshal it! if len(r) > 0 && r[0] == '{' && r[len(r)-1] == '}' { - if err := json.Unmarshal([]byte(r), &dataMap); err != nil { + if err := json.UnmarshalUseNumber([]byte(r), &dataMap); err != nil { return nil } } else { @@ -65,7 +65,7 @@ func doMapConvert(value interface{}, recursive bool, tags ...string) map[string] case []byte: // If it is a JSON string, automatically unmarshal it! if len(r) > 0 && r[0] == '{' && r[len(r)-1] == '}' { - if err := json.Unmarshal(r, &dataMap); err != nil { + if err := json.UnmarshalUseNumber(r, &dataMap); err != nil { return nil } } else { @@ -301,7 +301,7 @@ func doMapConvertForMapOrStructValue(isRoot bool, value interface{}, recursive b // It means this attribute field has desired tag. dataMap[mapKey] = doMapConvertForMapOrStructValue(false, rvAttrInterface, true, tags...) } else { - dataMap[mapKey] = doMapConvertForMapOrStructValue(false, rvAttrInterface, false, tags...) + dataMap[mapKey] = doMapConvertForMapOrStructValue(false, rvAttrInterface, recursive, tags...) } // The struct attribute is type of slice. diff --git a/util/gconv/gconv_maps.go b/util/gconv/gconv_maps.go index 8a05e9600..cc0dd33fb 100644 --- a/util/gconv/gconv_maps.go +++ b/util/gconv/gconv_maps.go @@ -23,7 +23,7 @@ func SliceStruct(params interface{}, pointer interface{}, mapping ...map[string] return Structs(params, pointer, mapping...) } -// Maps converts `i` to []map[string]interface{}. +// Maps converts `value` to []map[string]interface{}. // Note that it automatically checks and converts json string to []map if `value` is string/[]byte. func Maps(value interface{}, tags ...string) []map[string]interface{} { if value == nil { @@ -33,7 +33,7 @@ func Maps(value interface{}, tags ...string) []map[string]interface{} { case string: list := make([]map[string]interface{}, 0) if len(r) > 0 && r[0] == '[' && r[len(r)-1] == ']' { - if err := json.Unmarshal([]byte(r), &list); err != nil { + if err := json.UnmarshalUseNumber([]byte(r), &list); err != nil { return nil } return list @@ -44,7 +44,7 @@ func Maps(value interface{}, tags ...string) []map[string]interface{} { case []byte: list := make([]map[string]interface{}, 0) if len(r) > 0 && r[0] == '[' && r[len(r)-1] == ']' { - if err := json.Unmarshal(r, &list); err != nil { + if err := json.UnmarshalUseNumber(r, &list); err != nil { return nil } return list @@ -68,7 +68,7 @@ func Maps(value interface{}, tags ...string) []map[string]interface{} { } } -// MapsDeep converts `i` to []map[string]interface{} recursively. +// MapsDeep converts `value` to []map[string]interface{} recursively. // // TODO completely implement the recursive converting for all types. func MapsDeep(value interface{}, tags ...string) []map[string]interface{} { @@ -79,7 +79,7 @@ func MapsDeep(value interface{}, tags ...string) []map[string]interface{} { case string: list := make([]map[string]interface{}, 0) if len(r) > 0 && r[0] == '[' && r[len(r)-1] == ']' { - if err := json.Unmarshal([]byte(r), &list); err != nil { + if err := json.UnmarshalUseNumber([]byte(r), &list); err != nil { return nil } return list @@ -90,7 +90,7 @@ func MapsDeep(value interface{}, tags ...string) []map[string]interface{} { case []byte: list := make([]map[string]interface{}, 0) if len(r) > 0 && r[0] == '[' && r[len(r)-1] == ']' { - if err := json.Unmarshal(r, &list); err != nil { + if err := json.UnmarshalUseNumber(r, &list); err != nil { return nil } return list diff --git a/util/gconv/gconv_maptomap.go b/util/gconv/gconv_maptomap.go index 143d6d013..ded5ea744 100644 --- a/util/gconv/gconv_maptomap.go +++ b/util/gconv/gconv_maptomap.go @@ -36,20 +36,20 @@ func doMapToMap(params interface{}, pointer interface{}, mapping ...map[string]s if json.Valid(r) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(r, rv.Interface()) + return json.UnmarshalUseNumber(r, rv.Interface()) } } else { - return json.Unmarshal(r, pointer) + return json.UnmarshalUseNumber(r, pointer) } } case string: if paramsBytes := []byte(r); json.Valid(paramsBytes) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(paramsBytes, rv.Interface()) + return json.UnmarshalUseNumber(paramsBytes, rv.Interface()) } } else { - return json.Unmarshal(paramsBytes, pointer) + return json.UnmarshalUseNumber(paramsBytes, pointer) } } } diff --git a/util/gconv/gconv_maptomaps.go b/util/gconv/gconv_maptomaps.go index 9e0d58306..f4bebc7dc 100644 --- a/util/gconv/gconv_maptomaps.go +++ b/util/gconv/gconv_maptomaps.go @@ -40,20 +40,20 @@ func doMapToMaps(params interface{}, pointer interface{}, mapping ...map[string] if json.Valid(r) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(r, rv.Interface()) + return json.UnmarshalUseNumber(r, rv.Interface()) } } else { - return json.Unmarshal(r, pointer) + return json.UnmarshalUseNumber(r, pointer) } } case string: if paramsBytes := []byte(r); json.Valid(paramsBytes) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(paramsBytes, rv.Interface()) + return json.UnmarshalUseNumber(paramsBytes, rv.Interface()) } } else { - return json.Unmarshal(paramsBytes, pointer) + return json.UnmarshalUseNumber(paramsBytes, pointer) } } } diff --git a/util/gconv/gconv_slice_any.go b/util/gconv/gconv_slice_any.go index e249c56ab..90b5b372b 100644 --- a/util/gconv/gconv_slice_any.go +++ b/util/gconv/gconv_slice_any.go @@ -15,7 +15,7 @@ func SliceAny(any interface{}) []interface{} { return Interfaces(any) } -// Interfaces converts `i` to []interface{}. +// Interfaces converts `any` to []interface{}. func Interfaces(any interface{}) []interface{} { if any == nil { return nil diff --git a/util/gconv/gconv_slice_float.go b/util/gconv/gconv_slice_float.go index d3d442c03..86dd7ebe9 100644 --- a/util/gconv/gconv_slice_float.go +++ b/util/gconv/gconv_slice_float.go @@ -23,12 +23,12 @@ func SliceFloat64(any interface{}) []float64 { return Floats(any) } -// Floats converts `i` to []float64. +// Floats converts `any` to []float64. func Floats(any interface{}) []float64 { return Float64s(any) } -// Float32s converts `i` to []float32. +// Float32s converts `any` to []float32. func Float32s(any interface{}) []float32 { if any == nil { return nil @@ -148,7 +148,7 @@ func Float32s(any interface{}) []float32 { return array } -// Float64s converts `i` to []float64. +// Float64s converts `any` to []float64. func Float64s(any interface{}) []float64 { if any == nil { return nil diff --git a/util/gconv/gconv_slice_int.go b/util/gconv/gconv_slice_int.go index f19afadd4..1195d2053 100644 --- a/util/gconv/gconv_slice_int.go +++ b/util/gconv/gconv_slice_int.go @@ -23,7 +23,7 @@ func SliceInt64(any interface{}) []int64 { return Int64s(any) } -// Ints converts `i` to []int. +// Ints converts `any` to []int. func Ints(any interface{}) []int { if any == nil { return nil @@ -153,7 +153,7 @@ func Ints(any interface{}) []int { return array } -// Int32s converts `i` to []int32. +// Int32s converts `any` to []int32. func Int32s(any interface{}) []int32 { if any == nil { return nil @@ -283,7 +283,7 @@ func Int32s(any interface{}) []int32 { return array } -// Int64s converts `i` to []int64. +// Int64s converts `any` to []int64. func Int64s(any interface{}) []int64 { if any == nil { return nil diff --git a/util/gconv/gconv_slice_str.go b/util/gconv/gconv_slice_str.go index ab003cec2..0d235d529 100644 --- a/util/gconv/gconv_slice_str.go +++ b/util/gconv/gconv_slice_str.go @@ -13,7 +13,7 @@ func SliceStr(any interface{}) []string { return Strings(any) } -// Strings converts `i` to []string. +// Strings converts `any` to []string. func Strings(any interface{}) []string { if any == nil { return nil diff --git a/util/gconv/gconv_slice_uint.go b/util/gconv/gconv_slice_uint.go index f2277573b..2cb3321d9 100644 --- a/util/gconv/gconv_slice_uint.go +++ b/util/gconv/gconv_slice_uint.go @@ -23,7 +23,7 @@ func SliceUint64(any interface{}) []uint64 { return Uint64s(any) } -// Uints converts `i` to []uint. +// Uints converts `any` to []uint. func Uints(any interface{}) []uint { if any == nil { return nil @@ -149,7 +149,7 @@ func Uints(any interface{}) []uint { return array } -// Uint32s converts `i` to []uint32. +// Uint32s converts `any` to []uint32. func Uint32s(any interface{}) []uint32 { if any == nil { return nil @@ -274,7 +274,7 @@ func Uint32s(any interface{}) []uint32 { return array } -// Uint64s converts `i` to []uint64. +// Uint64s converts `any` to []uint64. func Uint64s(any interface{}) []uint64 { if any == nil { return nil diff --git a/util/gconv/gconv_struct.go b/util/gconv/gconv_struct.go index 0d4924bd7..06416f863 100644 --- a/util/gconv/gconv_struct.go +++ b/util/gconv/gconv_struct.go @@ -83,20 +83,20 @@ func doStruct(params interface{}, pointer interface{}, mapping map[string]string if json.Valid(r) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(r, rv.Interface()) + return json.UnmarshalUseNumber(r, rv.Interface()) } } else { - return json.Unmarshal(r, pointer) + return json.UnmarshalUseNumber(r, pointer) } } case string: if paramsBytes := []byte(r); json.Valid(paramsBytes) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(paramsBytes, rv.Interface()) + return json.UnmarshalUseNumber(paramsBytes, rv.Interface()) } } else { - return json.Unmarshal(paramsBytes, pointer) + return json.UnmarshalUseNumber(paramsBytes, pointer) } } } diff --git a/util/gconv/gconv_structs.go b/util/gconv/gconv_structs.go index 89e220ac4..8d3626d36 100644 --- a/util/gconv/gconv_structs.go +++ b/util/gconv/gconv_structs.go @@ -74,20 +74,20 @@ func doStructs(params interface{}, pointer interface{}, mapping map[string]strin if json.Valid(r) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(r, rv.Interface()) + return json.UnmarshalUseNumber(r, rv.Interface()) } } else { - return json.Unmarshal(r, pointer) + return json.UnmarshalUseNumber(r, pointer) } } case string: if paramsBytes := []byte(r); json.Valid(paramsBytes) { if rv, ok := pointer.(reflect.Value); ok { if rv.Kind() == reflect.Ptr { - return json.Unmarshal(paramsBytes, rv.Interface()) + return json.UnmarshalUseNumber(paramsBytes, rv.Interface()) } } else { - return json.Unmarshal(paramsBytes, pointer) + return json.UnmarshalUseNumber(paramsBytes, pointer) } } } diff --git a/util/gconv/gconv_time.go b/util/gconv/gconv_time.go index 006188985..20d9b77b7 100644 --- a/util/gconv/gconv_time.go +++ b/util/gconv/gconv_time.go @@ -13,7 +13,7 @@ import ( "github.com/gogf/gf/os/gtime" ) -// Time converts `i` to time.Time. +// Time converts `any` to time.Time. func Time(any interface{}, format ...string) time.Time { // It's already this type. if len(format) == 0 { @@ -27,9 +27,9 @@ func Time(any interface{}, format ...string) time.Time { return time.Time{} } -// Duration converts `i` to time.Duration. -// If `i` is string, then it uses time.ParseDuration to convert it. -// If `i` is numeric, then it converts `i` as nanoseconds. +// Duration converts `any` to time.Duration. +// If `any` is string, then it uses time.ParseDuration to convert it. +// If `any` is numeric, then it converts `any` as nanoseconds. func Duration(any interface{}) time.Duration { // It's already this type. if v, ok := any.(time.Duration); ok { @@ -43,19 +43,28 @@ func Duration(any interface{}) time.Duration { return time.Duration(Int64(any)) } -// GTime converts `i` to *gtime.Time. -// The parameter `format` can be used to specify the format of `i`. -// If no `format` given, it converts `i` using gtime.NewFromTimeStamp if `i` is numeric, -// or using gtime.StrToTime if `i` is string. +// GTime converts `any` to *gtime.Time. +// The parameter `format` can be used to specify the format of `any`. +// If no `format` given, it converts `any` using gtime.NewFromTimeStamp if `any` is numeric, +// or using gtime.StrToTime if `any` is string. func GTime(any interface{}, format ...string) *gtime.Time { if any == nil { return nil } + if v, ok := any.(apiGTime); ok { + return v.GTime(format...) + } // It's already this type. if len(format) == 0 { if v, ok := any.(*gtime.Time); ok { return v } + if t, ok := any.(time.Time); ok { + return gtime.New(t) + } + if t, ok := any.(*time.Time); ok { + return gtime.New(t) + } } s := String(any) if len(s) == 0 { diff --git a/util/gconv/gconv_z_unit_all_test.go b/util/gconv/gconv_z_unit_all_test.go index 2441e8e6b..a358c2362 100644 --- a/util/gconv/gconv_z_unit_all_test.go +++ b/util/gconv/gconv_z_unit_all_test.go @@ -647,13 +647,6 @@ func Test_Convert_All(t *testing.T) { }) } -func Test_Time_All(t *testing.T) { - gtest.C(t, func(t *gtest.T) { - t.AssertEQ(gconv.Duration(""), time.Duration(int64(0))) - t.AssertEQ(gconv.GTime(""), gtime.New()) - }) -} - func Test_Slice_All(t *testing.T) { gtest.C(t, func(t *gtest.T) { value := 123.456 diff --git a/util/gconv/gconv_z_unit_map_test.go b/util/gconv/gconv_z_unit_map_test.go index 55bca4f8d..f1676048c 100644 --- a/util/gconv/gconv_z_unit_map_test.go +++ b/util/gconv/gconv_z_unit_map_test.go @@ -317,6 +317,60 @@ func Test_MapDeep2(t *testing.T) { }) } +func Test_MapDeep3(t *testing.T) { + type Base struct { + Id int `c:"id"` + Date string `c:"date"` + } + type User struct { + UserBase Base `c:"base"` + Passport string `c:"passport"` + Password string `c:"password"` + Nickname string `c:"nickname"` + } + + gtest.C(t, func(t *gtest.T) { + user := &User{ + UserBase: Base{ + Id: 1, + Date: "2019-10-01", + }, + Passport: "john", + Password: "123456", + Nickname: "JohnGuo", + } + m := gconv.MapDeep(user) + t.Assert(m, g.Map{ + "base": g.Map{ + "id": user.UserBase.Id, + "date": user.UserBase.Date, + }, + "passport": user.Passport, + "password": user.Password, + "nickname": user.Nickname, + }) + }) + + gtest.C(t, func(t *gtest.T) { + user := &User{ + UserBase: Base{ + Id: 1, + Date: "2019-10-01", + }, + Passport: "john", + Password: "123456", + Nickname: "JohnGuo", + } + m := gconv.Map(user) + t.Assert(m, g.Map{ + "base": user.UserBase, + "passport": user.Passport, + "password": user.Password, + "nickname": user.Nickname, + }) + }) +} + func Test_MapDeepWithAttributeTag(t *testing.T) { type Ids struct { Id int `c:"id"` diff --git a/util/gconv/gconv_z_unit_slice_test.go b/util/gconv/gconv_z_unit_slice_test.go index 062b6fc17..c67f4bf3f 100644 --- a/util/gconv/gconv_z_unit_slice_test.go +++ b/util/gconv/gconv_z_unit_slice_test.go @@ -19,6 +19,8 @@ func Test_Slice(t *testing.T) { gtest.C(t, func(t *gtest.T) { value := 123.456 t.AssertEQ(gconv.Bytes("123"), []byte("123")) + t.AssertEQ(gconv.Bytes([]interface{}{1}), []byte{1}) + t.AssertEQ(gconv.Bytes([]interface{}{300}), []byte("[300]")) t.AssertEQ(gconv.Strings(value), []string{"123.456"}) t.AssertEQ(gconv.Ints(value), []int{123}) t.AssertEQ(gconv.Floats(value), []float64{123.456}) diff --git a/util/gconv/gconv_z_unit_struct_test.go b/util/gconv/gconv_z_unit_struct_test.go index 062c17d50..faa79d921 100644 --- a/util/gconv/gconv_z_unit_struct_test.go +++ b/util/gconv/gconv_z_unit_struct_test.go @@ -817,7 +817,7 @@ func Test_Struct_Complex(t *testing.T) { "errorMsg": null }` m := make(g.Map) - err := json.Unmarshal([]byte(data), &m) + err := json.UnmarshalUseNumber([]byte(data), &m) t.Assert(err, nil) model := new(XinYanModel) diff --git a/util/gconv/gconv_z_unit_time_test.go b/util/gconv/gconv_z_unit_time_test.go index 99d3e0115..f44acc035 100644 --- a/util/gconv/gconv_z_unit_time_test.go +++ b/util/gconv/gconv_z_unit_time_test.go @@ -7,6 +7,7 @@ package gconv_test import ( + "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/frame/g" "testing" "time" @@ -17,6 +18,11 @@ import ( ) func Test_Time(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.AssertEQ(gconv.Duration(""), time.Duration(int64(0))) + t.AssertEQ(gconv.GTime(""), gtime.New()) + }) + gtest.C(t, func(t *gtest.T) { s := "2011-10-10 01:02:03.456" t.AssertEQ(gconv.GTime(s), gtime.NewFromStr(s)) @@ -42,6 +48,15 @@ func Test_Time(t *testing.T) { t.AssertEQ(gconv.GTime(s), gtime.NewFromStr(s)) t.AssertEQ(gconv.Time(s), gtime.NewFromStr(s).Time) }) + gtest.C(t, func(t *gtest.T) { + t1 := gtime.NewFromStr("2021-05-21 05:04:51.206547+00") + t2 := gconv.GTime(gvar.New(t1)) + t3 := gvar.New(t1).GTime() + t.AssertEQ(t1, t2) + t.AssertEQ(t1.Local(), t2.Local()) + t.AssertEQ(t1, t3) + t.AssertEQ(t1.Local(), t3.Local()) + }) } func Test_Time_Slice_Attribute(t *testing.T) { diff --git a/util/gutil/gutil_comparator.go b/util/gutil/gutil_comparator.go index 3c902d601..39c364868 100644 --- a/util/gutil/gutil_comparator.go +++ b/util/gutil/gutil_comparator.go @@ -77,12 +77,28 @@ func ComparatorUint64(a, b interface{}) int { // ComparatorFloat32 provides a basic comparison on float32. func ComparatorFloat32(a, b interface{}) int { - return int(gconv.Float32(a) - gconv.Float32(b)) + aFloat := gconv.Float64(a) + bFloat := gconv.Float64(b) + if aFloat == bFloat { + return 0 + } + if aFloat > bFloat { + return 1 + } + return -1 } // ComparatorFloat64 provides a basic comparison on float64. func ComparatorFloat64(a, b interface{}) int { - return int(gconv.Float64(a) - gconv.Float64(b)) + aFloat := gconv.Float64(a) + bFloat := gconv.Float64(b) + if aFloat == bFloat { + return 0 + } + if aFloat > bFloat { + return 1 + } + return -1 } // ComparatorByte provides a basic comparison on byte. diff --git a/util/gutil/gutil_z_unit_comparator_test.go b/util/gutil/gutil_z_unit_comparator_test.go index 4538ba22b..db6ad9092 100755 --- a/util/gutil/gutil_z_unit_comparator_test.go +++ b/util/gutil/gutil_z_unit_comparator_test.go @@ -160,3 +160,20 @@ func Test_ComparatorTime(t *testing.T) { t.Assert(l, -1) }) } + +func Test_ComparatorFloat32OfFixed(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + t.Assert(gutil.ComparatorFloat32(0.1, 0.1), 0) + t.Assert(gutil.ComparatorFloat32(1.1, 2.1), -1) + t.Assert(gutil.ComparatorFloat32(2.1, 1.1), 1) + }) +} + +func Test_ComparatorFloat64OfFixed(t *testing.T) { + + gtest.C(t, func(t *gtest.T) { + t.Assert(gutil.ComparatorFloat64(0.1, 0.1), 0) + t.Assert(gutil.ComparatorFloat64(1.1, 2.1), -1) + t.Assert(gutil.ComparatorFloat64(2.1, 1.1), 1) + }) +} diff --git a/util/gvalid/gvalid.go b/util/gvalid/gvalid.go index 42351d218..5b390a125 100644 --- a/util/gvalid/gvalid.go +++ b/util/gvalid/gvalid.go @@ -8,6 +8,7 @@ package gvalid import ( + "context" "regexp" "strings" @@ -46,6 +47,7 @@ import ( // length format: length:min,max brief: Length between :min and :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. // min-length format: min-length:min brief: Length is equal or greater than :min. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. // max-length format: max-length:max brief: Length is equal or lesser than :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. +// size format: size:size brief: Length must be :size. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. // between format: between:min,max brief: Range between :min and :max. It supports both integer and float. // min format: min:min brief: Equal or greater than :min. It supports both integer and float. // max format: max:max brief: Equal or lesser than :max. It supports both integer and float. @@ -63,23 +65,32 @@ import ( // like: map[field] => string|map[rule]string type CustomMsg = map[string]interface{} +// apiNoValidation is an interface that marks current struct not validated by package `gvalid`. +type apiNoValidation interface { + NoValidation() +} + const ( - // regular expression pattern for single validation rule. - singleRulePattern = `^([\w-]+):{0,1}(.*)` - invalidRulesErrKey = "invalid_rules" - invalidParamsErrKey = "invalid_params" - invalidObjectErrKey = "invalid_object" + singleRulePattern = `^([\w-]+):{0,1}(.*)` // regular expression pattern for single validation rule. + internalRulesErrRuleName = "InvalidRules" // rule name for internal invalid rules validation error. + internalParamsErrRuleName = "InvalidParams" // rule name for internal invalid params validation error. + internalObjectErrRuleName = "InvalidObject" // rule name for internal invalid object validation error. + internalErrorMapKey = "__InternalError__" // error map key for internal errors. + internalDefaultRuleName = "__default__" // default rule name for i18n error message format if no i18n message found for specified error rule. + ruleMessagePrefixForI18n = "gf.gvalid.rule." // prefix string for each rule configuration in i18n content. + noValidationTagName = "nv" // no validation tag name for struct attribute. ) var ( - // defaultValidator is the default validator for package functions. - defaultValidator = New() + defaultValidator = New() // defaultValidator is the default validator for package functions. + structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. + aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. // all internal error keys. internalErrKeyMap = map[string]string{ - invalidRulesErrKey: invalidRulesErrKey, - invalidParamsErrKey: invalidParamsErrKey, - invalidObjectErrKey: invalidObjectErrKey, + internalRulesErrRuleName: internalRulesErrRuleName, + internalParamsErrRuleName: internalParamsErrRuleName, + internalObjectErrRuleName: internalObjectErrRuleName, } // regular expression object for single rule // which is compiled just once and of repeatable usage. @@ -133,6 +144,7 @@ var ( "length": {}, "min-length": {}, "max-length": {}, + "size": {}, "between": {}, "min": {}, "max": {}, @@ -158,9 +170,58 @@ var ( "off": {}, "no": {}, } + // defaultMessages is the default error messages. + // Note that these messages are synchronized from ./i18n/en/validation.toml . + defaultMessages = map[string]string{ + "required": "The :attribute field is required", + "required-if": "The :attribute field is required", + "required-unless": "The :attribute field is required", + "required-with": "The :attribute field is required", + "required-with-all": "The :attribute field is required", + "required-without": "The :attribute field is required", + "required-without-all": "The :attribute field is required", + "date": "The :attribute value is not a valid date", + "date-format": "The :attribute value does not match the format :format", + "email": "The :attribute value must be a valid email address", + "phone": "The :attribute value must be a valid phone number", + "telephone": "The :attribute value must be a valid telephone number", + "passport": "The :attribute value is not a valid passport format", + "password": "The :attribute value is not a valid passport format", + "password2": "The :attribute value is not a valid passport format", + "password3": "The :attribute value is not a valid passport format", + "postcode": "The :attribute value is not a valid passport format", + "resident-id": "The :attribute value is not a valid resident id number", + "bank-card": "The :attribute value must be a valid bank card number", + "qq": "The :attribute value must be a valid QQ number", + "ip": "The :attribute value must be a valid IP address", + "ipv4": "The :attribute value must be a valid IPv4 address", + "ipv6": "The :attribute value must be a valid IPv6 address", + "mac": "The :attribute value must be a valid MAC address", + "url": "The :attribute value must be a valid URL address", + "domain": "The :attribute value must be a valid domain format", + "length": "The :attribute value length must be between :min and :max", + "min-length": "The :attribute value length must be equal or greater than :min", + "max-length": "The :attribute value length must be equal or lesser than :max", + "size": "The :attribute value length must be :size", + "between": "The :attribute value must be between :min and :max", + "min": "The :attribute value must be equal or greater than :min", + "max": "The :attribute value must be equal or lesser than :max", + "json": "The :attribute value must be a valid JSON string", + "xml": "The :attribute value must be a valid XML string", + "array": "The :attribute value must be an array", + "integer": "The :attribute value must be an integer", + "float": "The :attribute value must be a float", + "boolean": "The :attribute value field must be true or false", + "same": "The :attribute value must be the same as field :field", + "different": "The :attribute value must be different from field :field", + "in": "The :attribute value is not in acceptable range", + "not-in": "The :attribute value is not in acceptable range", + "regex": "The :attribute value is invalid", + internalDefaultRuleName: "The :attribute value is invalid", + } ) -// Check checks single value with specified rules. +// CheckValue checks single value with specified rules. // It returns nil if successful validation. // // The parameter `value` can be any type of variable, which will be converted to string @@ -170,8 +231,12 @@ var ( // string/map/struct/*struct. // The optional parameter `params` specifies the extra validation parameters for some rules // like: required-*、same、different, etc. -func Check(value interface{}, rules string, messages interface{}, params ...interface{}) *Error { - return defaultValidator.Check(value, rules, messages, params...) +func CheckValue(ctx context.Context, value interface{}, rules string, messages interface{}, params ...interface{}) Error { + var data interface{} + if len(params) > 0 { + data = params[0] + } + return defaultValidator.Ctx(ctx).Rules(rules).Data(data).Messages(messages).CheckValue(value) } // CheckMap validates map and returns the error result. It returns nil if with successful validation. @@ -179,18 +244,40 @@ func Check(value interface{}, rules string, messages interface{}, params ...inte // The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result // if `rules` is type of []string. // The optional parameter `messages` specifies the custom error messages for specified keys and rules. -func CheckMap(params interface{}, rules interface{}, messages ...CustomMsg) *Error { - return defaultValidator.CheckMap(params, rules, messages...) +func CheckMap(ctx context.Context, params interface{}, rules interface{}, messages ...CustomMsg) Error { + var customErrorMessages CustomMsg + if len(messages) > 0 { + customErrorMessages = messages[0] + } + return defaultValidator.Ctx(ctx).Rules(rules).Messages(customErrorMessages).CheckMap(params) } -// CheckStruct validates strcut and returns the error result. +// CheckStruct validates struct and returns the error result. // // The parameter `object` should be type of struct/*struct. // The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result // if `rules` is type of []string. // The optional parameter `messages` specifies the custom error messages for specified keys and rules. -func CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) *Error { - return defaultValidator.CheckStruct(object, rules, messages...) +func CheckStruct(ctx context.Context, object interface{}, rules interface{}, messages ...CustomMsg) Error { + var customErrorMessages CustomMsg + if len(messages) > 0 { + customErrorMessages = messages[0] + } + return defaultValidator.Ctx(ctx).Rules(rules).Messages(customErrorMessages).CheckStruct(object) +} + +// CheckStructWithData validates struct with given parameter map and returns the error result. +// +// The parameter `object` should be type of struct/*struct. +// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result +// if `rules` is type of []string. +// The optional parameter `messages` specifies the custom error messages for specified keys and rules. +func CheckStructWithData(ctx context.Context, object interface{}, data interface{}, rules interface{}, messages ...CustomMsg) Error { + var customErrorMessages CustomMsg + if len(messages) > 0 { + customErrorMessages = messages[0] + } + return defaultValidator.Ctx(ctx).Data(data).Rules(rules).Messages(customErrorMessages).CheckStruct(object) } // parseSequenceTag parses one sequence tag to field, rule and error message. diff --git a/util/gvalid/gvalid_custom_rule.go b/util/gvalid/gvalid_custom_rule.go index 83fd4c2bb..d1e3589bb 100644 --- a/util/gvalid/gvalid_custom_rule.go +++ b/util/gvalid/gvalid_custom_rule.go @@ -6,13 +6,15 @@ package gvalid +import "context" + // 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 `params` specifies all the parameters that needs. You can ignore parameter `params` if -// you do not really need it in your custom validation rule. -type RuleFunc func(rule string, value interface{}, message string, params map[string]interface{}) error +// The parameter `data` specifies the `data` which is passed to the Validator. It might be 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 var ( // customRuleFuncMap stores the custom rule functions. diff --git a/util/gvalid/gvalid_error.go b/util/gvalid/gvalid_error.go index 49ebeb510..08c3b498e 100644 --- a/util/gvalid/gvalid_error.go +++ b/util/gvalid/gvalid_error.go @@ -9,47 +9,60 @@ package gvalid import ( "errors" "github.com/gogf/gf/text/gregex" + "github.com/gogf/gf/text/gstr" "strings" ) // Error is the validation error for validation result. -type Error struct { - rules []string // Rules by sequence, which is used for keeping error sequence. - errors ErrorMap // Error map. - firstKey string // The first error rule key(nil in default). - firstItem map[string]string // The first error rule value(nil in default). +type Error interface { + 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 + String() string + Strings() (errs []string) } -// ErrorMap is the validation error map: -// map[field]map[rule]message -type ErrorMap map[string]map[string]string +// validationError is the validation error for validation result. +type validationError struct { + rules []string // 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). +} // newError creates and returns a validation error. -func newError(rules []string, errors map[string]map[string]string) *Error { +func newError(rules []string, errors map[string]map[string]string) *validationError { for field, m := range errors { for k, v := range m { v = strings.Replace(v, ":attribute", field, -1) - m[k], _ = gregex.ReplaceString(`\s{2,}`, ` `, v) + v, _ = gregex.ReplaceString(`\s{2,}`, ` `, v) + v = gstr.Trim(v) + m[k] = v } errors[field] = m } - return &Error{ + return &validationError{ rules: rules, errors: errors, } } // newErrorStr creates and returns a validation error by string. -func newErrorStr(key, err string) *Error { +func newErrorStr(key, err string) *validationError { return newError(nil, map[string]map[string]string{ - "__gvalid__": { + internalErrorMapKey: { key: err, }, }) } // Map returns the first error message as map. -func (e *Error) Map() map[string]string { +func (e *validationError) Map() map[string]string { if e == nil { return map[string]string{} } @@ -58,15 +71,43 @@ func (e *Error) Map() map[string]string { } // Maps returns all error messages as map. -func (e *Error) Maps() ErrorMap { +func (e *validationError) Maps() map[string]map[string]string { if e == nil { return nil } return e.errors } +// 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) { + if e == nil { + return []map[string]map[string]string{} + } + items = make([]map[string]map[string]string, 0) + // By sequence. + if len(e.rules) > 0 { + for _, v := range e.rules { + name, _, _ := parseSequenceTag(v) + if errorItemMap, ok := e.errors[name]; ok { + items = append(items, map[string]map[string]string{ + name: errorItemMap, + }) + } + } + return items + } + // No sequence. + for name, errorRuleMap := range e.errors { + items = append(items, map[string]map[string]string{ + name: errorRuleMap, + }) + } + return +} + // FirstItem returns the field name and error messages for the first validation rule error. -func (e *Error) FirstItem() (key string, messages map[string]string) { +func (e *validationError) FirstItem() (key string, messages map[string]string) { if e == nil { return "", map[string]string{} } @@ -77,10 +118,10 @@ func (e *Error) FirstItem() (key string, messages map[string]string) { if len(e.rules) > 0 { for _, v := range e.rules { name, _, _ := parseSequenceTag(v) - if m, ok := e.errors[name]; ok { + if errorItemMap, ok := e.errors[name]; ok { e.firstKey = name - e.firstItem = m - return name, m + e.firstItem = errorItemMap + return name, errorItemMap } } } @@ -94,28 +135,28 @@ func (e *Error) FirstItem() (key string, messages map[string]string) { } // FirstRule returns the first error rule and message string. -func (e *Error) FirstRule() (rule string, err string) { +func (e *validationError) FirstRule() (rule string, err string) { if e == nil { return "", "" } // By sequence. if len(e.rules) > 0 { for _, v := range e.rules { - name, rule, _ := parseSequenceTag(v) - if m, ok := e.errors[name]; ok { - for _, rule := range strings.Split(rule, "|") { - array := strings.Split(rule, ":") - rule = strings.TrimSpace(array[0]) - if err, ok := m[rule]; ok { - return rule, err + name, ruleStr, _ := parseSequenceTag(v) + if errorItemMap, ok := e.errors[name]; ok { + for _, ruleItem := range strings.Split(ruleStr, "|") { + array := strings.Split(ruleItem, ":") + ruleItem = strings.TrimSpace(array[0]) + if err, ok = errorItemMap[ruleItem]; ok { + return ruleItem, err } } } } } // No sequence. - for _, m := range e.errors { - for k, v := range m { + for _, errorItemMap := range e.errors { + for k, v := range errorItemMap { return k, v } } @@ -124,7 +165,7 @@ func (e *Error) FirstRule() (rule string, err string) { // FirstString returns the first error message as string. // Note that the returned message might be different if it has no sequence. -func (e *Error) FirstString() (err string) { +func (e *validationError) FirstString() (err string) { if e == nil { return "" } @@ -133,7 +174,7 @@ func (e *Error) FirstString() (err string) { } // Current is alis of FirstString, which implements interface gerror.ApiCurrent. -func (e *Error) Current() error { +func (e *validationError) Current() error { if e == nil { return nil } @@ -142,7 +183,7 @@ func (e *Error) Current() error { } // String returns all error messages as string, multiple error messages joined using char ';'. -func (e *Error) String() string { +func (e *validationError) String() string { if e == nil { return "" } @@ -150,7 +191,7 @@ func (e *Error) String() string { } // Error implements interface of error.Error. -func (e *Error) Error() string { +func (e *validationError) Error() string { if e == nil { return "" } @@ -158,7 +199,7 @@ func (e *Error) Error() string { } // Strings returns all error messages as string array. -func (e *Error) Strings() (errs []string) { +func (e *validationError) Strings() (errs []string) { if e == nil { return []string{} } @@ -166,18 +207,18 @@ func (e *Error) Strings() (errs []string) { // By sequence. if len(e.rules) > 0 { for _, v := range e.rules { - name, rule, _ := parseSequenceTag(v) - if m, ok := e.errors[name]; ok { + name, ruleStr, _ := parseSequenceTag(v) + if errorItemMap, ok := e.errors[name]; ok { // validation error checks. - for _, rule := range strings.Split(rule, "|") { - rule = strings.TrimSpace(strings.Split(rule, ":")[0]) - if err, ok := m[rule]; ok { + for _, ruleItem := range strings.Split(ruleStr, "|") { + ruleItem = strings.TrimSpace(strings.Split(ruleItem, ":")[0]) + if err, ok := errorItemMap[ruleItem]; ok { errs = append(errs, err) } } // internal error checks. for k, _ := range internalErrKeyMap { - if err, ok := m[k]; ok { + if err, ok := errorItemMap[k]; ok { errs = append(errs, err) } } @@ -186,8 +227,8 @@ func (e *Error) Strings() (errs []string) { return errs } // No sequence. - for _, m := range e.errors { - for _, err := range m { + for _, errorItemMap := range e.errors { + for _, err := range errorItemMap { errs = append(errs, err) } } diff --git a/util/gvalid/gvalid_validator.go b/util/gvalid/gvalid_validator.go index 49c8505aa..e46c70295 100644 --- a/util/gvalid/gvalid_validator.go +++ b/util/gvalid/gvalid_validator.go @@ -6,14 +6,31 @@ package gvalid -// Validator is the validation manager. +import ( + "context" + "github.com/gogf/gf/i18n/gi18n" +) + +// Validator is the validation manager for chaining operations. type Validator struct { - i18nLang string // I18n language. + ctx context.Context // Context containing custom context variables. + i18nManager *gi18n.Manager // I18n manager for error message translation. + key string // Single validation key. + value interface{} // Single validation value. + data interface{} // Validation data, which is usually a map. + rules interface{} // Custom validation data. + messages interface{} // Custom validation error messages, which can be string or type of CustomMsg. + ruleFuncMap map[string]RuleFunc // ruleFuncMap stores custom rule functions for current Validator. + useDataInsteadOfObjectAttributes bool // Using `data` as its validation source instead of attribute values from `Object`. } // New creates and returns a new Validator. func New() *Validator { - return &Validator{} + return &Validator{ + ctx: context.TODO(), // Initialize an empty context. + i18nManager: gi18n.Instance(), // Use default i18n manager. + ruleFuncMap: make(map[string]RuleFunc), // Custom rule function storing map. + } } // Clone creates and returns a new Validator which is a shallow copy of current one. @@ -23,9 +40,67 @@ func (v *Validator) Clone() *Validator { return newValidator } -// I18n is a chaining operation function which sets the I18n language for next validation. -func (v *Validator) I18n(language string) *Validator { +// I18n sets the i18n manager for the validator. +func (v *Validator) I18n(i18nManager *gi18n.Manager) *Validator { newValidator := v.Clone() - newValidator.i18nLang = language + newValidator.i18nManager = i18nManager return newValidator } + +// Ctx is a chaining operation function, which sets the context for next validation. +func (v *Validator) Ctx(ctx context.Context) *Validator { + newValidator := v.Clone() + newValidator.ctx = ctx + return newValidator +} + +// Data is a chaining operation function, which sets validation data for current operation. +// The parameter `data` usually be type of map, which specifies the parameter map used in validation. +// Calling this function also sets `useDataInsteadOfObjectAttributes` true no mather the `data` is nil or not. +func (v *Validator) Data(data interface{}) *Validator { + newValidator := v.Clone() + newValidator.data = data + newValidator.useDataInsteadOfObjectAttributes = true + return newValidator +} + +// Rules is a chaining operation function, which sets custom validation rules for current operation. +func (v *Validator) Rules(rules interface{}) *Validator { + newValidator := v.Clone() + newValidator.rules = rules + return newValidator +} + +// Messages is a chaining operation function, which sets custom error messages for current operation. +// The parameter `messages` can be type of string/[]string/map[string]string. It supports sequence in error result +// if `rules` is type of []string. +func (v *Validator) Messages(messages interface{}) *Validator { + newValidator := v.Clone() + newValidator.messages = messages + return newValidator +} + +// RuleFunc registers one custom rule function to current Validator. +func (v *Validator) RuleFunc(rule string, f RuleFunc) *Validator { + newValidator := v.Clone() + newValidator.ruleFuncMap[rule] = f + return newValidator +} + +// RuleFuncMap registers multiple custom rule functions to current Validator. +func (v *Validator) RuleFuncMap(m map[string]RuleFunc) *Validator { + newValidator := v.Clone() + for k, v := range m { + newValidator.ruleFuncMap[k] = v + } + return newValidator +} + +// getRuleFunc retrieves and returns the custom rule function for specified rule. +func (v *Validator) getRuleFunc(rule string) RuleFunc { + ruleFunc := v.ruleFuncMap[rule] + if ruleFunc == nil { + ruleFunc = customRuleFuncMap[rule] + } + return ruleFunc +} diff --git a/util/gvalid/gvalid_validator_check_map.go b/util/gvalid/gvalid_validator_check_map.go index 3229e2752..e7e88d67e 100644 --- a/util/gvalid/gvalid_validator_check_map.go +++ b/util/gvalid/gvalid_validator_check_map.go @@ -12,22 +12,23 @@ import ( ) // CheckMap validates map and returns the error result. It returns nil if with successful validation. -// -// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result -// if `rules` is type of []string. -// The optional parameter `messages` specifies the custom error messages for specified keys and rules. -func (v *Validator) CheckMap(params interface{}, rules interface{}, messages ...CustomMsg) *Error { +// The parameter `params` should be type of map. +func (v *Validator) CheckMap(params interface{}) Error { + return v.doCheckMap(params) +} + +func (v *Validator) doCheckMap(params interface{}) Error { // If there's no validation rules, it does nothing and returns quickly. - if params == nil || rules == nil { + if params == nil || v.rules == nil { return nil } var ( checkRules = make(map[string]string) customMsgs = make(CustomMsg) errorRules = make([]string, 0) - errorMaps = make(ErrorMap) + errorMaps = make(map[string]map[string]string) ) - switch v := rules.(type) { + switch v := v.rules.(type) { // Sequence tag: []sequence tag // Sequence has order for error results. case []string: @@ -72,17 +73,17 @@ func (v *Validator) CheckMap(params interface{}, rules interface{}, messages ... data := gconv.Map(params) if data == nil { return newErrorStr( - "invalid_params", + internalParamsErrRuleName, "invalid params type: convert to map failed", ) } - if len(messages) > 0 && len(messages[0]) > 0 { + if msg, ok := v.messages.(CustomMsg); ok && len(msg) > 0 { if len(customMsgs) > 0 { - for k, v := range messages[0] { + for k, v := range msg { customMsgs[k] = v } } else { - customMsgs = messages[0] + customMsgs = msg } } var value interface{} @@ -95,7 +96,7 @@ func (v *Validator) CheckMap(params interface{}, rules interface{}, messages ... value = v } // It checks each rule and its value in loop. - if e := v.doCheck(key, value, rule, customMsgs[key], data); e != nil { + if e := v.doCheckValue(key, value, rule, customMsgs[key], params, data); e != nil { _, item := e.FirstItem() // =========================================================== // Only in map and struct validations, if value is nil or empty @@ -111,7 +112,7 @@ func (v *Validator) CheckMap(params interface{}, rules interface{}, messages ... break } // Custom rules are also required in default. - if _, ok := customRuleFuncMap[k]; ok { + if f := v.getRuleFunc(k); f != nil { required = true break } diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 3180806f4..58426abfb 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -9,57 +9,68 @@ package gvalid import ( "github.com/gogf/gf/internal/structs" "github.com/gogf/gf/util/gconv" - "reflect" + "github.com/gogf/gf/util/gutil" "strings" ) -var ( - structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. - aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. -) - // CheckStruct validates struct and returns the error result. -// // The parameter `object` should be type of struct/*struct. -// The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result -// if `rules` is type of []string. -// The optional parameter `messages` specifies the custom error messages for specified keys and rules. -func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) *Error { +func (v *Validator) CheckStruct(object interface{}) Error { + return v.doCheckStruct(object) +} + +func (v *Validator) doCheckStruct(object interface{}) Error { var ( - errorMaps = make(ErrorMap) // Returned error. + errorMaps = make(map[string]map[string]string) // Returning error. + fieldToAliasNameMap = make(map[string]string) // Field name to alias name map. ) - mapField, err := structs.FieldMap(object, aliasNameTagPriority) + fieldMap, err := structs.FieldMap(object, aliasNameTagPriority, true) if err != nil { - return newErrorStr("invalid_object", err.Error()) + return newErrorStr(internalObjectErrRuleName, err.Error()) } - // It checks the struct recursively the its attribute is also a struct. - for _, field := range mapField { - if field.OriginalKind() == reflect.Struct { - if err := v.CheckStruct(field.Value, rules, messages...); err != nil { + // It checks the struct recursively the its attribute is an embedded struct. + for _, field := range fieldMap { + if field.IsEmbedded() { + // No validation interface implements check. + if _, ok := field.Value.Interface().(apiNoValidation); ok { + continue + } + if _, ok := field.TagLookup(noValidationTagName); ok { + continue + } + if err := v.doCheckStruct(field.Value); err != nil { // It merges the errors into single error map. - for k, m := range err.errors { + for k, m := range err.(*validationError).errors { errorMaps[k] = m } } + } else { + if field.TagValue != "" { + fieldToAliasNameMap[field.Name()] = field.TagValue + } } } // It here must use structs.TagFields not structs.FieldMap to ensure error sequence. tagField, err := structs.TagFields(object, structTagPriority) if err != nil { - return newErrorStr("invalid_object", err.Error()) + return newErrorStr(internalObjectErrRuleName, err.Error()) } // If there's no struct tag and validation rules, it does nothing and returns quickly. - if len(tagField) == 0 && rules == nil { + if len(tagField) == 0 && v.messages == nil { return nil } + var ( - params = make(map[string]interface{}) - checkRules = make(map[string]string) - customMessage = make(CustomMsg) - fieldAliases = make(map[string]string) // Alias names for `messages` overwriting struct tag names. - errorRules = make([]string, 0) // Sequence rules. + inputParamMap map[string]interface{} + checkRules = make(map[string]string) + customMessage = make(CustomMsg) + checkValueData = v.data + errorRules = make([]string, 0) // Sequence rules. ) - switch v := rules.(type) { + if checkValueData == nil { + checkValueData = object + } + switch v := v.rules.(type) { // Sequence tag: []sequence tag // Sequence has order for error results. case []string: @@ -102,24 +113,52 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages if len(tagField) == 0 && len(checkRules) == 0 { return nil } - // Checks and extends the parameters map with struct alias tag. - for nameOrTag, field := range mapField { - params[nameOrTag] = field.Value.Interface() - params[field.Name()] = field.Value.Interface() + // Input parameter map handling. + if v.data == nil || !v.useDataInsteadOfObjectAttributes { + inputParamMap = make(map[string]interface{}) + } else { + inputParamMap = gconv.Map(v.data) } + // Checks and extends the parameters map with struct alias tag. + if !v.useDataInsteadOfObjectAttributes { + for nameOrTag, field := range fieldMap { + inputParamMap[nameOrTag] = field.Value.Interface() + if nameOrTag != field.Name() { + inputParamMap[field.Name()] = field.Value.Interface() + } + } + } + // Merge the custom validation rules with rules in struct tag. + // The custom rules has the most high priority that can overwrite the struct tag rules. for _, field := range tagField { - fieldName := field.Name() - // sequence tag == struct tag - // The name here is alias of field name. - name, rule, msg := parseSequenceTag(field.TagValue) + var ( + fieldName = field.Name() // Attribute name. + name, rule, msg = parseSequenceTag(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only. + ) if len(name) == 0 { - name = fieldName + if v, ok := fieldToAliasNameMap[fieldName]; ok { + // It uses alias name of the attribute if its alias name tag exists. + name = v + } else { + // It or else uses the attribute name directly. + name = fieldName + } } else { - fieldAliases[fieldName] = name + // It uses the alias name from validation rule. + fieldToAliasNameMap[fieldName] = name } // It here extends the params map using alias names. - if _, ok := params[name]; !ok { - params[name] = field.Value.Interface() + // Note that the variable `name` might be alias name or attribute name. + if _, ok := inputParamMap[name]; !ok { + if !v.useDataInsteadOfObjectAttributes { + inputParamMap[name] = field.Value.Interface() + } else { + if name != fieldName { + if foundKey, foundValue := gutil.MapPossibleItemByKey(inputParamMap, fieldName); foundKey != "" { + inputParamMap[name] = foundValue + } + } + } } if _, ok := checkRules[name]; !ok { if _, ok := checkRules[fieldName]; ok { @@ -132,7 +171,7 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages } errorRules = append(errorRules, name+"@"+rule) } else { - // The passed rules can overwrite the rules in struct tag. + // The input rules can overwrite the rules in struct tag. continue } if len(msg) > 0 { @@ -160,9 +199,9 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages // Custom error messages, // which have the most priority than `rules` and struct tag. - if len(messages) > 0 && len(messages[0]) > 0 { - for k, v := range messages[0] { - if a, ok := fieldAliases[k]; ok { + if msg, ok := v.messages.(CustomMsg); ok && len(msg) > 0 { + for k, v := range msg { + if a, ok := fieldToAliasNameMap[k]; ok { // Overwrite the key of field name. customMessage[a] = v } else { @@ -174,12 +213,9 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages // The following logic is the same as some of CheckMap. var value interface{} for key, rule := range checkRules { - value = nil - if v, ok := params[key]; ok { - value = v - } + _, value = gutil.MapPossibleItemByKey(inputParamMap, key) // It checks each rule and its value in loop. - if e := v.doCheck(key, value, rule, customMessage[key], params); e != nil { + if e := v.doCheckValue(key, value, rule, customMessage[key], checkValueData, inputParamMap); e != nil { _, item := e.FirstItem() // =================================================================== // Only in map and struct validations, if value is nil or empty string @@ -195,7 +231,7 @@ func (v *Validator) CheckStruct(object interface{}, rules interface{}, messages break } // Custom rules are also required in default. - if _, ok := customRuleFuncMap[k]; ok { + if f := v.getRuleFunc(k); f != nil { required = true break } diff --git a/util/gvalid/gvalid_validator_check.go b/util/gvalid/gvalid_validator_check_value.go similarity index 81% rename from util/gvalid/gvalid_validator_check.go rename to util/gvalid/gvalid_validator_check_value.go index 831930df1..a72cee3b3 100644 --- a/util/gvalid/gvalid_validator_check.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -8,15 +8,17 @@ package gvalid import ( "errors" + "strconv" + "strings" + "time" + "github.com/gogf/gf/internal/json" "github.com/gogf/gf/net/gipv4" "github.com/gogf/gf/net/gipv6" "github.com/gogf/gf/os/gtime" "github.com/gogf/gf/text/gregex" "github.com/gogf/gf/util/gconv" - "strconv" - "strings" - "time" + "github.com/gogf/gf/util/gutil" ) type apiTime interface { @@ -24,22 +26,27 @@ type apiTime interface { IsZero() bool } -// Check checks single value with specified rules. +// CheckValue checks single value with specified rules. // It returns nil if successful validation. -// -// The parameter `value` can be any type of variable, which will be converted to string -// for validation. -// The parameter `rules` can be one or more rules, multiple rules joined using char '|'. -// The parameter `messages` specifies the custom error messages, which can be type of: -// string/map/struct/*struct. -// The optional parameter `params` specifies the extra validation parameters for some rules -// like: required-*、same、different, etc. -func (v *Validator) Check(value interface{}, rules string, messages interface{}, params ...interface{}) *Error { - return v.doCheck("", value, rules, messages, params...) +func (v *Validator) CheckValue(value interface{}) Error { + return v.doCheckValue("", value, gconv.String(v.rules), v.messages, v.data, gconv.Map(v.data)) } -// doCheck does the really rules validation for single key-value. -func (v *Validator) doCheck(key string, value interface{}, rules string, messages interface{}, params ...interface{}) *Error { +// doCheckSingleValue does the really rules validation for single key-value. +// +// The parameter `rules` specifies the validation rules string, like "required", "required|between:1,100", etc. +// The parameter `value` specifies the value for this rules to be validated. +// The parameter `messages` specifies the custom error messages for this rule, which is usually type of map/slice. +// The parameter `dataRaw` specifies the `raw data` which is passed to the Validator. It might be type of map/struct or a nil value. +// The parameter `dataMap` specifies the map that is converted from `dataRaw`. It is usually used internally +func (v *Validator) doCheckValue( + key string, + value interface{}, + rules string, + messages interface{}, + dataRaw interface{}, + dataMap map[string]interface{}, +) Error { // If there's no validation rules, it does nothing and returns quickly. if rules == "" { return nil @@ -47,12 +54,8 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message // It converts value to string and then does the validation. var ( // Do not trim it as the space is also part of the value. - data = make(map[string]interface{}) errorMsgArray = make(map[string]string) ) - if len(params) > 0 { - data = gconv.Map(params[0]) - } // Custom error messages handling. var ( msgArray = make([]string, 0) @@ -72,14 +75,14 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message for i := 0; ; { array := strings.Split(ruleItems[i], ":") _, ok := allSupportedRules[array[0]] - if !ok && customRuleFuncMap[array[0]] == nil { + if !ok && v.getRuleFunc(array[0]) == nil { if i > 0 && ruleItems[i-1][:5] == "regex" { ruleItems[i-1] += "|" + ruleItems[i] ruleItems = append(ruleItems[:i], ruleItems[i+1:]...) } else { return newErrorStr( - invalidRulesErrKey, - invalidRulesErrKey+": "+rules, + internalRulesErrRuleName, + internalRulesErrRuleName+": "+rules, ) } } else { @@ -91,26 +94,26 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message } for index := 0; index < len(ruleItems); { var ( - err error - match = false - results = ruleRegex.FindStringSubmatch(ruleItems[index]) - ruleKey = strings.TrimSpace(results[1]) - rulePattern = strings.TrimSpace(results[2]) + err error + match = false + results = ruleRegex.FindStringSubmatch(ruleItems[index]) + ruleKey = strings.TrimSpace(results[1]) + rulePattern = strings.TrimSpace(results[2]) + customRuleFunc RuleFunc ) if len(msgArray) > index { customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index]) } - if f, ok := customRuleFuncMap[ruleKey]; ok { + // Custom rule handling. + // 1. It firstly checks and uses the custom registered rules functions in the current Validator. + // 2. It secondly checks and uses the globally registered rules functions. + // 3. It finally checks and uses the build-in rules functions. + customRuleFunc = v.getRuleFunc(ruleKey) + if customRuleFunc != nil { // It checks custom validation rules with most priority. - var ( - dataMap map[string]interface{} - message = v.getErrorMessageByRule(ruleKey, customMsgMap) - ) - if len(params) > 0 { - dataMap = gconv.Map(params[0]) - } - if err := f(ruleItems[index], value, message, dataMap); err != nil { + message := v.getErrorMessageByRule(ruleKey, customMsgMap) + if err := customRuleFunc(v.ctx, ruleItems[index], value, message, dataRaw); err != nil { match = false errorMsgArray[ruleKey] = err.Error() } else { @@ -118,7 +121,7 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message } } else { // It checks build-in validation rules if there's no custom rule. - match, err = v.doCheckBuildInRules(index, value, ruleKey, rulePattern, ruleItems, data, customMsgMap) + match, err = v.doCheckBuildInRules(index, value, ruleKey, rulePattern, ruleItems, dataMap, customMsgMap) if !match && err != nil { errorMsgArray[ruleKey] = err.Error() } @@ -135,7 +138,7 @@ func (v *Validator) doCheck(key string, value interface{}, rules string, message index++ } if len(errorMsgArray) > 0 { - return newError([]string{rules}, ErrorMap{ + return newError([]string{rules}, map[string]map[string]string{ key: errorMsgArray, }) } @@ -169,7 +172,8 @@ func (v *Validator) doCheckBuildInRules( case "length", "min-length", - "max-length": + "max-length", + "size": if msg := v.checkLength(valueStr, ruleKey, rulePattern, customMsgMap); msg != "" { return match, errors.New(msg) } else { @@ -204,16 +208,7 @@ func (v *Validator) doCheckBuildInRules( if v, ok := value.(apiTime); ok { return !v.IsZero(), nil } - // Standard date string, which must contain char '-' or '.'. - if _, err := gtime.StrToTime(valueStr); err == nil { - match = true - break - } - // Date that not contains char '-' or '.'. - if _, err := gtime.StrToTime(valueStr, "Ymd"); err == nil { - match = true - break - } + match = gregex.IsMatchString(`\d{4}[\.\-\_/]{0,1}\d{2}[\.\-\_/]{0,1}\d{2}`, valueStr) // Date rule with specified format. case "date-format": @@ -232,8 +227,9 @@ func (v *Validator) doCheckBuildInRules( // Values of two fields should be equal as string. case "same": - if v, ok := dataMap[rulePattern]; ok { - if strings.Compare(valueStr, gconv.String(v)) == 0 { + _, foundValue := gutil.MapPossibleItemByKey(dataMap, rulePattern) + if foundValue != nil { + if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { match = true } } @@ -247,8 +243,9 @@ func (v *Validator) doCheckBuildInRules( // Values of two fields should not be equal as string. case "different": match = true - if v, ok := dataMap[rulePattern]; ok { - if strings.Compare(valueStr, gconv.String(v)) == 0 { + _, foundValue := gutil.MapPossibleItemByKey(dataMap, rulePattern) + if foundValue != nil { + if strings.Compare(valueStr, gconv.String(foundValue)) == 0 { match = false } } @@ -302,6 +299,7 @@ func (v *Validator) doCheckBuildInRules( // 16x, 19x case "phone": match = gregex.IsMatchString(`^13[\d]{9}$|^14[5,7]{1}\d{8}$|^15[^4]{1}\d{8}$|^16[\d]{9}$|^17[0,2,3,5,6,7,8]{1}\d{8}$|^18[\d]{9}$|^19[\d]{9}$`, valueStr) + // Loose mobile phone number verification(宽松的手机号验证) // As long as the 11 digit numbers beginning with // 13, 14, 15, 16, 17, 18, 19 can pass the verification (只要满足 13、14、15、16、17、18、19开头的11位数字都可以通过验证) diff --git a/util/gvalid/gvalid_validator_message.go b/util/gvalid/gvalid_validator_message.go index 56bf17251..7701e5f96 100644 --- a/util/gvalid/gvalid_validator_message.go +++ b/util/gvalid/gvalid_validator_message.go @@ -6,77 +6,29 @@ package gvalid -import ( - "fmt" - "github.com/gogf/gf/i18n/gi18n" -) - -// defaultMessages is the default error messages. -// Note that these messages are synchronized from ./i18n/en/validation.toml . -var defaultMessages = map[string]string{ - "required": "The :attribute field is required", - "required-if": "The :attribute field is required", - "required-unless": "The :attribute field is required", - "required-with": "The :attribute field is required", - "required-with-all": "The :attribute field is required", - "required-without": "The :attribute field is required", - "required-without-all": "The :attribute field is required", - "date": "The :attribute value is not a valid date", - "date-format": "The :attribute value does not match the format :format", - "email": "The :attribute value must be a valid email address", - "phone": "The :attribute value must be a valid phone number", - "telephone": "The :attribute value must be a valid telephone number", - "passport": "The :attribute value is not a valid passport format", - "password": "The :attribute value is not a valid passport format", - "password2": "The :attribute value is not a valid passport format", - "password3": "The :attribute value is not a valid passport format", - "postcode": "The :attribute value is not a valid passport format", - "resident-id": "The :attribute value is not a valid resident id number", - "bank-card": "The :attribute value must be a valid bank card number", - "qq": "The :attribute value must be a valid QQ number", - "ip": "The :attribute value must be a valid IP address", - "ipv4": "The :attribute value must be a valid IPv4 address", - "ipv6": "The :attribute value must be a valid IPv6 address", - "mac": "The :attribute value must be a valid MAC address", - "url": "The :attribute value must be a valid URL address", - "domain": "The :attribute value must be a valid domain format", - "length": "The :attribute value length must be between :min and :max", - "min-length": "The :attribute value length must be equal or greater than :min", - "max-length": "The :attribute value length must be equal or lesser than :max", - "between": "The :attribute value must be between :min and :max", - "min": "The :attribute value must be equal or greater than :min", - "max": "The :attribute value must be equal or lesser than :max", - "json": "The :attribute value must be a valid JSON string", - "xml": "The :attribute value must be a valid XML string", - "array": "The :attribute value must be an array", - "integer": "The :attribute value must be an integer", - "float": "The :attribute value must be a float", - "boolean": "The :attribute value field must be true or false", - "same": "The :attribute value must be the same as field :field", - "different": "The :attribute value must be different from field :field", - "in": "The :attribute value is not in acceptable range", - "not-in": "The :attribute value is not in acceptable range", - "regex": "The :attribute value is invalid", - "__default__": "The :attribute value is invalid", -} - // getErrorMessageByRule retrieves and returns the error message for specified rule. // It firstly retrieves the message from custom message map, and then checks i18n manager, // it returns the default error message if it's not found in custom message map or i18n manager. func (v *Validator) getErrorMessageByRule(ruleKey string, customMsgMap map[string]string) string { content := customMsgMap[ruleKey] if content != "" { + // I18n translation. + i18nContent := v.i18nManager.GetContent(v.ctx, content) + if i18nContent != "" { + return i18nContent + } return content } - content = gi18n.GetContent(fmt.Sprintf(`gf.gvalid.rule.%s`, ruleKey), v.i18nLang) + // Retrieve default message according to certain rule. + content = v.i18nManager.GetContent(v.ctx, ruleMessagePrefixForI18n+ruleKey) if content == "" { content = defaultMessages[ruleKey] } // If there's no configured rule message, it uses default one. if content == "" { - content = gi18n.GetContent(`gf.gvalid.rule.__default__`, v.i18nLang) + content = v.i18nManager.GetContent(v.ctx, ruleMessagePrefixForI18n+internalDefaultRuleName) if content == "" { - content = defaultMessages["__default__"] + content = defaultMessages[internalDefaultRuleName] } } return content diff --git a/util/gvalid/gvalid_validator_rule_length.go b/util/gvalid/gvalid_validator_rule_length.go index 33edfa97a..7bd5b188e 100644 --- a/util/gvalid/gvalid_validator_rule_length.go +++ b/util/gvalid/gvalid_validator_rule_length.go @@ -58,6 +58,13 @@ func (v *Validator) checkLength(value, ruleKey, ruleVal string, customMsgMap map msg = v.getErrorMessageByRule(ruleKey, customMsgMap) msg = strings.Replace(msg, ":max", strconv.Itoa(max), -1) } + + case "size": + size, err := strconv.Atoi(ruleVal) + if valueLen != size || err != nil { + msg = v.getErrorMessageByRule(ruleKey, customMsgMap) + msg = strings.Replace(msg, ":size", strconv.Itoa(size), -1) + } } return msg } diff --git a/util/gvalid/gvalid_validator_rule_required.go b/util/gvalid/gvalid_validator_rule_required.go index 969d7bb56..9e30b704d 100644 --- a/util/gvalid/gvalid_validator_rule_required.go +++ b/util/gvalid/gvalid_validator_rule_required.go @@ -9,6 +9,7 @@ package gvalid import ( "github.com/gogf/gf/internal/empty" "github.com/gogf/gf/util/gconv" + "github.com/gogf/gf/util/gutil" "reflect" "strings" ) @@ -26,17 +27,19 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-if: id,1,age,18 case "required-if": required = false - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) // It supports multiple field and value pairs. if len(array)%2 == 0 { for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - if v, ok := dataMap[tk]; ok { - if strings.Compare(tv, gconv.String(v)) == 0 { - required = true - break - } + _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) + if strings.Compare(tv, gconv.String(foundValue)) == 0 { + required = true + break } i += 2 } @@ -46,18 +49,21 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-unless: id,1,age,18 case "required-unless": required = true - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) // It supports multiple field and value pairs. if len(array)%2 == 0 { for i := 0; i < len(array); { tk := array[i] tv := array[i+1] - if v, ok := dataMap[tk]; ok { - if strings.Compare(tv, gconv.String(v)) == 0 { - required = false - break - } + _, foundValue = gutil.MapPossibleItemByKey(dataMap, tk) + if strings.Compare(tv, gconv.String(foundValue)) == 0 { + required = false + break } + i += 2 } } @@ -66,9 +72,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-with": required = false - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if !empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if !empty.IsEmpty(foundValue) { required = true break } @@ -78,9 +88,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-with-all": required = true - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if empty.IsEmpty(foundValue) { required = false break } @@ -90,9 +104,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-without": required = false - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if empty.IsEmpty(foundValue) { required = true break } @@ -102,9 +120,13 @@ func (v *Validator) checkRequired(value interface{}, ruleKey, rulePattern string // Example: required-with:id,name case "required-without-all": required = true - array := strings.Split(rulePattern, ",") + var ( + array = strings.Split(rulePattern, ",") + foundValue interface{} + ) for i := 0; i < len(array); i++ { - if !empty.IsEmpty(dataMap[array[i]]) { + _, foundValue = gutil.MapPossibleItemByKey(dataMap, array[i]) + if !empty.IsEmpty(foundValue) { required = false break } diff --git a/util/gvalid/gvalid_z_example_test.go b/util/gvalid/gvalid_z_example_test.go index 2fa6cedfa..d11fb4e9f 100644 --- a/util/gvalid/gvalid_z_example_test.go +++ b/util/gvalid/gvalid_z_example_test.go @@ -7,14 +7,16 @@ package gvalid_test import ( + "context" "errors" "fmt" + "math" + "reflect" + "github.com/gogf/gf/container/gvar" "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gconv" "github.com/gogf/gf/util/gvalid" - "math" - "reflect" ) func ExampleCheckMap() { @@ -28,7 +30,7 @@ func ExampleCheckMap() { "password@required|length:6,16|same:password2#密码不能为空|密码长度应当在:min到:max之间|两次密码输入不相等", "password2@required|length:6,16#", } - if e := gvalid.CheckMap(params, rules); e != nil { + if e := gvalid.CheckMap(context.TODO(), params, rules); e != nil { fmt.Println(e.Map()) fmt.Println(e.FirstItem()) fmt.Println(e.FirstString()) @@ -50,7 +52,7 @@ func ExampleCheckMap2() { "password@required|length:6,16|same:password2#密码不能为空|密码长度应当在:min到:max之间|两次密码输入不相等", "password2@required|length:6,16#", } - if e := gvalid.CheckMap(params, rules); e != nil { + if e := gvalid.CheckMap(context.TODO(), params, rules); e != nil { fmt.Println(e.Map()) fmt.Println(e.FirstItem()) fmt.Println(e.FirstString()) @@ -72,7 +74,7 @@ func ExampleCheckStruct() { Page: 1, Size: 10, } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) fmt.Println(err == nil) // Output: // true @@ -89,7 +91,7 @@ func ExampleCheckStruct2() { Page: 1, Size: 10, } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) fmt.Println(err == nil) // Output: // true @@ -106,28 +108,13 @@ func ExampleCheckStruct3() { Page: 1, Size: 10, } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) fmt.Println(err) // Output: // project id must between 1, 10000 } func ExampleRegisterRule() { - rule := "unique-name" - gvalid.RegisterRule(rule, func(rule string, value interface{}, message string, params map[string]interface{}) error { - var ( - id = gconv.Int(params["Id"]) - name = gconv.String(value) - ) - n, err := g.Table("user").Where("id != ? and name = ?", id, name).Count() - if err != nil { - return err - } - if n > 0 { - return errors.New(message) - } - return nil - }) type User struct { Id int Name string `v:"required|unique-name # 请输入用户名称|用户名称已被占用"` @@ -138,7 +125,23 @@ func ExampleRegisterRule() { Name: "john", Pass: "123456", } - err := gvalid.CheckStruct(user, nil) + + rule := "unique-name" + gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + var ( + id = data.(*User).Id + name = gconv.String(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 nil + }) + err := gvalid.CheckStruct(context.TODO(), user, nil) fmt.Println(err.Error()) // May Output: // 用户名称已被占用 @@ -146,7 +149,7 @@ func ExampleRegisterRule() { func ExampleRegisterRule_OverwriteRequired() { rule := "required" - gvalid.RegisterRule(rule, func(rule string, value interface{}, message string, params map[string]interface{}) error { + gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { reflectValue := reflect.ValueOf(value) if reflectValue.Kind() == reflect.Ptr { reflectValue = reflectValue.Elem() @@ -172,18 +175,95 @@ func ExampleRegisterRule_OverwriteRequired() { } return nil }) - fmt.Println(gvalid.Check("", "required", "It's required")) - fmt.Println(gvalid.Check(0, "required", "It's required")) - fmt.Println(gvalid.Check(false, "required", "It's required")) + fmt.Println(gvalid.CheckValue(context.TODO(), "", "required", "It's required")) + fmt.Println(gvalid.CheckValue(context.TODO(), 0, "required", "It's required")) + fmt.Println(gvalid.CheckValue(context.TODO(), false, "required", "It's required")) gvalid.DeleteRule(rule) fmt.Println("rule deleted") - fmt.Println(gvalid.Check("", "required", "It's required")) - fmt.Println(gvalid.Check(0, "required", "It's required")) - fmt.Println(gvalid.Check(false, "required", "It's required")) + fmt.Println(gvalid.CheckValue(context.TODO(), "", "required", "It's required")) + fmt.Println(gvalid.CheckValue(context.TODO(), 0, "required", "It's required")) + fmt.Println(gvalid.CheckValue(context.TODO(), false, "required", "It's required")) // Output: // It's required // It's required // It's required // rule deleted // It's required + // + // +} + +func ExampleValidator_Rules() { + data := g.Map{ + "password": "123", + } + err := g.Validator().Data(data).Rules("required-with:password").Messages("请输入确认密码").CheckValue("") + fmt.Println(err.String()) + + // Output: + // 请输入确认密码 +} + +func ExampleValidator_CheckValue() { + err := g.Validator().Rules("min:18").Messages("未成年人不允许注册哟").CheckValue(16) + fmt.Println(err.String()) + + // Output: + // 未成年人不允许注册哟 +} + +func ExampleValidator_CheckMap() { + params := map[string]interface{}{ + "passport": "", + "password": "123456", + "password2": "1234567", + } + rules := map[string]string{ + "passport": "required|length:6,16", + "password": "required|length:6,16|same:password2", + "password2": "required|length:6,16", + } + messages := map[string]interface{}{ + "passport": "账号不能为空|账号长度应当在:min到:max之间", + "password": map[string]string{ + "required": "密码不能为空", + "same": "两次密码输入不相等", + }, + } + err := g.Validator().Messages(messages).Rules(rules).CheckMap(params) + if err != nil { + g.Dump(err.Maps()) + } + + // May Output: + //{ + // "passport": { + // "length": "账号长度应当在6到16之间", + // "required": "账号不能为空" + //}, + // "password": { + // "same": "两次密码输入不相等" + //} + //} +} + +func ExampleValidator_CheckStruct() { + type User struct { + Name string `v:"required#请输入用户姓名"` + Type int `v:"required#请选择用户类型"` + } + data := g.Map{ + "name": "john", + } + user := User{} + if err := gconv.Scan(data, &user); err != nil { + panic(err) + } + err := g.Validator().Data(data).CheckStruct(user) + if err != nil { + fmt.Println(err.Items()) + } + + // Output: + // [map[Type:map[required:请选择用户类型]]] } diff --git a/util/gvalid/gvalid_z_unit_basic_all_test.go b/util/gvalid/gvalid_z_unit_basic_all_test.go index bfde55995..30f44e470 100755 --- a/util/gvalid/gvalid_z_unit_basic_all_test.go +++ b/util/gvalid/gvalid_z_unit_basic_all_test.go @@ -7,6 +7,7 @@ package gvalid_test import ( + "context" "github.com/gogf/gf/errors/gerror" "github.com/gogf/gf/os/gtime" "testing" @@ -23,26 +24,26 @@ func Test_Check(t *testing.T) { val1 := 0 val2 := 7 val3 := 20 - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - t.Assert(err1, "invalid_rules: abc:6,16") - t.Assert(err2, "invalid_rules: abc:6,16") - t.Assert(err3, "invalid_rules: abc:6,16") + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + t.Assert(err1, "InvalidRules: abc:6,16") + t.Assert(err2, "InvalidRules: abc:6,16") + t.Assert(err3, "InvalidRules: abc:6,16") }) } func Test_Required(t *testing.T) { - if m := gvalid.Check("1", "required", nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "1", "required", nil); m != nil { t.Error(m) } - if m := gvalid.Check("", "required", nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "", "required", nil); m == nil { t.Error(m) } - if m := gvalid.Check("", "required-if: id,1,age,18", nil, map[string]interface{}{"id": 1, "age": 19}); m == nil { + if m := gvalid.CheckValue(context.TODO(), "", "required-if: id,1,age,18", nil, map[string]interface{}{"id": 1, "age": 19}); m == nil { t.Error("Required校验失败") } - if m := gvalid.Check("", "required-if: id,1,age,18", nil, map[string]interface{}{"id": 2, "age": 19}); m != nil { + if m := gvalid.CheckValue(context.TODO(), "", "required-if: id,1,age,18", nil, map[string]interface{}{"id": 2, "age": 19}); m != nil { t.Error("Required校验失败") } } @@ -50,20 +51,20 @@ func Test_Required(t *testing.T) { func Test_RequiredIf(t *testing.T) { gtest.C(t, func(t *gtest.T) { rule := "required-if:id,1,age,18" - t.AssertNE(gvalid.Check("", rule, nil, g.Map{"id": 1}), nil) - t.Assert(gvalid.Check("", rule, nil, g.Map{"id": 0}), nil) - t.AssertNE(gvalid.Check("", rule, nil, g.Map{"age": 18}), nil) - t.Assert(gvalid.Check("", rule, nil, g.Map{"age": 20}), nil) + t.AssertNE(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"id": 1}), nil) + t.Assert(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"id": 0}), nil) + t.AssertNE(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"age": 18}), nil) + t.Assert(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"age": 20}), nil) }) } func Test_RequiredUnless(t *testing.T) { gtest.C(t, func(t *gtest.T) { rule := "required-unless:id,1,age,18" - t.Assert(gvalid.Check("", rule, nil, g.Map{"id": 1}), nil) - t.AssertNE(gvalid.Check("", rule, nil, g.Map{"id": 0}), nil) - t.Assert(gvalid.Check("", rule, nil, g.Map{"age": 18}), nil) - t.AssertNE(gvalid.Check("", rule, nil, g.Map{"age": 20}), nil) + t.Assert(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"id": 1}), nil) + t.AssertNE(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"id": 0}), nil) + t.Assert(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"age": 18}), nil) + t.AssertNE(gvalid.CheckValue(context.TODO(), "", rule, nil, g.Map{"age": 20}), nil) }) } @@ -81,9 +82,9 @@ func Test_RequiredWith(t *testing.T) { "id": 100, "name": "john", } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.Assert(err1, nil) t.AssertNE(err2, nil) t.AssertNE(err3, nil) @@ -101,9 +102,9 @@ func Test_RequiredWith(t *testing.T) { params3 := g.Map{ "time": time.Time{}, } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.Assert(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -120,9 +121,9 @@ func Test_RequiredWith(t *testing.T) { params3 := g.Map{ "time": time.Now(), } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.Assert(err1, nil) t.AssertNE(err2, nil) t.AssertNE(err3, nil) @@ -139,7 +140,7 @@ func Test_RequiredWith(t *testing.T) { StartTime: nil, EndTime: nil, } - t.Assert(gvalid.CheckStruct(data, nil), nil) + t.Assert(gvalid.CheckStruct(context.TODO(), data, nil), nil) }) gtest.C(t, func(t *gtest.T) { type UserApiSearch struct { @@ -152,7 +153,7 @@ func Test_RequiredWith(t *testing.T) { StartTime: nil, EndTime: gtime.Now(), } - t.AssertNE(gvalid.CheckStruct(data, nil), nil) + t.AssertNE(gvalid.CheckStruct(context.TODO(), data, nil), nil) }) } @@ -170,9 +171,9 @@ func Test_RequiredWithAll(t *testing.T) { "id": 100, "name": "john", } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.Assert(err1, nil) t.Assert(err2, nil) t.AssertNE(err3, nil) @@ -193,9 +194,9 @@ func Test_RequiredWithOut(t *testing.T) { "id": 100, "name": "john", } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -216,9 +217,9 @@ func Test_RequiredWithOutAll(t *testing.T) { "id": 100, "name": "john", } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.AssertNE(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -235,20 +236,23 @@ func Test_Date(t *testing.T) { val5 := "2010.11.01" val6 := "2010/11/01" val7 := "2010=11=01" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) - err6 := gvalid.Check(val6, rule, nil) - err7 := gvalid.Check(val7, rule, nil) - t.Assert(err1, nil) - t.Assert(err2, nil) + val8 := "123" + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) + err6 := gvalid.CheckValue(context.TODO(), val6, rule, nil) + err7 := gvalid.CheckValue(context.TODO(), val7, rule, nil) + err8 := gvalid.CheckValue(context.TODO(), val8, rule, nil) + t.AssertNE(err1, nil) + t.AssertNE(err2, nil) t.Assert(err3, nil) t.Assert(err4, nil) t.Assert(err5, nil) t.Assert(err6, nil) t.AssertNE(err7, nil) + t.AssertNE(err8, nil) }) } @@ -260,12 +264,12 @@ func Test_DateFormat(t *testing.T) { val4 := "201011-01" val5 := "2010~11~01" val6 := "2010-11~01" - err1 := gvalid.Check(val1, "date-format:Y", nil) - err2 := gvalid.Check(val2, "date-format:Ym", nil) - err3 := gvalid.Check(val3, "date-format:Y.m", nil) - err4 := gvalid.Check(val4, "date-format:Ym-d", nil) - err5 := gvalid.Check(val5, "date-format:Y~m~d", nil) - err6 := gvalid.Check(val6, "date-format:Y~m~d", nil) + err1 := gvalid.CheckValue(context.TODO(), val1, "date-format:Y", nil) + err2 := gvalid.CheckValue(context.TODO(), val2, "date-format:Ym", nil) + err3 := gvalid.CheckValue(context.TODO(), val3, "date-format:Y.m", nil) + err4 := gvalid.CheckValue(context.TODO(), val4, "date-format:Ym-d", nil) + err5 := gvalid.CheckValue(context.TODO(), val5, "date-format:Y~m~d", nil) + err6 := gvalid.CheckValue(context.TODO(), val6, "date-format:Y~m~d", nil) t.Assert(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -276,8 +280,8 @@ func Test_DateFormat(t *testing.T) { gtest.C(t, func(t *gtest.T) { t1 := gtime.Now() t2 := time.Time{} - err1 := gvalid.Check(t1, "date-format:Y", nil) - err2 := gvalid.Check(t2, "date-format:Y", nil) + err1 := gvalid.CheckValue(context.TODO(), t1, "date-format:Y", nil) + err2 := gvalid.CheckValue(context.TODO(), t2, "date-format:Y", nil) t.Assert(err1, nil) t.AssertNE(err2, nil) }) @@ -290,10 +294,10 @@ func Test_Email(t *testing.T) { value2 := "m@www@johngcn" value3 := "m-m_m@mail.johng.cn" value4 := "m.m-m@johng.cn" - err1 := gvalid.Check(value1, rule, nil) - err2 := gvalid.Check(value2, rule, nil) - err3 := gvalid.Check(value3, rule, nil) - err4 := gvalid.Check(value4, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), value1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), value2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), value3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), value4, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -303,10 +307,10 @@ func Test_Email(t *testing.T) { func Test_Phone(t *testing.T) { gtest.C(t, func(t *gtest.T) { - err1 := gvalid.Check("1361990897", "phone", nil) - err2 := gvalid.Check("13619908979", "phone", nil) - err3 := gvalid.Check("16719908979", "phone", nil) - err4 := gvalid.Check("19719908989", "phone", nil) + err1 := gvalid.CheckValue(context.TODO(), "1361990897", "phone", nil) + err2 := gvalid.CheckValue(context.TODO(), "13619908979", "phone", nil) + err3 := gvalid.CheckValue(context.TODO(), "16719908979", "phone", nil) + err4 := gvalid.CheckValue(context.TODO(), "19719908989", "phone", nil) t.AssertNE(err1.String(), nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -316,12 +320,12 @@ func Test_Phone(t *testing.T) { func Test_PhoneLoose(t *testing.T) { gtest.C(t, func(t *gtest.T) { - err1 := gvalid.Check("13333333333", "phone-loose", nil) - err2 := gvalid.Check("15555555555", "phone-loose", nil) - err3 := gvalid.Check("16666666666", "phone-loose", nil) - err4 := gvalid.Check("23333333333", "phone-loose", nil) - err5 := gvalid.Check("1333333333", "phone-loose", nil) - err6 := gvalid.Check("10333333333", "phone-loose", nil) + err1 := gvalid.CheckValue(context.TODO(), "13333333333", "phone-loose", nil) + err2 := gvalid.CheckValue(context.TODO(), "15555555555", "phone-loose", nil) + err3 := gvalid.CheckValue(context.TODO(), "16666666666", "phone-loose", nil) + err4 := gvalid.CheckValue(context.TODO(), "23333333333", "phone-loose", nil) + err5 := gvalid.CheckValue(context.TODO(), "1333333333", "phone-loose", nil) + err6 := gvalid.CheckValue(context.TODO(), "10333333333", "phone-loose", nil) t.Assert(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -338,11 +342,11 @@ func Test_Telephone(t *testing.T) { val3 := "86292651" val4 := "028-8692651" val5 := "0830-8692651" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -359,11 +363,11 @@ func Test_Passport(t *testing.T) { val3 := "aaaaa" val4 := "aaaaaa" val5 := "a123_456" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.AssertNE(err3, nil) @@ -380,11 +384,11 @@ func Test_Password(t *testing.T) { val3 := "a12345-6" val4 := ">,/;'[09-" val5 := "a123_456" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -403,13 +407,13 @@ func Test_Password2(t *testing.T) { val5 := "a123_456" val6 := "Nant1986" val7 := "Nant1986!" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) - err6 := gvalid.Check(val6, rule, nil) - err7 := gvalid.Check(val7, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) + err6 := gvalid.CheckValue(context.TODO(), val6, rule, nil) + err7 := gvalid.CheckValue(context.TODO(), val7, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.AssertNE(err3, nil) @@ -430,13 +434,13 @@ func Test_Password3(t *testing.T) { val5 := "a123_456" val6 := "Nant1986" val7 := "Nant1986!" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) - err6 := gvalid.Check(val6, rule, nil) - err7 := gvalid.Check(val7, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) + err6 := gvalid.CheckValue(context.TODO(), val6, rule, nil) + err7 := gvalid.CheckValue(context.TODO(), val7, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.AssertNE(err3, nil) @@ -452,8 +456,8 @@ func Test_Postcode(t *testing.T) { rule := "postcode" val1 := "12345" val2 := "610036" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) t.AssertNE(err1, nil) t.Assert(err2, nil) }) @@ -467,11 +471,11 @@ func Test_ResidentId(t *testing.T) { val3 := "311128500121201" val4 := "510521198607185367" val5 := "51052119860718536x" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.AssertNE(err3, nil) @@ -485,8 +489,8 @@ func Test_BankCard(t *testing.T) { rule := "bank-card" val1 := "6230514630000424470" val2 := "6230514630000424473" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) t.AssertNE(err1, nil) t.Assert(err2, nil) }) @@ -500,11 +504,11 @@ func Test_QQ(t *testing.T) { val3 := "10000" val4 := "38996181" val5 := "389961817" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -514,31 +518,31 @@ func Test_QQ(t *testing.T) { } func Test_Ip(t *testing.T) { - if m := gvalid.Check("10.0.0.1", "ip", nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "10.0.0.1", "ip", nil); m != nil { t.Error(m) } - if m := gvalid.Check("10.0.0.1", "ipv4", nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "10.0.0.1", "ipv4", nil); m != nil { t.Error(m) } - if m := gvalid.Check("0.0.0.0", "ipv4", nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "0.0.0.0", "ipv4", nil); m != nil { t.Error(m) } - if m := gvalid.Check("1920.0.0.0", "ipv4", nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "1920.0.0.0", "ipv4", nil); m == nil { t.Error("ipv4校验失败") } - if m := gvalid.Check("1920.0.0.0", "ip", nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "1920.0.0.0", "ip", nil); m == nil { t.Error("ipv4校验失败") } - if m := gvalid.Check("fe80::5484:7aff:fefe:9799", "ipv6", nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "fe80::5484:7aff:fefe:9799", "ipv6", nil); m != nil { t.Error(m) } - if m := gvalid.Check("fe80::5484:7aff:fefe:9799123", "ipv6", nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "fe80::5484:7aff:fefe:9799123", "ipv6", nil); m == nil { t.Error(m) } - if m := gvalid.Check("fe80::5484:7aff:fefe:9799", "ip", nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "fe80::5484:7aff:fefe:9799", "ip", nil); m != nil { t.Error(m) } - if m := gvalid.Check("fe80::5484:7aff:fefe:9799123", "ip", nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "fe80::5484:7aff:fefe:9799123", "ip", nil); m == nil { t.Error(m) } } @@ -551,11 +555,11 @@ func Test_IPv4(t *testing.T) { val3 := "1.1.1.1" val4 := "255.255.255.0" val5 := "127.0.0.1" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -572,11 +576,11 @@ func Test_IPv6(t *testing.T) { val3 := "1030::C9B4:FF12:48AA:1A2B" val4 := "2000:0:0:0:0:0:0:1" val5 := "0000:0000:0000:0000:0000:ffff:c0a8:5909" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -591,9 +595,9 @@ func Test_MAC(t *testing.T) { val1 := "192.168.1.1" val2 := "44-45-53-54-00-00" val3 := "01:00:5e:00:00:00" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) t.AssertNE(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -607,10 +611,10 @@ func Test_URL(t *testing.T) { val2 := "https://www.baidu.com" val3 := "http://127.0.0.1" val4 := "file:///tmp/test.txt" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) t.AssertNE(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -637,7 +641,7 @@ func Test_Domain(t *testing.T) { } var err error for k, v := range m { - err = gvalid.Check(k, "domain", nil) + err = gvalid.CheckValue(context.TODO(), k, "domain", nil) if v { //fmt.Println(k) t.Assert(err, nil) @@ -651,10 +655,10 @@ func Test_Domain(t *testing.T) { func Test_Length(t *testing.T) { rule := "length:6,16" - if m := gvalid.Check("123456", rule, nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "123456", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("12345", rule, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "12345", rule, nil); m == nil { t.Error("长度校验失败") } } @@ -664,18 +668,18 @@ func Test_MinLength(t *testing.T) { msgs := map[string]string{ "min-length": "地址长度至少为:min位", } - if m := gvalid.Check("123456", rule, nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "123456", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("12345", rule, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "12345", rule, nil); m == nil { t.Error("长度校验失败") } - if m := gvalid.Check("12345", rule, msgs); m == nil { + if m := gvalid.CheckValue(context.TODO(), "12345", rule, msgs); m == nil { t.Error("长度校验失败") } rule2 := "min-length:abc" - if m := gvalid.Check("123456", rule2, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "123456", rule2, nil); m == nil { t.Error("长度校验失败") } } @@ -685,31 +689,41 @@ func Test_MaxLength(t *testing.T) { msgs := map[string]string{ "max-length": "地址长度至大为:max位", } - if m := gvalid.Check("12345", rule, nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "12345", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("1234567", rule, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "1234567", rule, nil); m == nil { t.Error("长度校验失败") } - if m := gvalid.Check("1234567", rule, msgs); m == nil { + if m := gvalid.CheckValue(context.TODO(), "1234567", rule, msgs); m == nil { t.Error("长度校验失败") } rule2 := "max-length:abc" - if m := gvalid.Check("123456", rule2, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "123456", rule2, nil); m == nil { + t.Error("长度校验失败") + } +} + +func Test_Size(t *testing.T) { + rule := "size:5" + if m := gvalid.CheckValue(context.TODO(), "12345", rule, nil); m != nil { + t.Error(m) + } + if m := gvalid.CheckValue(context.TODO(), "123456", rule, nil); m == nil { t.Error("长度校验失败") } } func Test_Between(t *testing.T) { rule := "between:6.01, 10.01" - if m := gvalid.Check(10, rule, nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), 10, rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check(10.02, rule, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), 10.02, rule, nil); m == nil { t.Error("大小范围校验失败") } - if m := gvalid.Check("a", rule, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "a", rule, nil); m == nil { t.Error("大小范围校验失败") } } @@ -722,11 +736,11 @@ func Test_Min(t *testing.T) { val3 := "100" val4 := "1000" val5 := "a" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -734,7 +748,7 @@ func Test_Min(t *testing.T) { t.AssertNE(err5, nil) rule2 := "min:a" - err6 := gvalid.Check(val1, rule2, nil) + err6 := gvalid.CheckValue(context.TODO(), val1, rule2, nil) t.AssertNE(err6, nil) }) } @@ -747,11 +761,11 @@ func Test_Max(t *testing.T) { val3 := "100" val4 := "1000" val5 := "a" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) t.Assert(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -759,7 +773,7 @@ func Test_Max(t *testing.T) { t.AssertNE(err5, nil) rule2 := "max:a" - err6 := gvalid.Check(val1, rule2, nil) + err6 := gvalid.CheckValue(context.TODO(), val1, rule2, nil) t.AssertNE(err6, nil) }) } @@ -773,12 +787,12 @@ func Test_Json(t *testing.T) { val4 := "[]" val5 := "[1,2,3,4]" val6 := `{"list":[1,2,3,4]}` - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) - err6 := gvalid.Check(val6, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) + err6 := gvalid.CheckValue(context.TODO(), val6, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -797,12 +811,12 @@ func Test_Integer(t *testing.T) { val4 := "1" val5 := "100" val6 := `999999999` - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) - err6 := gvalid.Check(val6, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) + err6 := gvalid.CheckValue(context.TODO(), val6, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -821,12 +835,12 @@ func Test_Float(t *testing.T) { val4 := "1.0" val5 := "1.1" val6 := `0.1` - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) - err6 := gvalid.Check(val6, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) + err6 := gvalid.CheckValue(context.TODO(), val6, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -845,12 +859,12 @@ func Test_Boolean(t *testing.T) { val4 := "1" val5 := "true" val6 := `off` - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) - err5 := gvalid.Check(val5, rule, nil) - err6 := gvalid.Check(val6, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) + err5 := gvalid.CheckValue(context.TODO(), val5, rule, nil) + err6 := gvalid.CheckValue(context.TODO(), val6, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -874,9 +888,9 @@ func Test_Same(t *testing.T) { "id": 100, "name": "john", } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.AssertNE(err1, nil) t.Assert(err2, nil) t.Assert(err3, nil) @@ -897,9 +911,9 @@ func Test_Different(t *testing.T) { "id": 100, "name": "john", } - err1 := gvalid.Check(val1, rule, nil, params1) - err2 := gvalid.Check(val1, rule, nil, params2) - err3 := gvalid.Check(val1, rule, nil, params3) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params1) + err2 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params2) + err3 := gvalid.CheckValue(context.TODO(), val1, rule, nil, params3) t.Assert(err1, nil) t.AssertNE(err2, nil) t.AssertNE(err3, nil) @@ -913,10 +927,10 @@ func Test_In(t *testing.T) { val2 := "1" val3 := "100" val4 := "200" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -931,10 +945,10 @@ func Test_NotIn(t *testing.T) { val2 := "1" val3 := "100" val4 := "200" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) t.Assert(err1, nil) t.Assert(err2, nil) t.AssertNE(err3, nil) @@ -946,10 +960,10 @@ func Test_NotIn(t *testing.T) { val2 := "1" val3 := "100" val4 := "200" - err1 := gvalid.Check(val1, rule, nil) - err2 := gvalid.Check(val2, rule, nil) - err3 := gvalid.Check(val3, rule, nil) - err4 := gvalid.Check(val4, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil) + err4 := gvalid.CheckValue(context.TODO(), val4, rule, nil) t.Assert(err1, nil) t.Assert(err2, nil) t.AssertNE(err3, nil) @@ -959,10 +973,10 @@ func Test_NotIn(t *testing.T) { func Test_Regex1(t *testing.T) { rule := `regex:\d{6}|\D{6}|length:6,16` - if m := gvalid.Check("123456", rule, nil); m != nil { + if m := gvalid.CheckValue(context.TODO(), "123456", rule, nil); m != nil { t.Error(m) } - if m := gvalid.Check("abcde6", rule, nil); m == nil { + if m := gvalid.CheckValue(context.TODO(), "abcde6", rule, nil); m == nil { t.Error("校验失败") } } @@ -973,9 +987,9 @@ func Test_Regex2(t *testing.T) { str1 := "" str2 := "data" str3 := "data:image/jpeg;base64,/9jrbattq22r" - err1 := gvalid.Check(str1, rule, nil) - err2 := gvalid.Check(str2, rule, nil) - err3 := gvalid.Check(str3, rule, nil) + err1 := gvalid.CheckValue(context.TODO(), str1, rule, nil) + err2 := gvalid.CheckValue(context.TODO(), str2, rule, nil) + err3 := gvalid.CheckValue(context.TODO(), str3, rule, nil) t.AssertNE(err1, nil) t.AssertNE(err2, nil) t.Assert(err3, nil) @@ -992,11 +1006,11 @@ func Test_InternalError_String(t *testing.T) { Name string `v:"hh"` } aa := a{Name: "2"} - err := gvalid.CheckStruct(&aa, nil) + err := gvalid.CheckStruct(context.TODO(), &aa, nil) - t.Assert(err.String(), "invalid_rules: hh") - t.Assert(err.Strings(), g.Slice{"invalid_rules: hh"}) - t.Assert(err.FirstString(), "invalid_rules: hh") - t.Assert(gerror.Current(err), "invalid_rules: hh") + t.Assert(err.String(), "InvalidRules: hh") + t.Assert(err.Strings(), g.Slice{"InvalidRules: hh"}) + t.Assert(err.FirstString(), "InvalidRules: hh") + t.Assert(gerror.Current(err), "InvalidRules: hh") }) } diff --git a/util/gvalid/gvalid_z_unit_checkmap_test.go b/util/gvalid/gvalid_z_unit_checkmap_test.go index 4660ebf21..dbb693e24 100755 --- a/util/gvalid/gvalid_z_unit_checkmap_test.go +++ b/util/gvalid/gvalid_z_unit_checkmap_test.go @@ -7,6 +7,7 @@ package gvalid_test import ( + "context" "github.com/gogf/gf/errors/gerror" "testing" @@ -24,7 +25,7 @@ func Test_CheckMap1(t *testing.T) { "id": "required|between:1,100", "name": "required|length:6,16", } - if m := gvalid.CheckMap(data, rules); m == nil { + if m := gvalid.CheckMap(context.TODO(), data, rules); m == nil { t.Error("CheckMap校验失败") } else { t.Assert(len(m.Maps()), 2) @@ -37,7 +38,7 @@ func Test_CheckMap1(t *testing.T) { func Test_CheckMap2(t *testing.T) { var params interface{} gtest.C(t, func(t *gtest.T) { - if err := gvalid.CheckMap(params, nil, nil); err == nil { + if err := gvalid.CheckMap(context.TODO(), params, nil, nil); err == nil { t.Assert(err, nil) } }) @@ -57,7 +58,7 @@ func Test_CheckMap2(t *testing.T) { "length": "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules, msgs); m == nil { + if m := gvalid.CheckMap(context.TODO(), kvmap, rules, msgs); m == nil { t.Error("CheckMap校验失败") } @@ -76,7 +77,7 @@ func Test_CheckMap2(t *testing.T) { "length": "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules, msgs); m != nil { + if m := gvalid.CheckMap(context.TODO(), kvmap, rules, msgs); m != nil { t.Error(m) } @@ -95,7 +96,7 @@ func Test_CheckMap2(t *testing.T) { "length": "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules, msgs); m != nil { + if m := gvalid.CheckMap(context.TODO(), kvmap, rules, msgs); m != nil { t.Error(m) } @@ -114,7 +115,7 @@ func Test_CheckMap2(t *testing.T) { "length": "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules2, msgs); m != nil { + if m := gvalid.CheckMap(context.TODO(), kvmap, rules2, msgs); m != nil { t.Error(m) } @@ -133,7 +134,7 @@ func Test_CheckMap2(t *testing.T) { "length": "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules2, msgs); m != nil { + if m := gvalid.CheckMap(context.TODO(), kvmap, rules2, msgs); m != nil { t.Error(m) } @@ -152,7 +153,7 @@ func Test_CheckMap2(t *testing.T) { "length": "名称长度为:min到:max个字符", }, } - if m := gvalid.CheckMap(kvmap, rules2, msgs); m != nil { + if m := gvalid.CheckMap(context.TODO(), kvmap, rules2, msgs); m != nil { t.Error(m) } } @@ -166,7 +167,7 @@ func Test_CheckMapWithNilAndNotRequiredField(t *testing.T) { "id": "required", "name": "length:4,16", } - if m := gvalid.CheckMap(data, rules); m != nil { + if m := gvalid.CheckMap(context.TODO(), data, rules); m != nil { t.Error(m) } } @@ -183,13 +184,18 @@ func Test_Sequence(t *testing.T) { "password@required|length:6,16|same:password2#密码不能为空|密码长度应当在:min到:max之间|两次密码输入不相等", "password2@required|length:6,16#", } - err := gvalid.CheckMap(params, rules) + err := gvalid.CheckMap(context.TODO(), params, rules) t.AssertNE(err, nil) t.Assert(len(err.Map()), 2) t.Assert(err.Map()["required"], "账号不能为空") t.Assert(err.Map()["length"], "账号长度应当在6到16之间") t.Assert(len(err.Maps()), 2) + t.Assert(len(err.Items()), 2) + t.Assert(err.Items()[0]["passport"]["length"], "账号长度应当在6到16之间") + t.Assert(err.Items()[0]["passport"]["required"], "账号不能为空") + t.Assert(err.Items()[1]["password"]["same"], "两次密码输入不相等") + t.Assert(err.String(), "账号不能为空; 账号长度应当在6到16之间; 两次密码输入不相等") t.Assert(err.Strings(), []string{"账号不能为空", "账号长度应当在6到16之间", "两次密码输入不相等"}) diff --git a/util/gvalid/gvalid_z_unit_checkstruct_test.go b/util/gvalid/gvalid_z_unit_checkstruct_test.go index 188ea4b5c..14a80fb3a 100755 --- a/util/gvalid/gvalid_z_unit_checkstruct_test.go +++ b/util/gvalid/gvalid_z_unit_checkstruct_test.go @@ -7,7 +7,9 @@ package gvalid_test import ( + "context" "github.com/gogf/gf/container/gvar" + "github.com/gogf/gf/os/gtime" "testing" "github.com/gogf/gf/frame/g" @@ -34,7 +36,7 @@ func Test_CheckStruct(t *testing.T) { "Age": "年龄为18到30周岁", } obj := &Object{"john", 16} - err := gvalid.CheckStruct(obj, rules, msgs) + err := gvalid.CheckStruct(context.TODO(), obj, rules, msgs) t.Assert(err, nil) }) @@ -55,7 +57,7 @@ func Test_CheckStruct(t *testing.T) { "Age": "年龄为18到30周岁", } obj := &Object{"john", 16} - err := gvalid.CheckStruct(obj, rules, msgs) + err := gvalid.CheckStruct(context.TODO(), obj, rules, msgs) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 2) t.Assert(err.Maps()["Name"]["required"], "") @@ -80,7 +82,7 @@ func Test_CheckStruct(t *testing.T) { "Age": "年龄为18到30周岁", } obj := &Object{"john", 16} - err := gvalid.CheckStruct(obj, rules, msgs) + err := gvalid.CheckStruct(context.TODO(), obj, rules, msgs) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 2) t.Assert(err.Maps()["Name"]["required"], "") @@ -105,7 +107,7 @@ func Test_CheckStruct(t *testing.T) { "Age": "年龄为18到30周岁", } obj := &Object{"john", 16} - err := gvalid.CheckStruct(obj, rules, msgs) + err := gvalid.CheckStruct(context.TODO(), obj, rules, msgs) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 2) t.Assert(err.Maps()["Name"]["required"], "") @@ -119,7 +121,7 @@ func Test_CheckStruct(t *testing.T) { Password string `json:"password" gvalid:"password@required#登录密码不能为空"` } var login LoginRequest - err := gvalid.CheckStruct(login, nil) + err := gvalid.CheckStruct(context.TODO(), login, nil) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 2) t.Assert(err.Maps()["username"]["required"], "用户名不能为空") @@ -132,7 +134,7 @@ func Test_CheckStruct(t *testing.T) { Password string `json:"password" gvalid:"@required#登录密码不能为空"` } var login LoginRequest - err := gvalid.CheckStruct(login, nil) + err := gvalid.CheckStruct(context.TODO(), login, nil) t.Assert(err, nil) }) @@ -142,7 +144,7 @@ func Test_CheckStruct(t *testing.T) { Password string `json:"password" gvalid:"password@required#登录密码不能为空"` } var login LoginRequest - err := gvalid.CheckStruct(login, nil) + err := gvalid.CheckStruct(context.TODO(), login, nil) t.AssertNE(err, nil) t.Assert(err.Maps()["password"]["required"], "登录密码不能为空") }) @@ -160,7 +162,7 @@ func Test_CheckStruct(t *testing.T) { Username: "john", Password: "123456", } - err := gvalid.CheckStruct(user, nil) + err := gvalid.CheckStruct(context.TODO(), user, nil) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 1) t.Assert(err.Maps()["uid"]["min"], "ID不能为空") @@ -183,7 +185,7 @@ func Test_CheckStruct(t *testing.T) { "username@required#用户名不能为空", } - err := gvalid.CheckStruct(user, rules) + err := gvalid.CheckStruct(context.TODO(), user, rules) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 1) t.Assert(err.Maps()["uid"]["min"], "ID不能为空") @@ -201,7 +203,7 @@ func Test_CheckStruct(t *testing.T) { Username: "john", Password: "123456", } - err := gvalid.CheckStruct(user, nil) + err := gvalid.CheckStruct(context.TODO(), user, nil) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 1) }) @@ -219,13 +221,64 @@ func Test_CheckStruct(t *testing.T) { Username: "john", Password: "123456", } - err := gvalid.CheckStruct(user, nil) + err := gvalid.CheckStruct(context.TODO(), user, nil) t.AssertNE(err, nil) t.Assert(len(err.Maps()), 1) t.Assert(err.Maps()["uid"]["min"], "ID不能为空") }) } +func Test_CheckStruct_EmbeddedObject_Attribute(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type Base struct { + Time *gtime.Time + } + type Object struct { + Base + Name string + Type int + } + rules := map[string]string{ + "Name": "required", + "Type": "required", + } + ruleMsg := map[string]interface{}{ + "Name": "名称必填", + "Type": "类型必填", + } + obj := &Object{} + obj.Type = 1 + obj.Name = "john" + obj.Time = gtime.Now() + err := gvalid.CheckStruct(context.TODO(), obj, rules, ruleMsg) + t.Assert(err, nil) + }) + gtest.C(t, func(t *gtest.T) { + type Base struct { + Name string + Type int + } + type Object struct { + Base Base + Name string + Type int + } + rules := map[string]string{ + "Name": "required", + "Type": "required", + } + ruleMsg := map[string]interface{}{ + "Name": "名称必填", + "Type": "类型必填", + } + obj := &Object{} + obj.Type = 1 + obj.Name = "john" + err := gvalid.CheckStruct(context.TODO(), obj, rules, ruleMsg) + t.Assert(err, nil) + }) +} + func Test_CheckStruct_With_EmbeddedObject(t *testing.T) { gtest.C(t, func(t *gtest.T) { type Pass struct { @@ -244,7 +297,7 @@ func Test_CheckStruct_With_EmbeddedObject(t *testing.T) { Pass2: "2", }, } - err := gvalid.CheckStruct(user, nil) + err := gvalid.CheckStruct(context.TODO(), user, nil) t.AssertNE(err, nil) t.Assert(err.Maps()["name"], g.Map{"required": "请输入您的姓名"}) t.Assert(err.Maps()["password1"], g.Map{"same": "您两次输入的密码不一致"}) @@ -259,18 +312,18 @@ func Test_CheckStruct_With_StructAttribute(t *testing.T) { Pass2 string `valid:"password2@required|same:password1#请再次输入您的密码|您两次输入的密码不一致"` } type User struct { - Id int - Name string `valid:"name@required#请输入您的姓名"` - Passwords Pass + Pass + Id int + Name string `valid:"name@required#请输入您的姓名"` } user := &User{ Name: "", - Passwords: Pass{ + Pass: Pass{ Pass1: "1", Pass2: "2", }, } - err := gvalid.CheckStruct(user, nil) + err := gvalid.CheckStruct(context.TODO(), user, nil) t.AssertNE(err, nil) t.Assert(err.Maps()["name"], g.Map{"required": "请输入您的姓名"}) t.Assert(err.Maps()["password1"], g.Map{"same": "您两次输入的密码不一致"}) @@ -289,7 +342,7 @@ func Test_CheckStruct_Optional(t *testing.T) { Page: 1, Size: 10, } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) t.Assert(err, nil) }) gtest.C(t, func(t *gtest.T) { @@ -302,7 +355,7 @@ func Test_CheckStruct_Optional(t *testing.T) { Page: 1, Size: 10, } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) t.Assert(err, nil) }) gtest.C(t, func(t *gtest.T) { @@ -315,7 +368,7 @@ func Test_CheckStruct_Optional(t *testing.T) { Page: 1, Size: 10, } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) t.Assert(err.String(), "project id must between 1, 10000") }) } @@ -331,7 +384,7 @@ func Test_CheckStruct_NoTag(t *testing.T) { Page: 1, Size: 10, } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) t.Assert(err, nil) }) } @@ -348,7 +401,66 @@ func Test_CheckStruct_InvalidRule(t *testing.T) { Age: 18, Phone: "123", } - err := gvalid.CheckStruct(obj, nil) + err := gvalid.CheckStruct(context.TODO(), obj, nil) t.AssertNE(err, nil) }) } + +func TestValidator_CheckStructWithData(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `v:"required"` + Nickname string `v:"required-with:uid"` + } + data := UserApiSearch{ + Uid: 1, + Nickname: "john", + } + t.Assert(gvalid.CheckStructWithData(context.TODO(), data, g.Map{"uid": 1, "nickname": "john"}, nil), nil) + }) + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `v:"required"` + Nickname string `v:"required-with:uid"` + } + data := UserApiSearch{} + t.AssertNE(gvalid.CheckStructWithData(context.TODO(), data, g.Map{}, nil), nil) + }) + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `json:"uid" v:"required"` + Nickname string `json:"nickname" v:"required-with:Uid"` + } + data := UserApiSearch{ + Uid: 1, + } + t.AssertNE(gvalid.CheckStructWithData(context.TODO(), data, g.Map{}, nil), nil) + }) + + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `json:"uid"` + Nickname string `json:"nickname" v:"required-with:Uid"` + StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"` + EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"` + } + data := UserApiSearch{ + StartTime: nil, + EndTime: nil, + } + t.Assert(gvalid.CheckStructWithData(context.TODO(), data, g.Map{}, nil), nil) + }) + gtest.C(t, func(t *gtest.T) { + type UserApiSearch struct { + Uid int64 `json:"uid"` + Nickname string `json:"nickname" v:"required-with:Uid"` + StartTime *gtime.Time `json:"start_time" v:"required-with:EndTime"` + EndTime *gtime.Time `json:"end_time" v:"required-with:StartTime"` + } + data := UserApiSearch{ + StartTime: gtime.Now(), + EndTime: nil, + } + t.AssertNE(gvalid.CheckStructWithData(context.TODO(), data, g.Map{"start_time": gtime.Now()}, nil), nil) + }) +} diff --git a/util/gvalid/gvalid_z_unit_custom_rule_test.go b/util/gvalid/gvalid_z_unit_custom_rule_test.go index 2fdc36a92..0a4e694b4 100644 --- a/util/gvalid/gvalid_z_unit_custom_rule_test.go +++ b/util/gvalid/gvalid_z_unit_custom_rule_test.go @@ -7,10 +7,12 @@ package gvalid_test import ( + "context" "errors" + "testing" + "github.com/gogf/gf/frame/g" "github.com/gogf/gf/util/gconv" - "testing" "github.com/gogf/gf/test/gtest" "github.com/gogf/gf/util/gvalid" @@ -18,21 +20,25 @@ import ( func Test_CustomRule1(t *testing.T) { rule := "custom" - err := gvalid.RegisterRule(rule, func(rule string, value interface{}, message string, params map[string]interface{}) error { - pass := gconv.String(value) - if len(pass) != 6 { - return errors.New(message) - } - if params["data"] != pass { - return errors.New(message) - } - return nil - }) + err := gvalid.RegisterRule( + rule, + func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + pass := gconv.String(value) + if len(pass) != 6 { + return errors.New(message) + } + m := gconv.Map(data) + if m["data"] != pass { + return errors.New(message) + } + return nil + }, + ) gtest.Assert(err, nil) gtest.C(t, func(t *gtest.T) { - err := gvalid.Check("123456", rule, "custom message") + err := gvalid.CheckValue(context.TODO(), "123456", rule, "custom message") t.Assert(err.String(), "custom message") - err = gvalid.Check("123456", rule, "custom message", g.Map{"data": "123456"}) + err = gvalid.CheckValue(context.TODO(), "123456", rule, "custom message", g.Map{"data": "123456"}) t.Assert(err, nil) }) // Error with struct validation. @@ -45,7 +51,7 @@ func Test_CustomRule1(t *testing.T) { Value: "123", Data: "123456", } - err := gvalid.CheckStruct(st, nil) + err := gvalid.CheckStruct(context.TODO(), st, nil) t.Assert(err.String(), "自定义错误") }) // No error with struct validation. @@ -58,14 +64,14 @@ func Test_CustomRule1(t *testing.T) { Value: "123456", Data: "123456", } - err := gvalid.CheckStruct(st, nil) + err := gvalid.CheckStruct(context.TODO(), st, nil) t.Assert(err, nil) }) } func Test_CustomRule2(t *testing.T) { rule := "required-map" - err := gvalid.RegisterRule(rule, func(rule string, value interface{}, message string, params map[string]interface{}) error { + err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { m := gconv.Map(value) if len(m) == 0 { return errors.New(message) @@ -76,8 +82,8 @@ func Test_CustomRule2(t *testing.T) { // Check. gtest.C(t, func(t *gtest.T) { errStr := "data map should not be empty" - t.Assert(gvalid.Check(g.Map{}, rule, errStr).String(), errStr) - t.Assert(gvalid.Check(g.Map{"k": "v"}, rule, errStr).String(), nil) + t.Assert(gvalid.CheckValue(context.TODO(), g.Map{}, rule, errStr).String(), errStr) + t.Assert(gvalid.CheckValue(context.TODO(), g.Map{"k": "v"}, rule, errStr), nil) }) // Error with struct validation. gtest.C(t, func(t *gtest.T) { @@ -89,7 +95,7 @@ func Test_CustomRule2(t *testing.T) { Value: map[string]string{}, Data: "123456", } - err := gvalid.CheckStruct(st, nil) + err := gvalid.CheckStruct(context.TODO(), st, nil) t.Assert(err.String(), "自定义错误") }) // No error with struct validation. @@ -102,14 +108,14 @@ func Test_CustomRule2(t *testing.T) { Value: map[string]string{"k": "v"}, Data: "123456", } - err := gvalid.CheckStruct(st, nil) + err := gvalid.CheckStruct(context.TODO(), st, nil) t.Assert(err, nil) }) } func Test_CustomRule_AllowEmpty(t *testing.T) { rule := "allow-empty-str" - err := gvalid.RegisterRule(rule, func(rule string, value interface{}, message string, params map[string]interface{}) error { + err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { s := gconv.String(value) if len(s) == 0 || s == "gf" { return nil @@ -120,9 +126,9 @@ func Test_CustomRule_AllowEmpty(t *testing.T) { // Check. gtest.C(t, func(t *gtest.T) { errStr := "error" - t.Assert(gvalid.Check("", rule, errStr).String(), "") - t.Assert(gvalid.Check("gf", rule, errStr).String(), "") - t.Assert(gvalid.Check("gf2", rule, errStr).String(), errStr) + t.Assert(gvalid.CheckValue(context.TODO(), "", rule, errStr), nil) + t.Assert(gvalid.CheckValue(context.TODO(), "gf", rule, errStr), nil) + t.Assert(gvalid.CheckValue(context.TODO(), "gf2", rule, errStr).String(), errStr) }) // Error with struct validation. gtest.C(t, func(t *gtest.T) { @@ -134,8 +140,8 @@ func Test_CustomRule_AllowEmpty(t *testing.T) { Value: "", Data: "123456", } - err := gvalid.CheckStruct(st, nil) - t.Assert(err.String(), "") + err := gvalid.CheckStruct(context.TODO(), st, nil) + t.Assert(err, nil) }) // No error with struct validation. gtest.C(t, func(t *gtest.T) { @@ -147,7 +153,122 @@ func Test_CustomRule_AllowEmpty(t *testing.T) { Value: "john", Data: "123456", } - err := gvalid.CheckStruct(st, nil) + err := gvalid.CheckStruct(context.TODO(), st, nil) t.Assert(err.String(), "自定义错误") }) } + +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) + if len(pass) != 6 { + return errors.New(message) + } + if m := gconv.Map(data); m["data"] != pass { + return errors.New(message) + } + return nil + } + gtest.C(t, func(t *gtest.T) { + err := g.Validator().Rules(ruleName).Messages("custom message").RuleFunc(ruleName, ruleFunc).CheckValue("123456") + t.Assert(err.String(), "custom message") + err = g.Validator(). + Rules(ruleName). + Messages("custom message"). + Data(g.Map{"data": "123456"}). + RuleFunc(ruleName, ruleFunc). + CheckValue("123456") + t.AssertNil(err) + }) + // Error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123", + Data: "123456", + } + err := g.Validator().RuleFunc(ruleName, ruleFunc).CheckStruct(st) + t.Assert(err.String(), "自定义错误") + }) + // No error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123456", + Data: "123456", + } + err := g.Validator().RuleFunc(ruleName, ruleFunc).CheckStruct(st) + t.AssertNil(err) + }) +} + +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) + if len(pass) != 6 { + return errors.New(message) + } + if m := gconv.Map(data); m["data"] != pass { + return errors.New(message) + } + return nil + } + gtest.C(t, func(t *gtest.T) { + err := g.Validator(). + Rules(ruleName). + Messages("custom message"). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }).CheckValue("123456") + t.Assert(err.String(), "custom message") + err = g.Validator(). + Rules(ruleName). + Messages("custom message"). + Data(g.Map{"data": "123456"}). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }). + CheckValue("123456") + t.AssertNil(err) + }) + // Error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123", + Data: "123456", + } + err := g.Validator(). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }).CheckStruct(st) + t.Assert(err.String(), "自定义错误") + }) + // No error with struct validation. + gtest.C(t, func(t *gtest.T) { + type T struct { + Value string `v:"uid@custom_1#自定义错误"` + Data string `p:"data"` + } + st := &T{ + Value: "123456", + Data: "123456", + } + err := g.Validator(). + RuleFuncMap(map[string]gvalid.RuleFunc{ + ruleName: ruleFunc, + }).CheckStruct(st) + t.AssertNil(err) + }) +} diff --git a/util/gvalid/gvalid_z_unit_customerror_test.go b/util/gvalid/gvalid_z_unit_customerror_test.go index 9f73afa4b..75bc8f21a 100755 --- a/util/gvalid/gvalid_z_unit_customerror_test.go +++ b/util/gvalid/gvalid_z_unit_customerror_test.go @@ -7,6 +7,7 @@ package gvalid_test import ( + "context" "strings" "testing" @@ -19,7 +20,7 @@ func Test_Map(t *testing.T) { var ( rule = "ipv4" val = "0.0.0" - err = gvalid.Check(val, rule, nil) + err = gvalid.CheckValue(context.TODO(), val, rule, nil) msg = map[string]string{ "ipv4": "The value must be a valid IPv4 address", } @@ -33,7 +34,7 @@ func Test_FirstString(t *testing.T) { var ( rule = "ipv4" val = "0.0.0" - err = gvalid.Check(val, rule, nil) + err = gvalid.CheckValue(context.TODO(), val, rule, nil) n = err.FirstString() ) t.Assert(n, "The value must be a valid IPv4 address") @@ -46,7 +47,7 @@ func Test_CustomError1(t *testing.T) { "integer": "请输入一个整数", "length": "参数长度不对啊老铁", } - e := gvalid.Check("6.66", rule, msgs) + e := gvalid.CheckValue(context.TODO(), "6.66", rule, msgs) if e == nil || len(e.Map()) != 2 { t.Error("规则校验失败") } else { @@ -66,7 +67,7 @@ func Test_CustomError1(t *testing.T) { func Test_CustomError2(t *testing.T) { rule := "integer|length:6,16" msgs := "请输入一个整数|参数长度不对啊老铁" - e := gvalid.Check("6.66", rule, msgs) + e := gvalid.CheckValue(context.TODO(), "6.66", rule, msgs) if e == nil || len(e.Map()) != 2 { t.Error("规则校验失败") } else { diff --git a/util/gvalid/gvalid_z_unit_i18n_test.go b/util/gvalid/gvalid_z_unit_i18n_test.go new file mode 100644 index 000000000..5294c7e3c --- /dev/null +++ b/util/gvalid/gvalid_z_unit_i18n_test.go @@ -0,0 +1,50 @@ +// 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 gvalid_test + +import ( + "context" + "github.com/gogf/gf/debug/gdebug" + "github.com/gogf/gf/i18n/gi18n" + "github.com/gogf/gf/util/gvalid" + "testing" + + "github.com/gogf/gf/test/gtest" +) + +func TestValidator_I18n(t *testing.T) { + var ( + err gvalid.Error + i18nManager = gi18n.New(gi18n.Options{Path: gdebug.TestDataPath("i18n")}) + ctxCn = gi18n.WithLanguage(context.TODO(), "cn") + validator = gvalid.New().I18n(i18nManager) + ) + gtest.C(t, func(t *gtest.T) { + err = validator.Rules("required").CheckValue("") + t.Assert(err.String(), "The field is required") + + err = validator.Ctx(ctxCn).Rules("required").CheckValue("") + t.Assert(err.String(), "字段不能为空") + }) + gtest.C(t, func(t *gtest.T) { + err = validator.Ctx(ctxCn).Rules("required").Messages("CustomMessage").CheckValue("") + t.Assert(err.String(), "自定义错误") + }) + gtest.C(t, func(t *gtest.T) { + type Params struct { + Page int `v:"required|min:1 # page is required"` + Size int `v:"required|between:1,100 # size is required"` + ProjectId int `v:"between:1,10000 # project id must between :min, :max"` + } + obj := &Params{ + Page: 1, + Size: 10, + } + err := validator.Ctx(ctxCn).CheckStruct(obj) + t.Assert(err.String(), "项目ID必须大于等于1并且要小于等于10000") + }) +} diff --git a/util/gvalid/i18n/cn/validation.toml b/util/gvalid/i18n/cn/validation.toml index fab6dfba6..fb6c7c787 100644 --- a/util/gvalid/i18n/cn/validation.toml +++ b/util/gvalid/i18n/cn/validation.toml @@ -28,6 +28,7 @@ "gf.gvalid.rule.length" = ":attribute 字段长度为:min到:max个字符" "gf.gvalid.rule.min-length" = ":attribute 字段最小长度为:min" "gf.gvalid.rule.max-length" = ":attribute 字段最大长度为:max" +"gf.gvalid.rule.size" = ":attribute 字段长度必须为:size" "gf.gvalid.rule.between" = ":attribute 字段大小为:min到:max" "gf.gvalid.rule.min" = ":attribute 字段最小值为:min" "gf.gvalid.rule.max" = ":attribute 字段最大值为:max" diff --git a/util/gvalid/i18n/en/validation.toml b/util/gvalid/i18n/en/validation.toml index 7dc56cab0..4de5880d2 100644 --- a/util/gvalid/i18n/en/validation.toml +++ b/util/gvalid/i18n/en/validation.toml @@ -28,6 +28,7 @@ "gf.gvalid.rule.length" = "The :attribute value length must be between :min and :max" "gf.gvalid.rule.min-length" = "The :attribute value length must be equal or greater than :min" "gf.gvalid.rule.max-length" = "The :attribute value length must be equal or lesser than :max" +"gf.gvalid.rule.size" = "The :attribute value length must be :size" "gf.gvalid.rule.between" = "The :attribute value must be between :min and :max" "gf.gvalid.rule.min" = "The :attribute value must be equal or greater than :min" "gf.gvalid.rule.max" = "The :attribute value must be equal or lesser than :max" diff --git a/util/gvalid/testdata/i18n/cn/validation.toml b/util/gvalid/testdata/i18n/cn/validation.toml new file mode 100644 index 000000000..531aaa2ca --- /dev/null +++ b/util/gvalid/testdata/i18n/cn/validation.toml @@ -0,0 +1,49 @@ +"gf.gvalid.rule.required" = ":attribute 字段不能为空" +"gf.gvalid.rule.required-if" = ":attribute 字段不能为空" +"gf.gvalid.rule.required-unless" = ":attribute 字段不能为空" +"gf.gvalid.rule.required-with" = ":attribute 字段不能为空" +"gf.gvalid.rule.required-with-all" = ":attribute 字段不能为空" +"gf.gvalid.rule.required-without" = ":attribute 字段不能为空" +"gf.gvalid.rule.required-without-all" = ":attribute 字段不能为空" +"gf.gvalid.rule.date" = ":attribute 日期格式不正确" +"gf.gvalid.rule.date-format" = ":attribute 日期格式不满足:format" +"gf.gvalid.rule.email" = ":attribute 邮箱地址格式不正确" +"gf.gvalid.rule.phone" = ":attribute 手机号码格式不正确" +"gf.gvalid.rule.phone-loose" = ":attribute 手机号码格式不正确" +"gf.gvalid.rule.telephone" = ":attribute 电话号码格式不正确" +"gf.gvalid.rule.passport" = ":attribute 账号格式不合法,必需以字母开头,只能包含字母、数字和下划线,长度在6~18之间" +"gf.gvalid.rule.password" = ":attribute 密码格式不合法,密码格式为任意6-18位的可见字符" +"gf.gvalid.rule.password2" = ":attribute 密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母和数字" +"gf.gvalid.rule.password3" = ":attribute 密码格式不合法,密码格式为任意6-18位的可见字符,必须包含大小写字母、数字和特殊字符" +"gf.gvalid.rule.postcode" = ":attribute 邮政编码不正确" +"gf.gvalid.rule.resident-id" = ":attribute 身份证号码格式不正确" +"gf.gvalid.rule.bank-card" = ":attribute 银行卡号格式不正确" +"gf.gvalid.rule.qq" = ":attribute QQ号码格式不正确" +"gf.gvalid.rule.ip" = ":attribute IP地址格式不正确" +"gf.gvalid.rule.ipv4" = ":attribute IPv4地址格式不正确" +"gf.gvalid.rule.ipv6" = ":attribute IPv6地址格式不正确" +"gf.gvalid.rule.mac" = ":attribute MAC地址格式不正确" +"gf.gvalid.rule.url" = ":attribute URL地址格式不正确" +"gf.gvalid.rule.domain" = ":attribute 域名格式不正确" +"gf.gvalid.rule.length" = ":attribute 字段长度为:min到:max个字符" +"gf.gvalid.rule.min-length" = ":attribute 字段最小长度为:min" +"gf.gvalid.rule.max-length" = ":attribute 字段最大长度为:max" +"gf.gvalid.rule.size" = ":attribute 字段长度为必须为:size" +"gf.gvalid.rule.between" = ":attribute 字段大小为:min到:max" +"gf.gvalid.rule.min" = ":attribute 字段最小值为:min" +"gf.gvalid.rule.max" = ":attribute 字段最大值为:max" +"gf.gvalid.rule.json" = ":attribute 字段应当为JSON格式" +"gf.gvalid.rule.xml" = ":attribute 字段应当为XML格式" +"gf.gvalid.rule.array" = ":attribute 字段应当为数组" +"gf.gvalid.rule.integer" = ":attribute 字段应当为整数" +"gf.gvalid.rule.float" = ":attribute 字段应当为浮点数" +"gf.gvalid.rule.boolean" = ":attribute 字段应当为布尔值" +"gf.gvalid.rule.same" = ":attribute 字段值必须和:field相同" +"gf.gvalid.rule.different" = ":attribute 字段值不能与:field相同" +"gf.gvalid.rule.in" = ":attribute 字段值不合法" +"gf.gvalid.rule.not-in" = ":attribute 字段值不合法" +"gf.gvalid.rule.regex" = ":attribute 字段值不合法" +"gf.gvalid.rule.__default__" = ":attribute 字段值不合法" + +"CustomMessage" = "自定义错误" +"project id must between :min, :max" = "项目ID必须大于等于:min并且要小于等于:max" \ No newline at end of file diff --git a/util/gvalid/testdata/i18n/en/validation.toml b/util/gvalid/testdata/i18n/en/validation.toml new file mode 100644 index 000000000..4de5880d2 --- /dev/null +++ b/util/gvalid/testdata/i18n/en/validation.toml @@ -0,0 +1,46 @@ +"gf.gvalid.rule.required" = "The :attribute field is required" +"gf.gvalid.rule.required-if" = "The :attribute field is required" +"gf.gvalid.rule.required-unless" = "The :attribute field is required" +"gf.gvalid.rule.required-with" = "The :attribute field is required" +"gf.gvalid.rule.required-with-all" = "The :attribute field is required" +"gf.gvalid.rule.required-without" = "The :attribute field is required" +"gf.gvalid.rule.required-without-all" = "The :attribute field is required" +"gf.gvalid.rule.date" = "The :attribute value is not a valid date" +"gf.gvalid.rule.date-format" = "The :attribute value does not match the format :format" +"gf.gvalid.rule.email" = "The :attribute value must be a valid email address" +"gf.gvalid.rule.phone" = "The :attribute value must be a valid phone number" +"gf.gvalid.rule.phone-loose" = "The :attribute value must be a valid phone number" +"gf.gvalid.rule.telephone" = "The :attribute value must be a valid telephone number" +"gf.gvalid.rule.passport" = "The :attribute value is not a valid passport format" +"gf.gvalid.rule.password" = "The :attribute value is not a valid passport format" +"gf.gvalid.rule.password2" = "The :attribute value is not a valid passport format" +"gf.gvalid.rule.password3" = "The :attribute value is not a valid passport format" +"gf.gvalid.rule.postcode" = "The :attribute value is not a valid passport format" +"gf.gvalid.rule.resident-id" = "The :attribute value is not a valid resident id number" +"gf.gvalid.rule.bank-card" = "The :attribute value must be a valid bank card number" +"gf.gvalid.rule.qq" = "The :attribute value must be a valid QQ number" +"gf.gvalid.rule.ip" = "The :attribute value must be a valid IP address" +"gf.gvalid.rule.ipv4" = "The :attribute value must be a valid IPv4 address" +"gf.gvalid.rule.ipv6" = "The :attribute value must be a valid IPv6 address" +"gf.gvalid.rule.mac" = "The :attribute value must be a valid MAC address" +"gf.gvalid.rule.url" = "The :attribute value must be a valid URL address" +"gf.gvalid.rule.domain" = "The :attribute value must be a valid domain format" +"gf.gvalid.rule.length" = "The :attribute value length must be between :min and :max" +"gf.gvalid.rule.min-length" = "The :attribute value length must be equal or greater than :min" +"gf.gvalid.rule.max-length" = "The :attribute value length must be equal or lesser than :max" +"gf.gvalid.rule.size" = "The :attribute value length must be :size" +"gf.gvalid.rule.between" = "The :attribute value must be between :min and :max" +"gf.gvalid.rule.min" = "The :attribute value must be equal or greater than :min" +"gf.gvalid.rule.max" = "The :attribute value must be equal or lesser than :max" +"gf.gvalid.rule.json" = "The :attribute value must be a valid JSON string" +"gf.gvalid.rule.xml" = "The :attribute value must be a valid XML string" +"gf.gvalid.rule.array" = "The :attribute value must be an array" +"gf.gvalid.rule.integer" = "The :attribute value must be an integer" +"gf.gvalid.rule.float" = "The :attribute value must be a float" +"gf.gvalid.rule.boolean" = "The :attribute value field must be true or false" +"gf.gvalid.rule.same" = "The :attribute value must be the same as field :field" +"gf.gvalid.rule.different" = "The :attribute value must be different from field :field" +"gf.gvalid.rule.in" = "The :attribute value is not in acceptable range" +"gf.gvalid.rule.not-in" = "The :attribute value is not in acceptable range" +"gf.gvalid.rule.regex" = "The :attribute value is invalid" +"gf.gvalid.rule.__default__" = "The :attribute value is invalid" \ No newline at end of file diff --git a/version.go b/version.go index b22017481..d2895b5ad 100644 --- a/version.go +++ b/version.go @@ -1,4 +1,4 @@ package gf -const VERSION = "v1.15.4" +const VERSION = "v1.16.0" const AUTHORS = "john"