Compare commits

..

55 Commits

Author SHA1 Message Date
9706a9c768 version updates 2020-10-26 21:20:34 +08:00
cee67a8d4e improve urlencoding handling for parameters posted along with file uploading 2020-10-26 20:21:09 +08:00
87650557fd remove debugging codes from package gtime 2020-10-26 19:06:27 +08:00
d3bf52f12f fix issue in unit testing case for package gi18n 2020-10-26 19:00:11 +08:00
6b13a4849b improve package gi18n 2020-10-25 17:33:14 +08:00
8e380c0d9d improve package gtime/gconv for map converting 2020-10-25 11:33:30 +08:00
0caf4bfcec improve gconv.StructDeep 2020-10-25 10:47:47 +08:00
9c3b978b50 improve package ghttp and internal/structs 2020-10-22 15:16:31 +08:00
ab689a7792 improve gutil.Dump 2020-10-22 09:24:57 +08:00
846646d92d improve json validation rule for package gvalid 2020-10-22 09:11:38 +08:00
2eb2b89432 improve gconv.Struct* by doing the converting using json.Unmarshal if given params is json string/bytes 2020-10-21 14:09:16 +08:00
43441a8218 allow custom validation rule validate empty or nil values 2020-10-21 00:08:36 +08:00
561a541fa1 add custom CreatedAt/UpdatedAt/DeletedAt filed name configuration for package gdb 2020-10-20 21:01:39 +08:00
ffe9ecc141 improve package internal/empty 2020-10-20 14:07:01 +08:00
77f7884604 add function gutil.Try/g.Try;improve error string for gconv.Struct 2020-10-20 13:36:43 +08:00
0a203d1e22 fix issue in struct converting for ghttp.Request 2020-10-19 11:29:41 +08:00
f4f4550483 improve package gerror 2020-10-18 20:18:55 +08:00
e87226a092 improve package gerror 2020-10-18 11:29:09 +08:00
391a3ec9bd version update 2020-10-18 11:26:19 +08:00
dd5cd31ef5 add automatic data key to field name mapping feature for package gdb 2020-10-17 18:16:13 +08:00
de92e804fe Merge branch 'master' of https://github.com/gogf/gf 2020-10-17 17:17:27 +08:00
7725d9aaaf add Current/Next function for package gerror 2020-10-17 17:17:10 +08:00
bd3e25adea Merge pull request #946 from yuancjun/patch-1
Update ghttp_server_config.go
2020-10-15 20:41:07 +08:00
0b1d49af4b upgrade fsnotify to v1.4.9 2020-10-15 20:14:35 +08:00
7efa9e351e remove default initialization function and add lazy initialization feature for package gfsnotify 2020-10-15 20:07:21 +08:00
0b0141954b Update ghttp_server_config.go 2020-10-15 18:04:32 +08:00
82b531fbfb add domain support for domain of ghttp.Server 2020-10-14 21:18:11 +08:00
67fb626339 add configuration SessionCookieOutput for ghttp.Server 2020-10-14 20:52:26 +08:00
9044d5f05d fix issue in custom session id lost for ghttp.Request 2020-10-13 20:34:31 +08:00
47663aa1f1 fix issue of EOF in ghttp.Request when no data posted using form-data 2020-10-13 20:12:54 +08:00
63c0aab19c improve package gtime 2020-10-12 23:32:32 +08:00
261216f5e4 improve structure sql for package gdb 2020-10-12 23:22:56 +08:00
f88b799d67 add more unit testing case for package gdb 2020-10-11 22:21:03 +08:00
9c48dd172c add auto-json support for slice/struct attribute for data inserting of package gdb 2020-10-10 17:29:38 +08:00
09ce105eee improve gdb.Model.Fields/FieldsEx for package gdb 2020-10-10 14:00:10 +08:00
651aa895f8 improve function addWordBoundariesToNumbers for package gstr 2020-10-10 13:38:28 +08:00
0509e41198 recover number for unit testing case of package gconv 2020-10-10 00:04:55 +08:00
1b40d6a53a do tx.Rollback if there's panic in gdb.Transaction 2020-10-09 23:42:33 +08:00
f9189d48d1 remove type ValueFunc from package gcache 2020-10-09 23:36:39 +08:00
849874a247 improve adapter definition for package gcache 2020-10-09 20:59:49 +08:00
3f6510bae7 fix issue in gconv.Struct 2020-09-29 23:47:37 +08:00
a585a26c39 improve custom rule function type for package gvalid 2020-09-29 23:25:20 +08:00
9943966a86 improva function formatSql for package gdb 2020-09-29 22:53:44 +08:00
3617e51c01 improve gdb.Model.ScanList 2020-09-28 23:52:02 +08:00
c931032f08 comment update for gdb.Model.Unscoped 2020-09-27 23:37:40 +08:00
37b286eaa4 improve performance for gconv.Maps;comment update for package gcache 2020-09-27 22:35:02 +08:00
619287c273 improve cache feature for package gdb 2020-09-27 00:15:11 +08:00
aeb9b68298 improve adapter feature for package gcache 2020-09-26 22:44:07 +08:00
bae8f6315b comment updat 2020-09-26 21:13:09 +08:00
a6f70f8935 comment updat 2020-09-26 21:00:28 +08:00
acca6f4009 add adapter feature for package gcache 2020-09-26 20:47:29 +08:00
727fdd2391 improve unit testing case for association feature for package gdb 2020-09-25 08:33:22 +08:00
1d174e00c0 improve package internal/intlog 2020-09-24 23:46:19 +08:00
a5e3e2f5ba change g.SetDebug function to control the debugging information for framework 2020-09-24 23:40:44 +08:00
da43c2d52f improve example for package gview 2020-09-22 20:12:34 +08:00
104 changed files with 2734 additions and 996 deletions

View File

@ -1,10 +1,15 @@
# MySQL数据库配置
# MySQL.
[database]
debug = true
link = "mysql:root:12345678@tcp(127.0.0.1:3306)/test?parseTime=true&loc=Local"
MaxOpen = 100
# Redis.
[redis]
default = "127.0.0.1:6379,0"
cache = "127.0.0.1:6379,1"
#[database]
# [[database.default]]
# type = "mysql"

View File

@ -3,6 +3,7 @@ package main
import (
"github.com/gogf/gf/database/gdb"
"github.com/gogf/gf/util/gutil"
"time"
)
func main() {
@ -10,7 +11,7 @@ func main() {
Host: "127.0.0.1",
Port: "3306",
User: "root",
Pass: "123456",
Pass: "12345678",
Name: "test",
Type: "mysql",
Role: "master",
@ -20,19 +21,20 @@ func main() {
if err != nil {
panic(err)
}
//db.GetCache().SetAdapter(adapter.NewRedis(g.Redis()))
// 开启调试模式以便于记录所有执行的SQL
db.SetDebug(true)
// 执行2次查询并将查询结果缓存3秒并可执行缓存名称(可选)
for i := 0; i < 2; i++ {
r, _ := db.Table("user").Cache(3, "vip-user").Where("uid=?", 1).One()
gutil.Dump(r.ToMap())
for i := 0; i < 3; i++ {
r, _ := db.Table("user").Cache(3000*time.Second).Where("id=?", 1).One()
gutil.Dump(r.Map())
}
// 执行更新操作,并清理指定名称的查询缓存
db.Table("user").Cache(-1, "vip-user").Data(gdb.Map{"name": "smith"}).Where("uid=?", 1).Update()
//db.Table("user").Cache(-1, "vip-user").Data(gdb.Map{"name": "smith"}).Where("id=?", 1).Update()
// 再次执行查询,启用查询缓存特性
r, _ := db.Table("user").Cache(3, "vip-user").Where("uid=?", 1).One()
gutil.Dump(r.ToMap())
//r, _ := db.Table("user").Cache(300000*time.Second, "vip-user").Where("id=?", 1).One()
//gutil.Dump(r.Map())
}

View File

@ -2,25 +2,14 @@ package main
import (
"fmt"
"github.com/gogf/gf/i18n/gi18n"
"github.com/gogf/gf/frame/g"
)
func main() {
t := gi18n.New()
t.SetPath("/Users/john/Workspace/Go/GOPATH/src/github.com/gogf/gf/.example/i18n/gi18n/i18n")
t.SetLanguage("en")
fmt.Println(t.Translate(`hello`))
fmt.Println(t.Translate(`{#hello}{#world}!`))
t.SetLanguage("ja")
fmt.Println(t.Translate(`hello`))
fmt.Println(t.Translate(`{#hello}{#world}!`))
t.SetLanguage("ru")
fmt.Println(t.Translate(`hello`))
fmt.Println(t.Translate(`{#hello}{#world}!`))
fmt.Println(t.Translate(`hello`, "zh-CN"))
fmt.Println(t.Translate(`{#hello}{#world}!`, "zh-CN"))
var (
orderId = 865271654
orderAmount = 99.8
)
fmt.Println(g.I18n().Tfl(`en`, `{#OrderPaid}`, orderId, orderAmount))
fmt.Println(g.I18n().Tfl(`zh-CN`, `{#OrderPaid}`, orderId, orderAmount))
}

View File

@ -1,3 +1 @@
hello = "Hello"
world = "World"
OrderPaid = "You have successfully complete order #%d payment, paid amount: ¥%0.2f."

View File

@ -1,2 +1 @@
hello = "你好"
world = "世界"
OrderPaid = "您已成功完成订单号 #%d 支付,支付金额¥%.2f。"

View File

@ -3,15 +3,21 @@ package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
"github.com/gogf/gf/os/glog"
)
type GetById struct {
Id *g.Var `p:"id" v:"required|integer#id不能为空|id必须为整数"`
}
func main() {
s := g.Server()
s.SetIndexFolder(true)
s.BindHandler("/", func(r *ghttp.Request) {
glog.Println(r.Header)
r.Response.Write("hello world")
var idInfo *GetById
if err := r.Parse(&idInfo); err != nil {
r.Response.Write(err)
}
r.Response.Write("ok")
})
s.SetPort(8999)
s.Run()

View File

@ -1,12 +1,13 @@
package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/net/ghttp"
)
func main() {
s := ghttp.GetServer()
s.EnablePProf()
s := g.Server()
s.Domain("localhost").EnablePProf()
s.BindHandler("/", func(r *ghttp.Request) {
r.Response.Writeln("哈喽世界!")
})

View File

@ -2,19 +2,18 @@ package main
import (
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/frame/gmvc"
"github.com/gogf/gf/net/ghttp"
)
type Controller struct {
gmvc.Controller
}
func (c *Controller) Test() {
c.View.Display("layout.html")
}
func main() {
s := g.Server()
s.BindControllerMethod("/", new(Controller), "Test")
s.BindHandler("/", func(r *ghttp.Request) {
r.Response.WriteTpl("layout.html", g.Map{
"header": "This is header",
"container": "This is container",
"footer": "This is footer",
})
})
s.SetPort(8199)
s.Run()
}

View File

@ -1,3 +1,3 @@
{{define "container"}}
<h1>CONTAINER</h1>
<h1>{{.container}}</h1>
{{end}}

View File

@ -1,3 +1,3 @@
{{define "footer"}}
<h1>FOOTER</h1>
<h1>{{.footer}}</h1>
{{end}}

View File

@ -1,3 +1,3 @@
{{define "header"}}
<h1>HEADER</h1>
<h1>{{.header}}</h1>
{{end}}

View File

@ -2,14 +2,14 @@
<html>
<head>
<title>GoFrame Layout</title>
{{template "header"}}
{{template "header" .}}
</head>
<body>
<div class="container">
{{template "container"}}
{{template "container" .}}
</div>
<div class="footer">
{{template "footer"}}
{{template "footer" .}}
</div>
</body>
</html>

View File

@ -9,11 +9,13 @@ func main() {
s := g.Server()
s.BindHandler("/main1", func(r *ghttp.Request) {
r.Response.WriteTpl("layout.html", g.Map{
"name": "smith",
"mainTpl": "main/main1.html",
})
})
s.BindHandler("/main2", func(r *ghttp.Request) {
r.Response.WriteTpl("layout.html", g.Map{
"name": "john",
"mainTpl": "main/main2.html",
})
})

View File

@ -11,7 +11,7 @@ func main() {
fmt.Println(1)
gutil.Throw("error")
fmt.Println(2)
}, func(err interface{}) {
}, func(err error) {
fmt.Println(err)
})
}

View File

@ -128,6 +128,7 @@ type DB interface {
GetDryRun() bool
SetLogger(logger *glog.Logger)
GetLogger() *glog.Logger
GetConfig() *ConfigNode
SetMaxIdleConnCount(n int)
SetMaxOpenConnCount(n int)
SetMaxConnLifetime(d time.Duration)
@ -156,7 +157,7 @@ type DB interface {
// Internal methods.
// ===========================================================================
filterFields(schema, table string, data map[string]interface{}) map[string]interface{}
mappingAndFilterData(schema, table string, data map[string]interface{}, filter bool) (map[string]interface{}, error)
convertValue(fieldValue interface{}, fieldType string) interface{}
rowsToResult(rows *sql.Rows) (Result, error)
}
@ -166,11 +167,12 @@ type Core struct {
DB DB // DB interface object.
group string // Configuration group name.
debug *gtype.Bool // Enable debug mode for the database.
cache *gcache.Cache // Cache manager.
cache *gcache.Cache // Cache manager, SQL result cache only.
schema *gtype.String // Custom schema for this object.
dryrun *gtype.Bool // Dry run.
prefix string // Table prefix.
logger *glog.Logger // Logger.
config *ConfigNode // Current config node.
maxIdleConnCount int // Max idle connection count.
maxOpenConnCount int // Max open connection count.
maxConnLifetime time.Duration // Max TTL for a connection.
@ -304,6 +306,7 @@ func New(group ...string) (db DB, err error) {
dryrun: gtype.NewBool(),
logger: glog.New(),
prefix: node.Prefix,
config: node,
maxIdleConnCount: gDEFAULT_CONN_MAX_IDLE_COUNT,
maxConnLifetime: gDEFAULT_CONN_MAX_LIFE_TIME, // Default max connection life time if user does not configure.
}
@ -438,11 +441,11 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
node = &n
}
// Cache the underlying connection pool object by node.
v := c.cache.GetOrSetFuncLock(node.String(), func() interface{} {
v, _ := gcache.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
return nil, err
}
if c.maxIdleConnCount > 0 {
sqlDb.SetMaxIdleConns(c.maxIdleConnCount)
@ -461,7 +464,7 @@ func (c *Core) getSqlDb(master bool, schema ...string) (sqlDb *sql.DB, err error
} else if node.MaxConnLifetime > 0 {
sqlDb.SetConnMaxLifetime(node.MaxConnLifetime * time.Second)
}
return sqlDb
return sqlDb, nil
}, 0)
if v != nil && sqlDb == nil {
sqlDb = v.(*sql.DB)

View File

@ -11,6 +11,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/gogf/gf/text/gstr"
"reflect"
"strings"
@ -310,6 +311,11 @@ func (c *Core) Transaction(f func(tx *TX) error) (err error) {
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
@ -426,10 +432,10 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b
if _, ok := data.(apiInterfaces); ok {
return c.DB.DoBatchInsert(link, table, data, option, batch...)
} else {
dataMap = DataToMapDeep(data)
dataMap = ConvertDataForTableRecord(data)
}
case reflect.Map:
dataMap = DataToMapDeep(data)
dataMap = ConvertDataForTableRecord(data)
default:
return result, errors.New(fmt.Sprint("unsupported data type:", reflectKind))
}
@ -450,7 +456,7 @@ func (c *Core) DoInsert(link Link, table string, data interface{}, option int, b
for k, _ := range dataMap {
// If it's SAVE operation,
// do not automatically update the creating time.
if utils.EqualFoldWithoutChars(k, gSOFT_FIELD_NAME_CREATE) {
if c.isSoftCreatedFiledName(k) {
continue
}
if len(updateStr) > 0 {
@ -521,10 +527,10 @@ func (c *Core) BatchSave(table string, list interface{}, batch ...int) (sql.Resu
func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option int, batch ...int) (result sql.Result, err error) {
table = c.DB.QuotePrefixTableName(table)
var (
keys []string
values []string
params []interface{}
listMap List
keys []string // Field names.
values []string // Value holder string array, like: (?,?,?)
params []interface{} // Values that will be committed to underlying database driver.
listMap List // The data list that passed from caller.
)
switch value := list.(type) {
case Result:
@ -549,10 +555,10 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
case reflect.Slice, reflect.Array:
listMap = make(List, rv.Len())
for i := 0; i < rv.Len(); i++ {
listMap[i] = DataToMapDeep(rv.Index(i).Interface())
listMap[i] = ConvertDataForTableRecord(rv.Index(i).Interface())
}
case reflect.Map:
listMap = List{DataToMapDeep(value)}
listMap = List{ConvertDataForTableRecord(value)}
case reflect.Struct:
if v, ok := value.(apiInterfaces); ok {
var (
@ -560,11 +566,11 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
list = make(List, len(array))
)
for i := 0; i < len(array); i++ {
list[i] = DataToMapDeep(array[i])
list[i] = ConvertDataForTableRecord(array[i])
}
listMap = list
} else {
listMap = List{DataToMapDeep(value)}
listMap = List{ConvertDataForTableRecord(value)}
}
default:
return result, errors.New(fmt.Sprint("unsupported list type:", kind))
@ -597,7 +603,7 @@ func (c *Core) DoBatchInsert(link Link, table string, list interface{}, option i
for _, k := range keys {
// If it's SAVE operation,
// do not automatically update the creating time.
if utils.EqualFoldWithoutChars(k, gSOFT_FIELD_NAME_CREATE) {
if c.isSoftCreatedFiledName(k) {
continue
}
if len(updateStr) > 0 {
@ -690,7 +696,7 @@ func (c *Core) DoUpdate(link Link, table string, data interface{}, condition str
case reflect.Map, reflect.Struct:
var (
fields []string
dataMap = DataToMapDeep(data)
dataMap = ConvertDataForTableRecord(data)
)
for k, v := range dataMap {
fields = append(fields, c.DB.QuoteWord(k)+"=?")
@ -824,3 +830,22 @@ func (c *Core) HasTable(name string) (bool, error) {
}
return false, nil
}
// isSoftCreatedFiledName checks and returns whether given filed name is an automatic-filled created time.
func (c *Core) isSoftCreatedFiledName(fieldName string) bool {
if fieldName == "" {
return false
}
if config := c.DB.GetConfig(); config.CreatedAt != "" {
if utils.EqualFoldWithoutChars(fieldName, config.CreatedAt) {
return true
}
return gstr.InArray(append([]string{config.CreatedAt}, createdFiledNames...), fieldName)
}
for _, v := range createdFiledNames {
if utils.EqualFoldWithoutChars(fieldName, v) {
return true
}
}
return false
}

View File

@ -39,6 +39,9 @@ type ConfigNode struct {
DryRun bool // (Optional) Dry run, which does SELECT but no INSERT/UPDATE/DELETE statements.
Weight int // (Optional) Weight for load balance calculating, it's useless if there's just one node.
Charset string // (Optional, "utf8mb4" in default) Custom charset when operating on database.
CreatedAt string // (Optional) The filed name of table for automatic-filled created datetime.
UpdatedAt string // (Optional) The filed name of table for automatic-filled updated datetime.
DeletedAt string // (Optional) The filed name of table for automatic-filled updated datetime.
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.
@ -212,3 +215,8 @@ func (c *Core) SetSchema(schema string) {
func (c *Core) GetSchema() string {
return c.schema.Val()
}
// GetConfig returns the current used node configuration.
func (c *Core) GetConfig() *ConfigNode {
return c.config
}

View File

@ -7,6 +7,8 @@
package gdb
import (
"github.com/gogf/gf/errors/gerror"
"github.com/gogf/gf/util/gutil"
"strings"
"time"
@ -145,15 +147,50 @@ func (c *Core) convertValue(fieldValue interface{}, fieldType string) interface{
}
// filterFields removes all key-value pairs which are not the field of given table.
func (c *Core) filterFields(schema, table string, data map[string]interface{}) map[string]interface{} {
// It must use data copy here to avoid its changing the origin data map.
newDataMap := make(map[string]interface{}, len(data))
if fields, err := c.DB.TableFields(table, schema); err == nil {
for k, v := range data {
if _, ok := fields[k]; ok {
newDataMap[k] = v
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 {
fieldsKeyMap := make(map[string]interface{}, len(fieldsMap))
for k, _ := range fieldsMap {
fieldsKeyMap[k] = nil
}
// Automatic data key to table field name mapping.
var foundKey string
for dataKey, dataValue := range data {
if _, ok := fieldsKeyMap[dataKey]; !ok {
foundKey, _ = gutil.MapPossibleItemByKey(fieldsKeyMap, dataKey)
if foundKey != "" {
data[foundKey] = dataValue
delete(data, dataKey)
} else if !filter {
if schema != "" {
return nil, gerror.Newf(`no column of name "%s" found for table "%s" in schema "%s"`, dataKey, table, schema)
}
return nil, gerror.Newf(`no column of name "%s" found for table "%s"`, dataKey, table)
}
}
}
// Data filtering.
if filter {
for dataKey, _ := range data {
if _, ok := fieldsMap[dataKey]; !ok {
delete(data, dataKey)
}
}
}
}
return newDataMap
return data, nil
}
//// filterFields removes all key-value pairs which are not the field of given table.
//func (c *Core) filterFields(schema, table string, data map[string]interface{}) map[string]interface{} {
// // It must use data copy here to avoid its changing the origin data map.
// newDataMap := make(map[string]interface{}, len(data))
// if fields, err := c.DB.TableFields(table, schema); err == nil {
// for k, v := range data {
// if _, ok := fields[k]; ok {
// newDataMap[k] = v
// }
// }
// }
// return newDataMap
//}

View File

@ -15,6 +15,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/gogf/gf/os/gcache"
"strconv"
"strings"
@ -198,47 +199,58 @@ func (d *DriverMssql) TableFields(table string, schema ...string) (fields map[st
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
fmt.Sprintf(`mssql_table_fields_%s_%s`, table, checkSchema), func() interface{} {
var result Result
var link *sql.DB
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`mssql_table_fields_%s_%s`, table, checkSchema),
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(link, fmt.Sprintf(`
SELECT a.name Field,
structureSql := fmt.Sprintf(`
SELECT
a.name Field,
CASE b.name
WHEN 'datetime' THEN 'datetime'
WHEN 'numeric' THEN b.name + '(' + convert(varchar(20),a.xprec) + ',' + convert(varchar(20),a.xscale) + ')'
WHEN 'char' THEN b.name + '(' + convert(varchar(20),a.length)+ ')'
WHEN 'varchar' THEN b.name + '(' + convert(varchar(20),a.length)+ ')'
ELSE b.name + '(' + convert(varchar(20),a.length)+ ')' END as TYPE,
case when a.isnullable=1 then 'YES'else 'NO' end as [Null],
case when exists(SELECT 1 FROM sysobjects where xtype='PK' and name in (
SELECT name FROM sysindexes WHERE indid in(
SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid
))) then 'PRI' else '' end AS [Key],
case when COLUMNPROPERTY(a.id,a.name,'IsIdentity')=1 then 'auto_increment'else '' end Extra,
isnull(e.text,'') as [Default],
WHEN 'numeric' THEN b.name + '(' + convert(varchar(20), a.xprec) + ',' + convert(varchar(20), a.xscale) + ')'
WHEN 'char' THEN b.name + '(' + convert(varchar(20), a.length)+ ')'
WHEN 'varchar' THEN b.name + '(' + convert(varchar(20), a.length)+ ')'
ELSE b.name + '(' + convert(varchar(20),a.length)+ ')' END AS Type,
CASE WHEN a.isnullable=1 THEN 'YES' ELSE 'NO' end AS [Null],
CASE WHEN exists (
SELECT 1 FROM sysobjects WHERE xtype='PK' AND name IN (
SELECT name FROM sysindexes WHERE indid IN (
SELECT indid FROM sysindexkeys WHERE id = a.id AND colid=a.colid
)
)
) THEN 'PRI' ELSE '' END AS [Key],
CASE WHEN COLUMNPROPERTY(a.id,a.name,'IsIdentity')=1 THEN 'auto_increment' ELSE '' END Extra,
isnull(e.text,'') AS [Default],
isnull(g.[value],'') AS [Comment]
FROM syscolumns a
left join systypes b on a.xtype=b.xtype and a.xusertype=b.xusertype
inner join sysobjects d on a.id=d.id and d.xtype='U' and d.name<>'dtproperties'
left join syscomments e on a.cdefault=e.id
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)))
FROM syscolumns a
LEFT JOIN systypes b ON a.xtype=b.xtype AND a.xusertype=b.xusertype
INNER JOIN sysobjects d ON a.id=d.id AND d.xtype='U' AND d.name<>'dtproperties'
LEFT JOIN syscomments e ON a.cdefault=e.id
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
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
fields[strings.ToLower(m["FIELD"].String())] = &TableField{
fields[strings.ToLower(m["Field"].String())] = &TableField{
Index: i,
Name: strings.ToLower(m["FIELD"].String()),
Type: strings.ToLower(m["TYPE"].String()),
Name: strings.ToLower(m["Field"].String()),
Type: strings.ToLower(m["Type"].String()),
Null: m["Null"].Bool(),
Key: m["Key"].String(),
Default: m["Default"].Val(),
@ -246,7 +258,7 @@ func (d *DriverMssql) TableFields(table string, schema ...string) (fields map[st
Comment: m["Comment"].String(),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/text/gregex"
"github.com/gogf/gf/text/gstr"
@ -102,21 +103,23 @@ func (d *DriverMysql) TableFields(table string, schema ...string) (fields map[st
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.cache.GetOrSetFunc(
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`mysql_table_fields_%s_%s`, table, checkSchema),
func() interface{} {
var result Result
var link *sql.DB
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(
link,
fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.DB.QuoteWord(table)),
)
if err != nil {
return nil
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
@ -131,7 +134,7 @@ func (d *DriverMysql) TableFields(table string, schema ...string) (fields map[st
Comment: m["Comment"].String(),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -16,6 +16,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/text/gstr"
"reflect"
"strconv"
@ -158,18 +159,24 @@ func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[s
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`oracle_table_fields_%s_%s`, table, checkSchema),
func() interface{} {
func() (interface{}, error) {
result := (Result)(nil)
result, err = d.DB.GetAll(fmt.Sprintf(`
SELECT COLUMN_NAME AS FIELD, CASE DATA_TYPE
WHEN 'NUMBER' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
WHEN 'FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE
FROM USER_TAB_COLUMNS WHERE TABLE_NAME = '%s' ORDER BY COLUMN_ID`, strings.ToUpper(table)))
structureSql := fmt.Sprintf(`
SELECT
COLUMN_NAME AS FIELD,
CASE DATA_TYPE
WHEN 'NUMBER' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
WHEN 'FLOAT' THEN DATA_TYPE||'('||DATA_PRECISION||','||DATA_SCALE||')'
ELSE DATA_TYPE||'('||DATA_LENGTH||')' END AS TYPE
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
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
@ -179,7 +186,7 @@ func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[s
Type: strings.ToLower(m["TYPE"].String()),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)
@ -189,24 +196,26 @@ func (d *DriverOracle) TableFields(table string, schema ...string) (fields map[s
func (d *DriverOracle) getTableUniqueIndex(table string) (fields map[string]map[string]string, err error) {
table = strings.ToUpper(table)
v := d.DB.GetCache().GetOrSetFunc("table_unique_index_"+table, func() interface{} {
res := (Result)(nil)
res, err = d.DB.GetAll(fmt.Sprintf(`
v, _ := gcache.GetOrSetFunc(
"table_unique_index_"+table,
func() (interface{}, error) {
res := (Result)(nil)
res, err = d.DB.GetAll(fmt.Sprintf(`
SELECT INDEX_NAME,COLUMN_NAME,CHAR_LENGTH FROM USER_IND_COLUMNS
WHERE TABLE_NAME = '%s'
AND INDEX_NAME IN(SELECT INDEX_NAME FROM USER_INDEXES WHERE TABLE_NAME='%s' AND UNIQUENESS='UNIQUE')
ORDER BY INDEX_NAME,COLUMN_POSITION`, table, table))
if err != nil {
return nil
}
fields := make(map[string]map[string]string)
for _, v := range res {
mm := make(map[string]string)
mm[v["COLUMN_NAME"].String()] = v["CHAR_LENGTH"].String()
fields[v["INDEX_NAME"].String()] = mm
}
return fields
}, 0)
if err != nil {
return nil, err
}
fields := make(map[string]map[string]string)
for _, v := range res {
mm := make(map[string]string)
mm[v["COLUMN_NAME"].String()] = v["CHAR_LENGTH"].String()
fields[v["INDEX_NAME"].String()] = mm
}
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]map[string]string)
}
@ -232,7 +241,7 @@ func (d *DriverOracle) DoInsert(link Link, table string, data interface{}, optio
case reflect.Map:
fallthrough
case reflect.Struct:
dataMap = DataToMapDeep(data)
dataMap = ConvertDataForTableRecord(data)
default:
return result, errors.New(fmt.Sprint("unsupported data type:", kind))
}
@ -352,12 +361,12 @@ func (d *DriverOracle) DoBatchInsert(link Link, table string, list interface{},
case reflect.Array:
listMap = make(List, rv.Len())
for i := 0; i < rv.Len(); i++ {
listMap[i] = DataToMapDeep(rv.Index(i).Interface())
listMap[i] = ConvertDataForTableRecord(rv.Index(i).Interface())
}
case reflect.Map:
fallthrough
case reflect.Struct:
listMap = List{Map(DataToMapDeep(list))}
listMap = List{Map(ConvertDataForTableRecord(list))}
default:
return result, errors.New(fmt.Sprint("unsupported list type:", kind))
}

View File

@ -16,6 +16,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/text/gstr"
"strings"
@ -107,21 +108,28 @@ func (d *DriverPgsql) TableFields(table string, schema ...string) (fields map[st
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
fmt.Sprintf(`pgsql_table_fields_%s_%s`, table, checkSchema), func() interface{} {
var result Result
var link *sql.DB
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`pgsql_table_fields_%s_%s`, table, checkSchema),
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(link, fmt.Sprintf(`
SELECT a.attname AS field, t.typname AS type 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 := fmt.Sprintf(`
SELECT a.attname AS field, t.typname AS type 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
return nil, err
}
fields = make(map[string]*TableField)
@ -132,7 +140,7 @@ func (d *DriverPgsql) TableFields(table string, schema ...string) (fields map[st
Type: m["type"].String(),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -15,6 +15,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/os/gcache"
"github.com/gogf/gf/os/gfile"
"github.com/gogf/gf/text/gstr"
"strings"
@ -98,17 +99,20 @@ func (d *DriverSqlite) TableFields(table string, schema ...string) (fields map[s
if len(schema) > 0 && schema[0] != "" {
checkSchema = schema[0]
}
v := d.DB.GetCache().GetOrSetFunc(
fmt.Sprintf(`sqlite_table_fields_%s_%s`, table, checkSchema), func() interface{} {
var result Result
var link *sql.DB
v, _ := gcache.GetOrSetFunc(
fmt.Sprintf(`sqlite_table_fields_%s_%s`, table, checkSchema),
func() (interface{}, error) {
var (
result Result
link *sql.DB
)
link, err = d.DB.GetSlave(checkSchema)
if err != nil {
return nil
return nil, err
}
result, err = d.DB.DoGetAll(link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, table))
if err != nil {
return nil
return nil, err
}
fields = make(map[string]*TableField)
for i, m := range result {
@ -118,7 +122,7 @@ func (d *DriverSqlite) TableFields(table string, schema ...string) (fields map[s
Type: strings.ToLower(m["type"].String()),
}
}
return fields
return fields, nil
}, 0)
if err == nil {
fields = v.(map[string]*TableField)

View File

@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"github.com/gogf/gf/internal/empty"
"github.com/gogf/gf/internal/json"
"github.com/gogf/gf/internal/utils"
"github.com/gogf/gf/os/gtime"
"github.com/gogf/gf/util/gutil"
@ -97,9 +98,52 @@ func GetInsertOperationByOption(option int) string {
return operator
}
// DataToMapDeep converts struct object to map type recursively.
// ConvertDataForTableRecord is a very important function, which does converting for any data that
// will be inserted into table as a record.
//
// The parameter <obj> should be type of *map/map/*struct/struct.
// It supports inherit struct definition for struct.
func ConvertDataForTableRecord(value interface{}) map[string]interface{} {
var (
rvValue reflect.Value
rvKind reflect.Kind
data = DataToMapDeep(value)
)
for k, v := range data {
rvValue = reflect.ValueOf(v)
rvKind = rvValue.Kind()
for rvKind == reflect.Ptr {
rvValue = rvValue.Elem()
rvKind = rvValue.Kind()
}
switch rvKind {
case reflect.Slice, reflect.Array, reflect.Map:
// It should ignore the bytes type.
if _, ok := v.([]byte); !ok {
// Convert the value to JSON.
data[k], _ = json.Marshal(v)
}
case reflect.Struct:
switch v.(type) {
case time.Time, *time.Time, gtime.Time, *gtime.Time:
continue
default:
// Use string conversion in default.
if s, ok := v.(apiString); ok {
data[k] = s.String()
} else {
// Convert the value to JSON.
data[k], _ = json.Marshal(v)
}
}
}
}
return data
}
// DataToMapDeep converts <value> to map type recursively.
// The parameter <value> should be type of *map/map/*struct/struct.
// It supports inherit struct definition for struct.
func DataToMapDeep(value interface{}) map[string]interface{} {
if v, ok := value.(apiMapStrAny); ok {
return v.MapStrAny()
@ -342,9 +386,10 @@ func GetPrimaryKeyCondition(primary string, where ...interface{}) (newWhereCondi
// The internal handleArguments function might be called twice during the SQL procedure,
// but do not worry about it, it's safe and efficient.
func formatSql(sql string, args []interface{}) (newSql string, newArgs []interface{}) {
sql = gstr.Trim(sql)
sql = gstr.Replace(sql, "\n", " ")
sql, _ = gregex.ReplaceString(`\s{2,}`, ` `, sql)
// DO NOT do this as there may be multiple lines and comments in the sql.
// sql = gstr.Trim(sql)
// sql = gstr.Replace(sql, "\n", " ")
// sql, _ = gregex.ReplaceString(`\s{2,}`, ` `, sql)
return handleArguments(sql, args)
}

View File

@ -12,13 +12,6 @@ import (
"github.com/gogf/gf/os/gtime"
)
// Unscoped disables the soft deleting feature.
func (m *Model) Unscoped() *Model {
model := m.getModel()
model.unscoped = true
return model
}
// Delete does "DELETE FROM ... " statement for the model.
// The optional parameter <where> is the same as the parameter of Model.Where function,
// see Model.Where.
@ -32,7 +25,7 @@ func (m *Model) Delete(where ...interface{}) (result sql.Result, err error) {
}
}()
var (
fieldNameDelete = m.getSoftFieldNameDelete()
fieldNameDelete = m.getSoftFieldNameDeleted()
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false)
)
// Soft deleting.

View File

@ -24,18 +24,24 @@ func (m *Model) Filter() *Model {
}
// Fields sets the operation fields of the model, multiple fields joined using char ','.
func (m *Model) Fields(fields string) *Model {
model := m.getModel()
model.fields = fields
return model
func (m *Model) Fields(fields ...string) *Model {
if len(fields) > 0 {
model := m.getModel()
model.fields = gstr.Join(fields, ",")
return model
}
return m
}
// FieldsEx sets the excluded operation fields of the model, multiple fields joined using char ','.
// Note that this function supports only single table operations.
func (m *Model) FieldsEx(fields string) *Model {
model := m.getModel()
model.fieldsEx = fields
return model
func (m *Model) FieldsEx(fields ...string) *Model {
if len(fields) > 0 {
model := m.getModel()
model.fieldsEx = gstr.Join(fields, ",")
return model
}
return m
}
// Deprecated, use GetFieldsStr instead.

View File

@ -56,8 +56,10 @@ func (m *Model) Data(data ...interface{}) *Model {
case Map:
model.data = params
default:
rv := reflect.ValueOf(params)
kind := rv.Kind()
var (
rv = reflect.ValueOf(params)
kind = rv.Kind()
)
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()
@ -66,11 +68,11 @@ func (m *Model) Data(data ...interface{}) *Model {
case reflect.Slice, reflect.Array:
list := make(List, rv.Len())
for i := 0; i < rv.Len(); i++ {
list[i] = DataToMapDeep(rv.Index(i).Interface())
list[i] = ConvertDataForTableRecord(rv.Index(i).Interface())
}
model.data = list
case reflect.Map:
model.data = DataToMapDeep(data[0])
model.data = ConvertDataForTableRecord(data[0])
case reflect.Struct:
if v, ok := data[0].(apiInterfaces); ok {
var (
@ -78,11 +80,11 @@ func (m *Model) Data(data ...interface{}) *Model {
list = make(List, len(array))
)
for i := 0; i < len(array); i++ {
list[i] = DataToMapDeep(array[i])
list[i] = ConvertDataForTableRecord(array[i])
}
model.data = list
} else {
model.data = DataToMapDeep(data[0])
model.data = ConvertDataForTableRecord(data[0])
}
default:
model.data = data[0]
@ -147,9 +149,9 @@ func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.
}
var (
nowString = gtime.Now().String()
fieldNameCreate = m.getSoftFieldNameCreate()
fieldNameUpdate = m.getSoftFieldNameUpdate()
fieldNameDelete = m.getSoftFieldNameDelete()
fieldNameCreate = m.getSoftFieldNameCreated()
fieldNameUpdate = m.getSoftFieldNameUpdated()
fieldNameDelete = m.getSoftFieldNameDeleted()
)
// Batch operation.
if list, ok := m.data.(List); ok {
@ -170,10 +172,14 @@ func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.
list[k] = v
}
}
newData, err := m.filterDataForInsertOrUpdate(list)
if err != nil {
return nil, err
}
return m.db.DoBatchInsert(
m.getLink(true),
m.tables,
m.filterDataForInsertOrUpdate(list),
newData,
option,
batch,
)
@ -190,10 +196,14 @@ func (m *Model) doInsertWithOption(option int, data ...interface{}) (result sql.
data[fieldNameUpdate] = nowString
}
}
newData, err := m.filterDataForInsertOrUpdate(data)
if err != nil {
return nil, err
}
return m.db.DoInsert(
m.getLink(true),
m.tables,
m.filterDataForInsertOrUpdate(data),
newData,
option,
)
}

View File

@ -10,6 +10,7 @@ import (
"fmt"
"github.com/gogf/gf/container/gset"
"github.com/gogf/gf/container/gvar"
"github.com/gogf/gf/internal/json"
"github.com/gogf/gf/text/gstr"
"github.com/gogf/gf/util/gconv"
"reflect"
@ -431,23 +432,35 @@ 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()
// Retrieve from cache.
if m.cacheEnabled && m.tx == nil {
cacheKey = m.cacheName
if len(cacheKey) == 0 {
cacheKey = sql + "/" + gconv.String(args)
cacheKey = sql + ", @PARAMS:" + gconv.String(args)
}
if v := m.db.GetCache().Get(cacheKey); v != nil {
return v.(Result), nil
if v, _ := cacheObj.GetVar(cacheKey); !v.IsNil() {
if result, ok := v.Val().(Result); ok {
// In-memory cache.
return result, nil
} else {
// Other cache, it needs conversion.
var result Result
if err = json.Unmarshal(v.Bytes(), &result); err != nil {
return nil, err
} else {
return result, nil
}
}
}
}
result, err = m.db.DoGetAll(m.getLink(false), sql, m.mergeArguments(args)...)
// Cache the result.
if cacheKey != "" && err == nil {
if m.cacheDuration < 0 {
m.db.GetCache().Remove(cacheKey)
cacheObj.Remove(cacheKey)
} else {
m.db.GetCache().Set(cacheKey, result, m.cacheDuration)
cacheObj.Set(cacheKey, result, m.cacheDuration)
}
}
return result, err

View File

@ -15,58 +15,82 @@ import (
"github.com/gogf/gf/util/gutil"
)
const (
gSOFT_FIELD_NAME_CREATE = "create_at"
gSOFT_FIELD_NAME_UPDATE = "update_at"
gSOFT_FIELD_NAME_DELETE = "delete_at"
var (
createdFiledNames = []string{"created_at", "create_at"} // Default filed names of table for automatic-filled created datetime.
updatedFiledNames = []string{"updated_at", "update_at"} // Default filed names of table for automatic-filled updated datetime.
deletedFiledNames = []string{"updated_at", "delete_at"} // Default filed names of table for automatic-filled deleted datetime.
)
// Unscoped disables the auto-update time feature for insert, update and delete options.
func (m *Model) Unscoped() *Model {
model := m.getModel()
model.unscoped = true
return model
}
// getSoftFieldNameCreate checks and returns the field name for record creating time.
// If there's no field name for storing creating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameCreate(table ...string) string {
func (m *Model) getSoftFieldNameCreated(table ...string) string {
tableName := ""
if len(table) > 0 {
tableName = table[0]
} else {
tableName = m.getPrimaryTableName()
}
return m.getSoftFieldName(tableName, gSOFT_FIELD_NAME_CREATE)
config := m.db.GetConfig()
if config.CreatedAt != "" {
return m.getSoftFieldName(tableName, append([]string{config.CreatedAt}, createdFiledNames...))
}
return m.getSoftFieldName(tableName, createdFiledNames)
}
// getSoftFieldNameUpdate checks and returns the field name for record updating time.
// If there's no field name for storing updating time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameUpdate(table ...string) (field string) {
func (m *Model) getSoftFieldNameUpdated(table ...string) (field string) {
tableName := ""
if len(table) > 0 {
tableName = table[0]
} else {
tableName = m.getPrimaryTableName()
}
return m.getSoftFieldName(tableName, gSOFT_FIELD_NAME_UPDATE)
config := m.db.GetConfig()
if config.UpdatedAt != "" {
return m.getSoftFieldName(tableName, append([]string{config.UpdatedAt}, updatedFiledNames...))
}
return m.getSoftFieldName(tableName, updatedFiledNames)
}
// getSoftFieldNameDelete checks and returns the field name for record deleting time.
// If there's no field name for storing deleting time, it returns an empty string.
// It checks the key with or without cases or chars '-'/'_'/'.'/' '.
func (m *Model) getSoftFieldNameDelete(table ...string) (field string) {
func (m *Model) getSoftFieldNameDeleted(table ...string) (field string) {
tableName := ""
if len(table) > 0 {
tableName = table[0]
} else {
tableName = m.getPrimaryTableName()
}
return m.getSoftFieldName(tableName, gSOFT_FIELD_NAME_DELETE)
config := m.db.GetConfig()
if config.UpdatedAt != "" {
return m.getSoftFieldName(tableName, append([]string{config.DeletedAt}, deletedFiledNames...))
}
return m.getSoftFieldName(tableName, deletedFiledNames)
}
// getSoftFieldName retrieves and returns the field name of the table for possible key.
func (m *Model) getSoftFieldName(table string, key string) (field string) {
func (m *Model) getSoftFieldName(table string, keys []string) (field string) {
fieldsMap, _ := m.db.TableFields(table)
if len(fieldsMap) > 0 {
field, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), key,
)
for _, key := range keys {
field, _ = gutil.MapPossibleItemByKey(
gconv.Map(fieldsMap), key,
)
if field != "" {
return
}
}
}
return
}
@ -103,7 +127,7 @@ func (m *Model) getConditionForSoftDeleting() string {
return conditionArray.Join(" AND ")
}
// Only one table.
if fieldName := m.getSoftFieldNameDelete(); fieldName != "" {
if fieldName := m.getSoftFieldNameDeleted(); fieldName != "" {
return fmt.Sprintf(`%s IS NULL`, m.db.QuoteWord(fieldName))
}
return ""
@ -122,7 +146,7 @@ func (m *Model) getConditionOfTableStringForSoftDeleting(s string) string {
} else {
table = array2[0]
}
field = m.getSoftFieldNameDelete(table)
field = m.getSoftFieldNameDeleted(table)
if field == "" {
return ""
}

View File

@ -42,9 +42,9 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
}
var (
updateData = m.data
fieldNameCreate = m.getSoftFieldNameCreate()
fieldNameUpdate = m.getSoftFieldNameUpdate()
fieldNameDelete = m.getSoftFieldNameDelete()
fieldNameCreate = m.getSoftFieldNameCreated()
fieldNameUpdate = m.getSoftFieldNameUpdated()
fieldNameDelete = m.getSoftFieldNameDeleted()
conditionWhere, conditionExtra, conditionArgs = m.formatCondition(false)
)
// Automatically update the record updating time.
@ -59,7 +59,7 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
}
switch refKind {
case reflect.Map, reflect.Struct:
dataMap := DataToMapDeep(m.data)
dataMap := ConvertDataForTableRecord(m.data)
gutil.MapDelete(dataMap, fieldNameCreate, fieldNameUpdate, fieldNameDelete)
if fieldNameUpdate != "" {
dataMap[fieldNameUpdate] = gtime.Now().String()
@ -73,10 +73,14 @@ func (m *Model) Update(dataAndWhere ...interface{}) (result sql.Result, err erro
updateData = updates
}
}
newData, err := m.filterDataForInsertOrUpdate(updateData)
if err != nil {
return nil, err
}
return m.db.DoUpdate(
m.getLink(true),
m.tables,
m.filterDataForInsertOrUpdate(updateData),
newData,
conditionWhere+conditionExtra,
m.mergeArguments(conditionArgs)...,
)

View File

@ -28,27 +28,33 @@ func (m *Model) getModel() *Model {
// filterDataForInsertOrUpdate does filter feature with data for inserting/updating operations.
// Note that, it does not filter list item, which is also type of map, for "omit empty" feature.
func (m *Model) filterDataForInsertOrUpdate(data interface{}) interface{} {
func (m *Model) filterDataForInsertOrUpdate(data interface{}) (interface{}, error) {
var err error
switch value := data.(type) {
case List:
for k, item := range value {
value[k] = m.doFilterDataMapForInsertOrUpdate(item, false)
value[k], err = m.doMappingAndFilterForInsertOrUpdateDataMap(item, false)
if err != nil {
return nil, err
}
}
return value
return value, nil
case Map:
return m.doFilterDataMapForInsertOrUpdate(value, true)
return m.doMappingAndFilterForInsertOrUpdateDataMap(value, true)
default:
return data
return data, nil
}
}
// doFilterDataMapForInsertOrUpdate does the filter features for map.
// doMappingAndFilterForInsertOrUpdateDataMap does the filter features for map.
// Note that, it does not filter list item, which is also type of map, for "omit empty" feature.
func (m *Model) doFilterDataMapForInsertOrUpdate(data Map, allowOmitEmpty bool) Map {
if m.filter {
data = m.db.filterFields(m.schema, m.tables, data)
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)
if err != nil {
return nil, err
}
// Remove key-value pairs of which the value is empty.
if allowOmitEmpty && m.option&OPTION_OMITEMPTY > 0 {
@ -103,7 +109,7 @@ func (m *Model) doFilterDataMapForInsertOrUpdate(data Map, allowOmitEmpty bool)
delete(data, v)
}
}
return data
return data, nil
}
// getLink returns the underlying database link object with configured <linkType> attribute.

View File

@ -181,7 +181,7 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
}
}
if len(relationDataMap) > 0 && !relationValue.IsValid() {
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
switch attrKind {
case reflect.Array, reflect.Slice:
@ -196,7 +196,7 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
}
} else {
// May be the attribute does not exist yet.
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
} else {
return fmt.Errorf(`relationKey should not be empty as field "%s" is slice`, attributeName)
@ -207,15 +207,25 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
if len(relationDataMap) > 0 {
relationField = relationValue.FieldByName(relationAttrName)
if relationField.IsValid() {
if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil {
v := relationDataMap[gconv.String(relationField.Interface())]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
} else {
// May be the attribute does not exist yet.
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
} else {
if err = gconv.Struct(r[i], e); err != nil {
v := r[i]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
}
@ -226,15 +236,25 @@ func (r Result) ScanList(listPointer interface{}, attributeName string, relation
if len(relationDataMap) > 0 {
relationField = relationValue.FieldByName(relationAttrName)
if relationField.IsValid() {
if err = gconv.Struct(relationDataMap[gconv.String(relationField.Interface())], e); err != nil {
v := relationDataMap[gconv.String(relationField.Interface())]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
} else {
// May be the attribute does not exist yet.
return fmt.Errorf(`invalid relation: %s, %s`, relation[0], relation[1])
return fmt.Errorf(`invalid relation: "%s:%s"`, relation[0], relation[1])
}
} else {
if err = gconv.Struct(r[i], e); err != nil {
v := r[i]
if v == nil {
// There's no relational data.
continue
}
if err = gconv.Struct(v, e); err != nil {
return err
}
}

View File

@ -17,7 +17,152 @@ import (
"github.com/gogf/gf/test/gtest"
)
func Test_Table_Relation(t *testing.T) {
func Test_Table_Relation_One(t *testing.T) {
var (
tableUser = "user_" + gtime.TimestampMicroStr()
tableUserDetail = "user_detail_" + gtime.TimestampMicroStr()
tableUserScores = "user_scores_" + gtime.TimestampMicroStr()
)
if _, err := db.Exec(fmt.Sprintf(`
CREATE TABLE %s (
uid int(10) unsigned NOT NULL AUTO_INCREMENT,
name varchar(45) NOT NULL,
PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, tableUser)); err != nil {
gtest.Error(err)
}
defer dropTable(tableUser)
if _, err := db.Exec(fmt.Sprintf(`
CREATE TABLE %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 %s (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
uid int(10) unsigned NOT NULL,
score int(10) unsigned NOT NULL,
course varchar(45) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
`, tableUserScores)); err != nil {
gtest.Error(err)
}
defer dropTable(tableUserScores)
type EntityUser struct {
Uid int `orm:"uid"`
Name string `orm:"name"`
}
type EntityUserDetail struct {
Uid int `orm:"uid"`
Address string `orm:"address"`
}
type EntityUserScores struct {
Id int `orm:"id"`
Uid int `orm:"uid"`
Score int `orm:"score"`
Course string `orm:"course"`
}
type Entity struct {
User *EntityUser
UserDetail *EntityUserDetail
UserScores []*EntityUserScores
}
// Initialize the data.
var err error
gtest.C(t, func(t *gtest.T) {
err = db.Transaction(func(tx *gdb.TX) error {
r, err := tx.Table(tableUser).Save(EntityUser{
Name: "john",
})
if err != nil {
return err
}
uid, err := r.LastInsertId()
if err != nil {
return err
}
_, err = tx.Table(tableUserDetail).Save(EntityUserDetail{
Uid: int(uid),
Address: "Beijing DongZhiMen #66",
})
if err != nil {
return err
}
_, err = tx.Table(tableUserScores).Save(g.Slice{
EntityUserScores{Uid: int(uid), Score: 100, Course: "math"},
EntityUserScores{Uid: int(uid), Score: 99, Course: "physics"},
})
return err
})
t.Assert(err, nil)
})
// Data check.
gtest.C(t, func(t *gtest.T) {
r, err := db.Table(tableUser).All()
t.Assert(err, nil)
t.Assert(r.Len(), 1)
t.Assert(r[0]["uid"].Int(), 1)
t.Assert(r[0]["name"].String(), "john")
r, err = db.Table(tableUserDetail).Where("uid", r[0]["uid"].Int()).All()
t.Assert(err, nil)
t.Assert(r.Len(), 1)
t.Assert(r[0]["uid"].Int(), 1)
t.Assert(r[0]["address"].String(), `Beijing DongZhiMen #66`)
r, err = db.Table(tableUserScores).Where("uid", r[0]["uid"].Int()).All()
t.Assert(err, nil)
t.Assert(r.Len(), 2)
t.Assert(r[0]["uid"].Int(), 1)
t.Assert(r[1]["uid"].Int(), 1)
t.Assert(r[0]["course"].String(), `math`)
t.Assert(r[1]["course"].String(), `physics`)
})
// Entity query.
gtest.C(t, func(t *gtest.T) {
var user Entity
// SELECT * FROM `user` WHERE `name`='john'
err := db.Table(tableUser).Scan(&user.User, "name", "john")
t.Assert(err, nil)
// SELECT * FROM `user_detail` WHERE `uid`=1
err = db.Table(tableUserDetail).Scan(&user.UserDetail, "uid", user.User.Uid)
t.Assert(err, nil)
// SELECT * FROM `user_scores` WHERE `uid`=1
err = db.Table(tableUserScores).Scan(&user.UserScores, "uid", user.User.Uid)
t.Assert(err, nil)
t.Assert(user.User, EntityUser{
Uid: 1,
Name: "john",
})
t.Assert(user.UserDetail, EntityUserDetail{
Uid: 1,
Address: "Beijing DongZhiMen #66",
})
t.Assert(user.UserScores, []EntityUserScores{
{Id: 1, Uid: 1, Course: "math", Score: 100},
{Id: 2, Uid: 1, Course: "physics", Score: 99},
})
})
}
func Test_Table_Relation_Many(t *testing.T) {
var (
tableUser = "user_" + gtime.TimestampMicroStr()
tableUserDetail = "user_detail_" + gtime.TimestampMicroStr()

View File

@ -204,9 +204,9 @@ CREATE TABLE %s (
gtest.C(t, func(t *gtest.T) {
model := db.Table(table1)
gtest.Assert(model.getSoftFieldNameCreate(table2), "createat")
gtest.Assert(model.getSoftFieldNameUpdate(table2), "updateat")
gtest.Assert(model.getSoftFieldNameDelete(table2), "deleteat")
gtest.Assert(model.getSoftFieldNameCreated(table2), "createat")
gtest.Assert(model.getSoftFieldNameUpdated(table2), "updateat")
gtest.Assert(model.getSoftFieldNameDeleted(table2), "deleteat")
})
}
@ -292,12 +292,12 @@ CREATE TABLE %s (
}
// Fix issue: https://github.com/gogf/gf/issues/819
func Test_Func_DataToMapDeep(t *testing.T) {
func Test_Func_ConvertDataForTableRecord(t *testing.T) {
type Test struct {
ResetPasswordTokenAt mysql.NullTime `orm:"reset_password_token_at"`
}
gtest.C(t, func(t *gtest.T) {
m := DataToMapDeep(new(Test))
m := ConvertDataForTableRecord(new(Test))
t.Assert(len(m), 1)
t.AssertNE(m["reset_password_token_at"], nil)
t.Assert(m["reset_password_token_at"], new(mysql.NullTime))

View File

@ -9,6 +9,7 @@ package gdb_test
import (
"fmt"
"github.com/gogf/gf/container/garray"
"github.com/gogf/gf/encoding/gparser"
"testing"
"time"
@ -183,6 +184,119 @@ func Test_DB_Insert(t *testing.T) {
})
}
// Fix issue: https://github.com/gogf/gf/issues/819
func Test_DB_Insert_WithStructAndSliceAttribute(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type Password struct {
Salt string `json:"salt"`
Pass string `json:"pass"`
}
data := g.Map{
"id": 1,
"passport": "t1",
"password": &Password{"123", "456"},
"nickname": []string{"A", "B", "C"},
"create_time": gtime.Now().String(),
}
_, err := db.Insert(table, data)
t.Assert(err, nil)
one, err := db.GetOne(fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.Assert(err, nil)
t.Assert(one["passport"], data["passport"])
t.Assert(one["create_time"], data["create_time"])
t.Assert(one["nickname"], gparser.MustToJson(data["nickname"]))
})
}
func Test_DB_Insert_KeyFieldNameMapping(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
}
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.Assert(err, nil)
one, err := db.GetOne(fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_DB_Upadte_KeyFieldNameMapping(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_10",
Password: "pass_10",
Nickname: "name_10",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Update(table, data, "id=1")
t.Assert(err, nil)
one, err := db.GetOne(fmt.Sprintf("SELECT * FROM %s WHERE id=?", table), 1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
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()
defer dropTable(table)

View File

@ -11,6 +11,7 @@ import (
"fmt"
"github.com/gogf/gf/container/garray"
"github.com/gogf/gf/container/gmap"
"github.com/gogf/gf/encoding/gparser"
"github.com/gogf/gf/util/gutil"
"testing"
"time"
@ -96,6 +97,140 @@ func Test_Model_Insert(t *testing.T) {
})
}
// Fix issue: https://github.com/gogf/gf/issues/819
func Test_Model_Insert_WithStructAndSliceAttribute(t *testing.T) {
table := createTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type Password struct {
Salt string `json:"salt"`
Pass string `json:"pass"`
}
data := g.Map{
"id": 1,
"passport": "t1",
"password": &Password{"123", "456"},
"nickname": []string{"A", "B", "C"},
"create_time": gtime.Now().String(),
}
_, err := db.Table(table).Data(data).Insert()
t.Assert(err, nil)
one, err := db.Table(table).One("id", 1)
t.Assert(err, nil)
t.Assert(one["passport"], data["passport"])
t.Assert(one["create_time"], data["create_time"])
t.Assert(one["nickname"], gparser.MustToJson(data["nickname"]))
})
}
func Test_Model_Insert_KeyFieldNameMapping(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
}
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.Assert(err, nil)
one, err := db.Model(table).FindOne(1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
func Test_Model_Update_KeyFieldNameMapping(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Passport string
Password string
Nickname string
CreateTime string
}
data := User{
Id: 1,
Passport: "user_10",
Password: "pass_10",
Nickname: "name_10",
CreateTime: "2020-10-10 12:00:01",
}
_, err := db.Model(table).Data(data).WherePri(1).Update()
t.Assert(err, nil)
one, err := db.Model(table).FindOne(1)
t.Assert(err, nil)
t.Assert(one["passport"], data.Passport)
t.Assert(one["create_time"], data.CreateTime)
t.Assert(one["nickname"], data.Nickname)
})
}
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)
gtest.C(t, func(t *gtest.T) {
data := g.Map{
"id": 1,
"passport": "t1",
"password": "p1",
"nickname": "n1",
"create_time": "2020-10-10 20:09:18.334",
}
_, err := db.Table(table).Data(data).Insert()
t.Assert(err, nil)
one, err := db.Table(table).One("id", 1)
t.Assert(err, nil)
t.Assert(one["passport"], data["passport"])
t.Assert(one["create_time"], "2020-10-10 20:09:18")
t.Assert(one["nickname"], data["nickname"])
})
}
func Test_Model_BatchInsertWithArrayStruct(t *testing.T) {
table := createTable()
defer dropTable(table)

View File

@ -151,7 +151,7 @@ CREATE TABLE %s (
}
func Test_SoftUpdateTime(t *testing.T) {
table := "time_test_table"
table := "time_test_table_" + gtime.TimestampNanoStr()
if _, err := db.Exec(fmt.Sprintf(`
CREATE TABLE %s (
id int(11) NOT NULL,

View File

@ -764,3 +764,31 @@ func Test_Transaction(t *testing.T) {
}
})
}
func Test_Transaction_Panic(t *testing.T) {
table := createInitTable()
defer dropTable(table)
gtest.C(t, func(t *gtest.T) {
err := db.Transaction(func(tx *gdb.TX) error {
if _, err := tx.Replace(table, g.Map{
"id": 1,
"passport": "USER_1",
"password": "PASS_1",
"nickname": "NAME_1",
"create_time": gtime.Now().String(),
}); err != nil {
t.Error(err)
}
panic("error")
return nil
})
t.AssertNE(err, nil)
if value, err := db.Table(table).Fields("nickname").Where("id", 1).Value(); err != nil {
gtest.Error(err)
} else {
t.Assert(value.String(), "name_1")
}
})
}

View File

@ -68,6 +68,7 @@ func StackWithFilters(filters []string, skip ...int) string {
file[0:len(goRootForFilter)] == goRootForFilter {
continue
}
// Custom filtering.
filtered = false
for _, filter := range filters {
if filter != "" && strings.Contains(file, filter) {

View File

@ -69,3 +69,31 @@ func Test_Load_New_CustomStruct(t *testing.T) {
t.Assert(s == `{"Id":1,"Name":"john"}` || s == `{"Name":"john","Id":1}`, true)
})
}
func Test_Load_New_HierarchicalStruct(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type Me struct {
Name string `json:"name"`
Score int `json:"score"`
Children []Me `json:"children"`
}
me := Me{
Name: "john",
Score: 100,
Children: []Me{
{
Name: "Bean",
Score: 99,
},
{
Name: "Sam",
Score: 98,
},
},
}
j := gjson.New(me)
t.Assert(j.Remove("children.0.score"), nil)
t.Assert(j.Remove("children.1.score"), nil)
t.Assert(j.MustToJsonString(), `{"children":[{"children":null,"name":"Bean"},{"children":null,"name":"Sam"}],"name":"john","score":100}`)
})
}

View File

@ -26,6 +26,12 @@ type ApiCause interface {
Cause() error
}
// ApiLevel is the interface for Current/Next feature.
type ApiLevel interface {
Current() error
Next() error
}
// New creates and returns an error which is formatted from given text.
func New(text string) error {
if text == "" {
@ -120,3 +126,27 @@ func Stack(err error) string {
}
return ""
}
// Current creates and returns the current level error.
// It returns nil if current level error is nil.
func Current(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ApiLevel); ok {
return e.Current()
}
return err
}
// Next returns the next level error.
// It returns nil if current level error or the next level error is nil.
func Next(err error) error {
if err == nil {
return nil
}
if e, ok := err.(ApiLevel); ok {
return e.Next()
}
return nil
}

View File

@ -27,6 +27,7 @@ const (
var (
// goRootForFilter is used for stack filtering purpose.
// Mainly for development environment.
goRootForFilter = runtime.GOROOT()
)
@ -36,8 +37,11 @@ func init() {
}
}
// Error implements the interface of Error, it returns the error as string.
// Error implements the interface of Error, it returns all the error as string.
func (err *Error) Error() string {
if err == nil {
return ""
}
if err.text != "" {
if err.error != nil {
return err.text + ": " + err.error.Error()
@ -49,6 +53,9 @@ func (err *Error) Error() string {
// Cause returns the root cause error.
func (err *Error) Cause() error {
if err == nil {
return nil
}
loop := err
for loop != nil {
if loop.error != nil {
@ -66,8 +73,8 @@ func (err *Error) Cause() error {
// Format formats the frame according to the fmt.Formatter interface.
//
// %v, %s : Print the error string;
// %-v, %-s : Print current error string;
// %v, %s : Print all the error string;
// %-v, %-s : Print current level error string;
// %+s : Print full stack error list;
// %+v : Print the error string and full stack error list;
func (err *Error) Format(s fmt.State, verb rune) {
@ -120,6 +127,28 @@ func (err *Error) Stack() string {
return buffer.String()
}
// Current creates and returns the current level error.
// It returns nil if current level error is nil.
func (err *Error) Current() error {
if err == nil {
return nil
}
return &Error{
error: nil,
stack: err.stack,
text: err.text,
}
}
// Next returns the next level error.
// It returns nil if current level error or the next level error is nil.
func (err *Error) Next() error {
if err == nil {
return nil
}
return err.error
}
// formatSubStack formats the stack for error.
func formatSubStack(st stack, buffer *bytes.Buffer) {
index := 1

View File

@ -127,3 +127,31 @@ func Test_Stack(t *testing.T) {
//fmt.Printf("%+v", err)
})
}
func Test_Current(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := errors.New("1")
err = gerror.Wrap(err, "2")
err = gerror.Wrap(err, "3")
t.Assert(err.Error(), "3: 2: 1")
t.Assert(gerror.Current(err).Error(), "3")
})
}
func Test_Next(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
err := errors.New("1")
err = gerror.Wrap(err, "2")
err = gerror.Wrap(err, "3")
t.Assert(err.Error(), "3: 2: 1")
err = gerror.Next(err)
t.Assert(err.Error(), "2: 1")
err = gerror.Next(err)
t.Assert(err.Error(), "1")
err = gerror.Next(err)
t.Assert(err, nil)
})
}

View File

@ -39,8 +39,15 @@ func Throw(exception interface{}) {
gutil.Throw(exception)
}
// TryCatch does the try...catch... mechanism.
func TryCatch(try func(), catch ...func(exception interface{})) {
// Try implements try... logistics using internal panic...recover.
// It returns error if any exception occurs, or else it returns nil.
func Try(try func()) (err error) {
return gutil.Try(try)
}
// TryCatch implements try...catch... logistics using internal panic...recover.
// It automatically calls function <catch> if any exception occurs ans passes the exception as an error.
func TryCatch(try func(), catch ...func(exception error)) {
gutil.TryCatch(try, catch...)
}

View File

@ -10,17 +10,14 @@ import (
"github.com/gogf/gf/os/glog"
)
// SetDebug disables/enables debug level for logging component globally.
func SetDebug(debug bool) {
glog.SetDebug(debug)
}
// SetLogLevel sets the logging level globally.
// Deprecated, use functions of package glog or g.Log() instead.
func SetLogLevel(level int) {
glog.SetLevel(level)
}
// GetLogLevel returns the global logging level.
// Deprecated, use functions of package glog or g.Log() instead.
func GetLogLevel() int {
return glog.GetLevel()
}

View File

@ -6,7 +6,17 @@
package g
import "github.com/gogf/gf/net/ghttp"
import (
"github.com/gogf/gf/internal/intlog"
"github.com/gogf/gf/net/ghttp"
)
// SetEnabled 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) {
intlog.SetEnabled(enabled)
}
// SetServerGraceful enables/disables graceful reload feature of http Web Server.
// This feature is disabled in default.

3
go.mod
View File

@ -5,7 +5,7 @@ go 1.11
require (
github.com/BurntSushi/toml v0.3.1
github.com/clbanning/mxj v1.8.5-0.20200714211355-ff02cfb8ea28
github.com/fsnotify/fsnotify v1.4.7
github.com/fsnotify/fsnotify v1.4.9
github.com/go-sql-driver/mysql v1.5.0
github.com/gomodule/redigo v2.0.0+incompatible
github.com/gorilla/websocket v1.4.1
@ -15,7 +15,6 @@ require (
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/olekukonko/tablewriter v0.0.1
golang.org/x/net v0.0.0-20200602114024-627f9648deb9
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
golang.org/x/text v0.3.2
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
)

View File

@ -32,14 +32,14 @@ func T(content string, language ...string) string {
return defaultManager.T(content, language...)
}
// TF is alias of TranslateFormat for convenience.
func TF(format string, values ...interface{}) string {
// Tf is alias of TranslateFormat for convenience.
func Tf(format string, values ...interface{}) string {
return defaultManager.TranslateFormat(format, values...)
}
// TFL is alias of TranslateFormatLang for convenience.
func TFL(format string, language string, values ...interface{}) string {
return defaultManager.TranslateFormatLang(format, language, values...)
// Tfl is alias of TranslateFormatLang for convenience.
func Tfl(language string, format string, values ...interface{}) string {
return defaultManager.TranslateFormatLang(language, format, values...)
}
// TranslateFormat translates, formats and returns the <format> with configured language
@ -52,7 +52,7 @@ func TranslateFormat(format string, values ...interface{}) string {
// and given <values>. The parameter <language> specifies custom translation language ignoring
// configured language. If <language> is given empty string, it uses the default configured
// language for the translation.
func TranslateFormatLang(format string, language string, values ...interface{}) string {
func TranslateFormatLang(language string, format string, values ...interface{}) string {
return defaultManager.TranslateFormatLang(format, language, values...)
}

View File

@ -124,14 +124,14 @@ func (m *Manager) T(content string, language ...string) string {
return m.Translate(content, language...)
}
// TF is alias of TranslateFormat for convenience.
func (m *Manager) TF(format string, values ...interface{}) string {
// 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(format string, language string, values ...interface{}) string {
return m.TranslateFormatLang(format, language, 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...)
}
// TranslateFormat translates, formats and returns the <format> with configured language
@ -144,7 +144,7 @@ func (m *Manager) TranslateFormat(format string, values ...interface{}) string {
// and given <values>. The parameter <language> specifies custom translation language ignoring
// configured language. If <language> is given empty string, it uses the default configured
// language for the translation.
func (m *Manager) TranslateFormatLang(format string, language string, values ...interface{}) string {
func (m *Manager) TranslateFormatLang(language string, format string, values ...interface{}) string {
return fmt.Sprintf(m.Translate(format, language), values...)
}

View File

@ -74,24 +74,24 @@ func Test_Basic(t *testing.T) {
}
func Test_TranslateFormat(t *testing.T) {
// TF
// Tf
gtest.C(t, func(t *gtest.T) {
i18n := gi18n.New(gi18n.Options{
Path: gdebug.TestDataPath("i18n"),
})
i18n.SetLanguage("none")
t.Assert(i18n.TF("{#hello}{#world} %d", 2020), "{#hello}{#world} 2020")
t.Assert(i18n.Tf("{#hello}{#world} %d", 2020), "{#hello}{#world} 2020")
i18n.SetLanguage("ja")
t.Assert(i18n.TF("{#hello}{#world} %d", 2020), "こんにちは世界 2020")
t.Assert(i18n.Tf("{#hello}{#world} %d", 2020), "こんにちは世界 2020")
})
// TFL
// Tfl
gtest.C(t, func(t *gtest.T) {
i18n := gi18n.New(gi18n.Options{
Path: gdebug.TestDataPath("i18n"),
})
t.Assert(i18n.TFL("{#hello}{#world} %d", "ja", 2020), "こんにちは世界 2020")
t.Assert(i18n.TFL("{#hello}{#world} %d", "zh-CN", 2020), "你好世界 2020")
t.Assert(i18n.Tfl("ja", "{#hello}{#world} %d", 2020), "こんにちは世界 2020")
t.Assert(i18n.Tfl("zh-CN", "{#hello}{#world} %d", 2020), "你好世界 2020")
})
}

View File

@ -69,12 +69,21 @@ func IsEmpty(value interface{}) bool {
default:
// Common interfaces checks.
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.

View File

@ -33,9 +33,12 @@ func init() {
}
// SetEnabled enables/disables the internal logging manually.
// Note that this function is not current safe, be aware of the DATA RACE.
// Note that this function is not concurrent safe, be aware of the DATA RACE.
func SetEnabled(enabled bool) {
isGFDebug = enabled
// If they're the same, it does not write the <isGFDebug> but only reading operation.
if isGFDebug != enabled {
isGFDebug = enabled
}
}
// IsEnabled checks and returns whether current process is in GF development.

View File

@ -22,6 +22,21 @@ import (
//
// Note that it only retrieves the exported attributes with first letter up-case from struct.
func MapField(pointer interface{}, priority []string, recursive bool) map[string]*Field {
// If <pointer> points to an invalid address, for example a nil variable,
// it here creates an empty struct using reflect feature.
var (
tempValue reflect.Value
pointerValue = reflect.ValueOf(pointer)
)
for pointerValue.Kind() == reflect.Ptr {
tempValue = pointerValue.Elem()
if !tempValue.IsValid() {
pointer = reflect.New(pointerValue.Type().Elem()).Elem()
break
} else {
pointerValue = tempValue
}
}
var (
fields []*structs.Field
fieldMap = make(map[string]*Field)
@ -59,8 +74,10 @@ func MapField(pointer interface{}, priority []string, recursive bool) map[string
}
}
if recursive {
rv := reflect.ValueOf(field.Value())
kind := rv.Kind()
var (
rv = reflect.ValueOf(field.Value())
kind = rv.Kind()
)
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()

View File

@ -27,6 +27,21 @@ func TagFields(pointer interface{}, priority []string, recursive bool) []*Field
// tag internally.
// The parameter <pointer> should be type of struct/*struct.
func doTagFields(pointer interface{}, priority []string, recursive bool, tagMap map[string]struct{}) []*Field {
// If <pointer> points to an invalid address, for example a nil variable,
// it here creates an empty struct using reflect feature.
var (
tempValue reflect.Value
pointerValue = reflect.ValueOf(pointer)
)
for pointerValue.Kind() == reflect.Ptr {
tempValue = pointerValue.Elem()
if !tempValue.IsValid() {
pointer = reflect.New(pointerValue.Type().Elem()).Elem()
break
} else {
pointerValue = tempValue
}
}
var fields []*structs.Field
if v, ok := pointer.(reflect.Value); ok {
fields = structs.Fields(v.Interface())

View File

@ -71,3 +71,20 @@ func Test_Basic(t *testing.T) {
t.Assert(structs.TagMapName(user2, []string{"params"}, true), g.Map{"password1": "Pass1", "password2": "Pass2"})
})
}
func Test_StructOfNilPointer(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
type User struct {
Id int
Name string `params:"name"`
Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"`
}
var user *User
t.Assert(structs.TagMapName(user, []string{"params"}, true), g.Map{"name": "Name", "pass": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"params"}, true), g.Map{"name": "Name", "pass": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"params", "my-tag1"}, true), g.Map{"name": "Name", "pass": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"my-tag1", "params"}, true), g.Map{"name": "Name", "pass1": "Pass"})
t.Assert(structs.TagMapName(&user, []string{"my-tag2", "params"}, true), g.Map{"name": "Name", "pass2": "Pass"})
})
}

View File

@ -123,7 +123,7 @@ func (c *Client) DoRequest(method, url string, data ...interface{}) (resp *Clien
param = BuildParams(data[0])
}
}
req := (*http.Request)(nil)
var req *http.Request
if strings.Contains(param, "@file:") {
// File uploading request.
buffer := new(bytes.Buffer)

View File

@ -8,12 +8,17 @@ package ghttp
import (
"github.com/gogf/gf/errors/gerror"
"github.com/gogf/gf/text/gstr"
"strings"
"github.com/gogf/gf/encoding/gurl"
"github.com/gogf/gf/util/gconv"
)
const (
fileUploadingKey = "@file:"
)
// BuildParams builds the request string for the http client. The <params> can be type of:
// string/[]byte/map/struct/*struct.
//
@ -38,13 +43,22 @@ func BuildParams(params interface{}, noUrlEncode ...bool) (encodedParamStr strin
if len(noUrlEncode) == 1 {
urlEncode = !noUrlEncode[0]
}
// If there's file uploading, it ignores the url encoding.
if urlEncode {
for k, v := range m {
if gstr.Contains(k, fileUploadingKey) || gstr.Contains(gconv.String(v), fileUploadingKey) {
urlEncode = false
break
}
}
}
s := ""
for k, v := range m {
if len(encodedParamStr) > 0 {
encodedParamStr += "&"
}
s = gconv.String(v)
if urlEncode && len(s) > 6 && strings.Compare(s[0:6], "@file:") != 0 {
if urlEncode && len(s) > 6 && strings.Compare(s[0:6], fileUploadingKey) != 0 {
s = gurl.Encode(s)
}
encodedParamStr += k + "=" + s

View File

@ -120,7 +120,7 @@ func (m *Middleware) Next() {
// There should be a "Next" function to be called in the middleware in order to manage the workflow.
loop = false
}
}, func(exception interface{}) {
}, func(exception error) {
if e, ok := exception.(gerror.ApiStack); ok {
// It's already an error that has stack info.
m.request.error = e.(error)

View File

@ -25,6 +25,12 @@ import (
"strings"
)
const (
parseTypeRequest = 0
parseTypeQuery = 1
parseTypeForm = 2
)
var (
// xmlHeaderBytes is the most common XML format header.
xmlHeaderBytes = []byte("<?xml")
@ -37,11 +43,26 @@ var (
// The parameter <pointer> can be type of: *struct/**struct/*[]struct/*[]*struct.
//
// It supports single and multiple struct convertion:
// 1. Single struct, post content like: {"id":1, "name":"john"}
// 1. Single struct, post content like: {"id":1, "name":"john"} or ?id=1&name=john
// 2. Multiple struct, post content like: [{"id":1, "name":"john"}, {"id":, "name":"smith"}]
//
// TODO: Improve the performance by reducing duplicated reflect usage on the same variable across packages.
func (r *Request) Parse(pointer interface{}) error {
return r.doParse(pointer, parseTypeRequest)
}
// ParseQuery performs like function Parse, but only parses the query parameters.
func (r *Request) ParseQuery(pointer interface{}) error {
return r.doParse(pointer, parseTypeQuery)
}
// ParseForm performs like function Parse, but only parses the form parameters or the body content.
func (r *Request) ParseForm(pointer interface{}) error {
return r.doParse(pointer, parseTypeForm)
}
// doParse parses the request data to struct/structs according to request type.
func (r *Request) doParse(pointer interface{}, requestType int) error {
var (
reflectVal1 = reflect.ValueOf(pointer)
reflectKind1 = reflectVal1.Kind()
@ -58,18 +79,31 @@ func (r *Request) Parse(pointer interface{}) error {
)
switch reflectKind2 {
// Single struct, post content like:
// {"id":1, "name":"john"}
// 1. {"id":1, "name":"john"}
// 2. ?id=1&name=john
case reflect.Ptr, reflect.Struct:
// Conversion.
if err := r.GetStruct(pointer); err != nil {
return err
// Converting.
switch requestType {
case parseTypeQuery:
if err := r.GetQueryStruct(pointer); err != nil {
return err
}
case parseTypeForm:
if err := r.GetFormStruct(pointer); err != nil {
return err
}
default:
if err := r.GetStruct(pointer); err != nil {
return err
}
}
// Validation.
if err := gvalid.CheckStruct(pointer, nil); err != nil {
return err
}
// Multiple struct, post content like:
// Multiple struct, it only supports JSON type post content like:
// [{"id":1, "name":"john"}, {"id":, "name":"smith"}]
case reflect.Array, reflect.Slice:
// If struct slice conversion, it might post JSON/XML content,
@ -276,6 +310,10 @@ func (r *Request) parseBody() {
return
}
r.parsedBody = true
// There's no data posted.
if r.ContentLength == 0 {
return
}
if body := r.GetBody(); len(body) > 0 {
// Trim space/new line characters.
body = bytes.TrimSpace(body)
@ -306,6 +344,10 @@ func (r *Request) parseForm() {
return
}
r.parsedForm = true
// There's no data posted.
if r.ContentLength == 0 {
return
}
if contentType := r.Header.Get("Content-Type"); contentType != "" {
var err error
if gstr.Contains(contentType, "multipart/") {
@ -363,11 +405,14 @@ func (r *Request) parseForm() {
}
}
}
if r.formMap == nil {
}
// It parses the request body without checking the Content-Type.
if r.formMap == nil {
if r.Method != "GET" {
r.parseBody()
if len(r.bodyMap) > 0 {
r.formMap = r.bodyMap
}
}
if len(r.bodyMap) > 0 {
r.formMap = r.bodyMap
}
}
}

View File

@ -193,7 +193,7 @@ func (r *Request) GetFormStruct(pointer interface{}, mapping ...map[string]strin
if m == nil {
m = map[string]interface{}{}
}
return gconv.StructDeep(m, pointer, mapping...)
return gconv.Struct(m, pointer, mapping...)
}
// GetFormToStruct is alias of GetFormStruct. See GetFormStruct.

View File

@ -204,7 +204,7 @@ func (r *Request) GetPostMapStrVar(kvMap ...map[string]interface{}) map[string]*
//
// Deprecated.
func (r *Request) GetPostStruct(pointer interface{}, mapping ...map[string]string) error {
return gconv.StructDeep(r.GetPostMap(), pointer, mapping...)
return gconv.Struct(r.GetPostMap(), pointer, mapping...)
}
// GetPostToStruct is alias of GetQueryStruct. See GetPostStruct.

View File

@ -34,7 +34,9 @@ func (r *Request) GetQuery(key string, def ...interface{}) interface{} {
return v
}
}
r.parseBody()
if r.Method == "GET" {
r.parseBody()
}
if len(r.bodyMap) > 0 {
if v, ok := r.bodyMap[key]; ok {
return v
@ -118,7 +120,9 @@ func (r *Request) GetQueryInterfaces(key string, def ...interface{}) []interface
// in order of priority: query > body.
func (r *Request) GetQueryMap(kvMap ...map[string]interface{}) map[string]interface{} {
r.parseQuery()
r.parseBody()
if r.Method == "GET" {
r.parseBody()
}
var m map[string]interface{}
if len(kvMap) > 0 && kvMap[0] != nil {
if len(r.queryMap) == 0 && len(r.bodyMap) == 0 {
@ -197,7 +201,7 @@ func (r *Request) GetQueryStruct(pointer interface{}, mapping ...map[string]stri
if m == nil {
m = map[string]interface{}{}
}
return gconv.StructDeep(m, pointer, mapping...)
return gconv.Struct(m, pointer, mapping...)
}
// GetQueryToStruct is alias of GetQueryStruct. See GetQueryStruct.

View File

@ -271,7 +271,7 @@ func (r *Request) GetRequestStruct(pointer interface{}, mapping ...map[string]st
if m == nil {
m = map[string]interface{}{}
}
return gconv.StructDeep(m, pointer, mapping...)
return gconv.Struct(m, pointer, mapping...)
}
// GetRequestToStruct is alias of GetRequestStruct. See GetRequestStruct.

View File

@ -92,7 +92,7 @@ type ServerConfig struct {
// size of the request body.
//
// It can be configured in configuration file using string like: 1m, 10m, 500kb etc.
// It's 1024 bytes in default.
// It's 10240 bytes in default.
MaxHeaderBytes int
// KeepAlive enables HTTP keep-alive.
@ -157,6 +157,9 @@ type ServerConfig struct {
// SessionIdName specifies the session id name.
SessionIdName string
// SessionCookieOutput specifies whether automatic outputting session id to cookie.
SessionCookieOutput bool
// SessionPath specifies the session storage directory path for storing session files.
// It only makes sense if the session storage is type of file storage.
SessionPath string
@ -231,50 +234,56 @@ type ServerConfig struct {
Graceful bool
}
// Config creates and returns a ServerConfig object with default configurations.
// Deprecated. Use NewConfig instead.
func Config() ServerConfig {
return NewConfig()
}
// NewConfig creates and returns a ServerConfig object with default configurations.
// Note that, do not define this default configuration to local package variable, as there're
// some pointer attributes that may be shared in different servers.
func Config() ServerConfig {
func NewConfig() ServerConfig {
return ServerConfig{
Address: "",
HTTPSAddr: "",
Handler: nil,
ReadTimeout: 60 * time.Second,
WriteTimeout: 0, // No timeout.
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 10240, // 10KB
KeepAlive: true,
IndexFiles: []string{"index.html", "index.htm"},
IndexFolder: false,
ServerAgent: "GF HTTP Server",
ServerRoot: "",
StaticPaths: make([]staticPathItem, 0),
FileServerEnabled: false,
CookieMaxAge: time.Hour * 24 * 365,
CookiePath: "/",
CookieDomain: "",
SessionMaxAge: time.Hour * 24,
SessionIdName: "gfsessionid",
SessionPath: gsession.DefaultStorageFilePath,
Logger: glog.New(),
LogStdout: true,
ErrorStack: true,
ErrorLogEnabled: true,
ErrorLogPattern: "error-{Ymd}.log",
AccessLogEnabled: false,
AccessLogPattern: "access-{Ymd}.log",
DumpRouterMap: true,
ClientMaxBodySize: 8 * 1024 * 1024, // 8MB
FormParsingMemory: 1024 * 1024, // 1MB
Rewrites: make(map[string]string),
Graceful: false,
Address: "",
HTTPSAddr: "",
Handler: nil,
ReadTimeout: 60 * time.Second,
WriteTimeout: 0, // No timeout.
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 10240, // 10KB
KeepAlive: true,
IndexFiles: []string{"index.html", "index.htm"},
IndexFolder: false,
ServerAgent: "GF HTTP Server",
ServerRoot: "",
StaticPaths: make([]staticPathItem, 0),
FileServerEnabled: false,
CookieMaxAge: time.Hour * 24 * 365,
CookiePath: "/",
CookieDomain: "",
SessionMaxAge: time.Hour * 24,
SessionIdName: "gfsessionid",
SessionPath: gsession.DefaultStorageFilePath,
SessionCookieOutput: true,
Logger: glog.New(),
LogStdout: true,
ErrorStack: true,
ErrorLogEnabled: true,
ErrorLogPattern: "error-{Ymd}.log",
AccessLogEnabled: false,
AccessLogPattern: "access-{Ymd}.log",
DumpRouterMap: true,
ClientMaxBodySize: 8 * 1024 * 1024, // 8MB
FormParsingMemory: 1024 * 1024, // 1MB
Rewrites: make(map[string]string),
Graceful: false,
}
}
// ConfigFromMap creates and returns a ServerConfig object with given map and
// default configuration object.
func ConfigFromMap(m map[string]interface{}) (ServerConfig, error) {
config := Config()
config := NewConfig()
if err := gconv.Struct(m, &config); err != nil {
return config, err
}

View File

@ -27,6 +27,11 @@ func (s *Server) SetSessionStorage(storage gsession.Storage) {
s.config.SessionStorage = storage
}
// SetSessionCookieOutput sets the SetSessionCookieOutput for server.
func (s *Server) SetSessionCookieOutput(enabled bool) {
s.config.SessionCookieOutput = enabled
}
// GetSessionMaxAge returns the SessionMaxAge of server.
func (s *Server) GetSessionMaxAge() time.Duration {
return s.config.SessionMaxAge

View File

@ -161,7 +161,9 @@ 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 request.Session.IsDirty() && request.Session.Id() != request.GetSessionId() {
if s.config.SessionCookieOutput &&
request.Session.IsDirty() &&
request.Session.Id() != request.GetSessionId() {
request.Cookie.SetSessionId(request.Session.Id())
}
// Output the cookie content to client.

View File

@ -24,14 +24,19 @@ const (
// EnablePProf enables PProf feature for server.
func (s *Server) EnablePProf(pattern ...string) {
s.Domain(gDEFAULT_DOMAIN).EnablePProf(pattern...)
}
// EnablePProf enables PProf feature for server of specified domain.
func (d *Domain) EnablePProf(pattern ...string) {
p := gDEFAULT_PPROF_PATTERN
if len(pattern) > 0 && pattern[0] != "" {
p = pattern[0]
}
up := &utilPProf{}
_, _, uri, _ := s.parsePattern(p)
_, _, uri, _ := d.server.parsePattern(p)
uri = strings.TrimRight(uri, "/")
s.Group(uri, func(group *RouterGroup) {
d.Group(uri, func(group *RouterGroup) {
group.ALL("/*action", up.Index)
group.ALL("/cmdline", up.Cmdline)
group.ALL("/profile", up.Profile)

View File

@ -44,13 +44,15 @@ func (s *Server) getHandlersWithCache(r *Request) (parsedItems []*handlerParsedI
}
}
// Search and cache the router handlers.
value := s.serveCache.GetOrSetFunc(s.serveHandlerKey(method, r.URL.Path, r.GetHost()), func() interface{} {
parsedItems, hasHook, hasServe = s.searchHandlers(method, r.URL.Path, r.GetHost())
if parsedItems != nil {
return &handlerCacheItem{parsedItems, hasHook, hasServe}
}
return nil
}, gROUTE_CACHE_DURATION)
value, _ := s.serveCache.GetOrSetFunc(
s.serveHandlerKey(method, r.URL.Path, r.GetHost()),
func() (interface{}, error) {
parsedItems, hasHook, hasServe = s.searchHandlers(method, r.URL.Path, r.GetHost())
if parsedItems != nil {
return &handlerCacheItem{parsedItems, hasHook, hasServe}, nil
}
return nil, nil
}, gROUTE_CACHE_DURATION)
if value != nil {
item := value.(*handlerCacheItem)
return item.parsedItems, item.hasHook, item.hasServe

View File

@ -295,6 +295,8 @@ func Test_Client_Param_Containing_Special_Char(t *testing.T) {
})
}
// It posts data along with file uploading.
// It does not url-encodes the parameters.
func Test_Client_File_And_Param(t *testing.T) {
p, _ := ports.PopRand()
s := g.Server(p)
@ -308,7 +310,7 @@ func Test_Client_File_And_Param(t *testing.T) {
_, err = file.Save(tmpPath)
gtest.Assert(err, nil)
r.Response.Write(
r.Get("key"),
r.Get("json"),
gfile.GetContents(gfile.Join(tmpPath, gfile.Basename(file.Filename))),
)
})
@ -320,12 +322,13 @@ func Test_Client_File_And_Param(t *testing.T) {
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
path := gdebug.TestDataPath("upload", "file1.txt")
data := g.Map{
"file": "@file:" + path,
"json": `{"uuid": "luijquiopm", "isRelative": false, "fileName": "test111.xls"}`,
}
c := g.Client()
c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
filePath := gdebug.TestDataPath("upload", "file1.txt")
t.Assert(
c.PostContent("/", "key=1&file=@file:"+filePath),
"1"+gfile.GetContents(filePath),
)
t.Assert(c.PostContent("/", data), data["json"].(string)+gfile.GetContents(path))
})
}

View File

@ -27,13 +27,11 @@ func Test_Params_Parse(t *testing.T) {
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse", func(r *ghttp.Request) {
if m := r.GetMap(); len(m) > 0 {
var user *User
if err := r.Parse(&user); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(user.Map["id"], user.Map["score"])
var user *User
if err := r.Parse(&user); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(user.Map["id"], user.Map["score"])
})
s.SetPort(p)
s.SetDumpRouterMap(false)
@ -48,6 +46,250 @@ func Test_Params_Parse(t *testing.T) {
})
}
func Test_Params_ParseQuery(t *testing.T) {
type User struct {
Id int
Name string
}
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse-query", func(r *ghttp.Request) {
var user *User
if err := r.ParseQuery(&user); 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("/parse-query"), `0`)
t.Assert(c.GetContent("/parse-query?id=1&name=john"), `1john`)
t.Assert(c.PostContent("/parse-query"), `0`)
t.Assert(c.PostContent("/parse-query", g.Map{
"id": 1,
"name": "john",
}), `0`)
})
}
func Test_Params_ParseForm(t *testing.T) {
type User struct {
Id int
Name string
}
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse-form", func(r *ghttp.Request) {
var user *User
if err := r.ParseForm(&user); 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("/parse-form"), `0`)
t.Assert(c.GetContent("/parse-form", g.Map{
"id": 1,
"name": "john",
}), 0)
t.Assert(c.PostContent("/parse-form"), `0`)
t.Assert(c.PostContent("/parse-form", g.Map{
"id": 1,
"name": "john",
}), `1john`)
})
}
func Test_Params_ComplexJsonStruct(t *testing.T) {
type ItemEnv struct {
Type string
Key string
Value string
Brief string
}
type ItemProbe struct {
Type string
Port int
Path string
Brief string
Period int
InitialDelay int
TimeoutSeconds int
}
type ItemKV struct {
Key string
Value string
}
type ItemPort struct {
Port int
Type string
Alias string
Brief string
}
type ItemMount struct {
Type string
DstPath string
Src string
SrcPath string
Brief string
}
type SaveRequest struct {
AppId uint
Name string
Type string
Cluster string
Replicas uint
ContainerName string
ContainerImage string
VersionTag string
Namespace string
Id uint
Status uint
Metrics string
InitImage string
CpuRequest uint
CpuLimit uint
MemRequest uint
MemLimit uint
MeshEnabled uint
ContainerPorts []ItemPort
Labels []ItemKV
NodeSelector []ItemKV
EnvReserve []ItemKV
EnvGlobal []ItemEnv
EnvContainer []ItemEnv
Mounts []ItemMount
LivenessProbe ItemProbe
ReadinessProbe ItemProbe
}
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/parse", func(r *ghttp.Request) {
if m := r.GetMap(); len(m) > 0 {
var data *SaveRequest
if err := r.Parse(&data); err != nil {
r.Response.WriteExit(err)
}
r.Response.WriteExit(data)
}
})
s.SetPort(p)
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
content := `
{
"app_id": 5,
"cluster": "test",
"container_image": "nginx",
"container_name": "test",
"container_ports": [
{
"alias": "别名",
"brief": "描述",
"port": 80,
"type": "tcp"
}
],
"cpu_limit": 100,
"cpu_request": 10,
"create_at": "2020-10-10 12:00:00",
"creator": 1,
"env_container": [
{
"brief": "用户环境变量",
"key": "NAME",
"type": "string",
"value": "john"
}
],
"env_global": [
{
"brief": "数据数量",
"key": "NUMBER",
"type": "string",
"value": "1"
}
],
"env_reserve": [
{
"key": "NODE_IP",
"value": "status.hostIP"
}
],
"liveness_probe": {
"brief": "存活探针",
"initial_delay": 10,
"path": "",
"period": 5,
"port": 80,
"type": "tcpSocket"
},
"readiness_probe": {
"brief": "就绪探针",
"initial_delay": 10,
"path": "",
"period": 5,
"port": 80,
"type": "tcpSocket"
},
"id": 0,
"init_image": "",
"labels": [
{
"key": "app",
"value": "test"
}
],
"mem_limit": 1000,
"mem_request": 100,
"mesh_enabled": 0,
"metrics": "",
"mounts": [],
"name": "test",
"namespace": "test",
"node_selector": [
{
"key": "group",
"value": "app"
}
],
"replicas": 1,
"type": "test",
"update_at": "2020-10-10 12:00:00",
"version_tag": "test"
}
`
t.Assert(client.PostContent("/parse", content), `{"AppId":5,"Name":"test","Type":"test","Cluster":"test","Replicas":1,"ContainerName":"test","ContainerImage":"nginx","VersionTag":"test","Namespace":"test","Id":0,"Status":0,"Metrics":"","InitImage":"","CpuRequest":10,"CpuLimit":100,"MemRequest":100,"MemLimit":1000,"MeshEnabled":0,"ContainerPorts":[{"Port":80,"Type":"tcp","Alias":"别名","Brief":"描述"}],"Labels":[{"Key":"app","Value":"test"}],"NodeSelector":[{"Key":"group","Value":"app"}],"EnvReserve":[{"Key":"NODE_IP","Value":"status.hostIP"}],"EnvGlobal":[{"Type":"string","Key":"NUMBER","Value":"1","Brief":"数据数量"}],"EnvContainer":[{"Type":"string","Key":"NAME","Value":"john","Brief":"用户环境变量"}],"Mounts":[],"LivenessProbe":{"Type":"tcpSocket","Port":80,"Path":"","Brief":"存活探针","Period":5,"InitialDelay":10,"TimeoutSeconds":0},"ReadinessProbe":{"Type":"tcpSocket","Port":80,"Path":"","Brief":"就绪探针","Period":5,"InitialDelay":10,"TimeoutSeconds":0}}`)
})
}
func Test_Params_Parse_Attr_Pointer1(t *testing.T) {
type User struct {
Id *int

View File

@ -404,7 +404,7 @@ func Test_Params_Basic(t *testing.T) {
t.Assert(client.GetContent("/struct", `id=1&name=john&password1=123&password2=456`), `1john123456`)
t.Assert(client.PostContent("/struct", `id=1&name=john&password1=123&password2=456`), `1john123456`)
t.Assert(client.PostContent("/struct-with-nil", ``), ``)
t.Assert(client.PostContent("/struct-with-base", `id=1&name=john&password1=123&password2=456`), "1john1234561john123456")
t.Assert(client.PostContent("/struct-with-base", `id=1&name=john&password1=123&password2=456`), "1john1234561john")
})
}
@ -433,10 +433,10 @@ func Test_Params_SupportChars(t *testing.T) {
p, _ := ports.PopRand()
s := g.Server(p)
s.BindHandler("/form-value", func(r *ghttp.Request) {
r.Response.Write(r.GetQuery("test-value"))
r.Response.Write(r.GetForm("test-value"))
})
s.BindHandler("/form-array", func(r *ghttp.Request) {
r.Response.Write(r.GetQuery("test-array"))
r.Response.Write(r.GetForm("test-array"))
})
s.SetPort(p)
s.SetDumpRouterMap(false)
@ -445,12 +445,10 @@ func Test_Params_SupportChars(t *testing.T) {
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
prefix := fmt.Sprintf("http://127.0.0.1:%d", p)
client := ghttp.NewClient()
client.SetPrefix(prefix)
t.Assert(client.PostContent("/form-value", "test-value=100"), "100")
t.Assert(client.PostContent("/form-array", "test-array[]=1&test-array[]=2"), `["1","2"]`)
c := g.Client()
c.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
t.Assert(c.PostContent("/form-value", "test-value=100"), "100")
t.Assert(c.PostContent("/form-array", "test-array[]=1&test-array[]=2"), `["1","2"]`)
})
}

View File

@ -152,3 +152,47 @@ func Test_Session_StorageFile(t *testing.T) {
t.Assert(client.GetContent("/get?k=key1"), "")
})
}
func Test_Session_Custom_Id(t *testing.T) {
var (
sessionId = "1234567890"
key = "key"
value = "value"
p, _ = ports.PopRand()
s = g.Server(p)
)
s.BindHandler("/id", func(r *ghttp.Request) {
if err := r.Session.SetId(sessionId); err != nil {
r.Response.WriteExit(err.Error())
}
if err := r.Session.Set(key, value); err != nil {
r.Response.WriteExit(err.Error())
}
r.Response.WriteExit(r.Session.Id())
})
s.BindHandler("/value", func(r *ghttp.Request) {
r.Response.WriteExit(r.Session.Get(key))
})
s.SetPort(p)
s.SetDumpRouterMap(false)
s.Start()
defer s.Shutdown()
time.Sleep(100 * time.Millisecond)
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
r, err := client.Get("/id")
t.Assert(err, nil)
defer r.Close()
t.Assert(r.ReadAllString(), sessionId)
t.Assert(r.GetCookie(s.GetSessionIdName()), sessionId)
})
gtest.C(t, func(t *gtest.T) {
client := g.Client()
client.SetPrefix(fmt.Sprintf("http://127.0.0.1:%d", p))
client.SetHeader(s.GetSessionIdName(), sessionId)
t.Assert(client.GetContent("/value"), value)
})
}

View File

@ -4,7 +4,8 @@
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package gcache provides high performance and concurrent-safe in-memory cache for process.
// Package gcache provides kinds of cache management for process.
// It default provides a concurrent-safe in-memory cache adapter for process.
package gcache
import (
@ -21,33 +22,27 @@ func Set(key interface{}, value interface{}, duration time.Duration) {
defaultCache.Set(key, value, duration)
}
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned <exist> value is false if the <key> does not exist in the cache.
func Update(key interface{}, value interface{}) (oldValue interface{}, exist bool) {
return defaultCache.Update(key, value)
}
// SetIfNotExist sets cache with <key>-<value> pair if <key> does not exist in the cache,
// which is expired after <duration>. It does not expire if <duration> == 0.
func SetIfNotExist(key interface{}, value interface{}, duration time.Duration) bool {
func SetIfNotExist(key interface{}, value interface{}, duration time.Duration) (bool, error) {
return defaultCache.SetIfNotExist(key, value, duration)
}
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
func Sets(data map[interface{}]interface{}, duration time.Duration) {
defaultCache.Sets(data, duration)
func Sets(data map[interface{}]interface{}, duration time.Duration) error {
return defaultCache.Sets(data, duration)
}
// Get returns the value of <key>.
// It returns nil if it does not exist or its value is nil.
func Get(key interface{}) interface{} {
func Get(key interface{}) (interface{}, error) {
return defaultCache.Get(key)
}
// GetVar retrieves and returns the value of <key> as gvar.Var.
func GetVar(key interface{}) *gvar.Var {
func GetVar(key interface{}) (*gvar.Var, error) {
return defaultCache.GetVar(key)
}
@ -56,14 +51,14 @@ func GetVar(key interface{}) *gvar.Var {
// The key-value pair expires after <duration>.
//
// It does not expire if <duration> == 0.
func GetOrSet(key interface{}, value interface{}, duration time.Duration) interface{} {
func GetOrSet(key interface{}, value interface{}, duration time.Duration) (interface{}, error) {
return defaultCache.GetOrSet(key, value, duration)
}
// GetOrSetFunc returns the value of <key>, or sets <key> with result of function <f>
// and returns its result if <key> does not exist in the cache. The key-value pair expires
// after <duration>. It does not expire if <duration> == 0.
func GetOrSetFunc(key interface{}, f func() interface{}, duration time.Duration) interface{} {
func GetOrSetFunc(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
return defaultCache.GetOrSetFunc(key, f, duration)
}
@ -72,18 +67,18 @@ func GetOrSetFunc(key interface{}, f func() interface{}, duration time.Duration)
// after <duration>. It does not expire if <duration> == 0.
//
// Note that the function <f> is executed within writing mutex lock.
func GetOrSetFuncLock(key interface{}, f func() interface{}, duration time.Duration) interface{} {
func GetOrSetFuncLock(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
return defaultCache.GetOrSetFuncLock(key, f, duration)
}
// Contains returns true if <key> exists in the cache, or else returns false.
func Contains(key interface{}) bool {
func Contains(key interface{}) (bool, error) {
return defaultCache.Contains(key)
}
// Remove deletes the one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the deleted last item.
func Remove(keys ...interface{}) (value interface{}) {
func Remove(keys ...interface{}) (value interface{}, err error) {
return defaultCache.Remove(keys...)
}
@ -94,38 +89,44 @@ func Removes(keys []interface{}) {
}
// Data returns a copy of all key-value pairs in the cache as map type.
func Data() map[interface{}]interface{} {
func Data() (map[interface{}]interface{}, error) {
return defaultCache.Data()
}
// Keys returns all keys in the cache as slice.
func Keys() []interface{} {
func Keys() ([]interface{}, error) {
return defaultCache.Keys()
}
// KeyStrings returns all keys in the cache as string slice.
func KeyStrings() []string {
func KeyStrings() ([]string, error) {
return defaultCache.KeyStrings()
}
// Values returns all values in the cache as slice.
func Values() []interface{} {
func Values() ([]interface{}, error) {
return defaultCache.Values()
}
// Size returns the size of the cache.
func Size() int {
func Size() (int, error) {
return defaultCache.Size()
}
// GetExpire retrieves and returns the expiration of <key>.
// It returns -1 if the <key> does not exist in the cache.
func GetExpire(key interface{}) time.Duration {
func GetExpire(key interface{}) (time.Duration, error) {
return defaultCache.GetExpire(key)
}
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned <exist> value is false if the <key> does not exist in the cache.
func Update(key interface{}, value interface{}) (oldValue interface{}, exist bool, err error) {
return defaultCache.Update(key, value)
}
// UpdateExpire updates the expiration of <key> and returns the old expiration duration value.
// It returns -1 if the <key> does not exist in the cache.
func UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration) {
func UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
return defaultCache.UpdateExpire(key, duration)
}

117
os/gcache/gcache_adapter.go Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2020 gf Author(https://github.com/gogf/gf). 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 gcache
import (
"time"
)
// Adapter is the adapter for cache features implements.
type Adapter interface {
// Set sets cache with <key>-<value> pair, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0.
Set(key interface{}, value interface{}, duration time.Duration) error
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the keys of <data> if <duration> < 0 or given <value> is nil.
Sets(data map[interface{}]interface{}, duration time.Duration) error
// SetIfNotExist sets cache with <key>-<value> pair which is expired after <duration>
// if <key> does not exist in the cache. It returns true the <key> dose not exist in the
// cache and it sets <value> successfully to the cache, or else it returns false.
//
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if its
// result is nil.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil.
SetIfNotExist(key interface{}, value interface{}, duration time.Duration) (bool, error)
// Get retrieves and returns the associated value of given <key>.
// It returns nil if it does not exist or its value is nil.
Get(key interface{}) (interface{}, error)
// GetOrSet retrieves and returns the value of <key>, or sets <key>-<value> pair and
// returns <value> if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
GetOrSet(key interface{}, value interface{}, duration time.Duration) (interface{}, error)
// GetOrSetFunc retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
GetOrSetFunc(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error)
// GetOrSetFuncLock retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
//
// Note that the function <f> should be executed within writing mutex lock for concurrent
// safety purpose.
GetOrSetFuncLock(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error)
// Contains returns true if <key> exists in the cache, or else returns false.
Contains(key interface{}) (bool, error)
// GetExpire retrieves and returns the expiration of <key> in the cache.
//
// It returns 0 if the <key> does not expire.
// It returns -1 if the <key> does not exist in the cache.
GetExpire(key interface{}) (time.Duration, error)
// Remove deletes one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the last deleted item.
Remove(keys ...interface{}) (value interface{}, err error)
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned value <exist> is false if the <key> does not exist in the cache.
//
// It deletes the <key> if given <value> is nil.
// It does nothing if <key> does not exist in the cache.
Update(key interface{}, value interface{}) (oldValue interface{}, exist bool, err error)
// UpdateExpire updates the expiration of <key> and returns the old expiration duration value.
//
// It returns -1 and does nothing if the <key> does not exist in the cache.
// It deletes the <key> if <duration> < 0.
UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration, err error)
// Size returns the number of items in the cache.
Size() (size int, err error)
// Data returns a copy of all key-value pairs in the cache as map type.
// Note that this function may leads lots of memory usage, you can implement this function
// if necessary.
Data() (map[interface{}]interface{}, error)
// Keys returns all keys in the cache as slice.
Keys() ([]interface{}, error)
// Values returns all values in the cache as slice.
Values() ([]interface{}, error)
// Clear clears all data of the cache.
// Note that this function is sensitive and should be carefully used.
Clear() error
// Close closes the cache if necessary.
Close() error
}

View File

@ -7,7 +7,6 @@
package gcache
import (
"github.com/gogf/gf/container/gvar"
"math"
"sync"
"time"
@ -17,11 +16,10 @@ import (
"github.com/gogf/gf/container/gtype"
"github.com/gogf/gf/os/gtime"
"github.com/gogf/gf/os/gtimer"
"github.com/gogf/gf/util/gconv"
)
// Internal cache object.
type memCache struct {
type adapterMemory struct {
// dataMu ensures the concurrent safety of underlying data map.
dataMu sync.RWMutex
@ -38,7 +36,7 @@ type memCache struct {
cap int
// data is the underlying cache data which is stored in a hash table.
data map[interface{}]memCacheItem
data map[interface{}]adapterMemoryItem
// expireTimes is the expiring key to its timestamp mapping,
// which is used for quick indexing and deleting.
@ -49,7 +47,7 @@ type memCache struct {
expireSets map[int64]*gset.Set
// lru is the LRU manager, which is enabled when attribute cap > 0.
lru *memCacheLru
lru *adapterMemoryLru
// lruGetList is the LRU history according with Get function.
lruGetList *glist.List
@ -62,13 +60,13 @@ type memCache struct {
}
// Internal cache item.
type memCacheItem struct {
type adapterMemoryItem struct {
v interface{} // Value.
e int64 // Expire timestamp in milliseconds.
}
// Internal event item.
type memCacheEvent struct {
type adapterMemoryEvent struct {
k interface{} // Key.
e int64 // Expire time in milliseconds.
}
@ -79,11 +77,11 @@ const (
gDEFAULT_MAX_EXPIRE = 9223372036854
)
// newMemCache creates and returns a new memory cache object.
func newMemCache(lruCap ...int) *memCache {
c := &memCache{
// newAdapterMemory creates and returns a new memory cache object.
func newAdapterMemory(lruCap ...int) *adapterMemory {
c := &adapterMemory{
lruGetList: glist.New(true),
data: make(map[interface{}]memCacheItem),
data: make(map[interface{}]adapterMemoryItem),
expireTimes: make(map[interface{}]int64),
expireSets: make(map[int64]*gset.Set),
eventList: glist.New(true),
@ -97,96 +95,330 @@ func newMemCache(lruCap ...int) *memCache {
}
// Set sets cache with <key>-<value> pair, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
func (c *memCache) Set(key interface{}, value interface{}, duration time.Duration) {
// It deletes the <key> if <duration> < 0.
func (c *adapterMemory) Set(key interface{}, value interface{}, duration time.Duration) error {
expireTime := c.getInternalExpire(duration)
c.dataMu.Lock()
c.data[key] = memCacheItem{
c.data[key] = adapterMemoryItem{
v: value,
e: expireTime,
}
c.dataMu.Unlock()
c.eventList.PushBack(&memCacheEvent{
c.eventList.PushBack(&adapterMemoryEvent{
k: key,
e: expireTime,
})
return nil
}
// Update updates the value of <key> without changing its expiration and returns the old value.
// The returned <exist> value is false if the <key> does not exist in the cache.
func (c *memCache) Update(key interface{}, value interface{}) (oldValue interface{}, exist bool) {
// The returned value <exist> is false if the <key> does not exist in the cache.
//
// It deletes the <key> if given <value> is nil.
// It does nothing if <key> does not exist in the cache.
func (c *adapterMemory) Update(key interface{}, value interface{}) (oldValue interface{}, exist bool, err error) {
c.dataMu.Lock()
defer c.dataMu.Unlock()
if item, ok := c.data[key]; ok {
c.data[key] = memCacheItem{
c.data[key] = adapterMemoryItem{
v: value,
e: item.e,
}
return item.v, true
return item.v, true, nil
}
return nil, false
return nil, false, nil
}
// UpdateExpire updates the expiration of <key> and returns the old expiration duration value.
// It returns -1 if the <key> does not exist in the cache.
func (c *memCache) UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration) {
//
// It returns -1 and does nothing if the <key> does not exist in the cache.
// It deletes the <key> if <duration> < 0.
func (c *adapterMemory) UpdateExpire(key interface{}, duration time.Duration) (oldDuration time.Duration, err error) {
newExpireTime := c.getInternalExpire(duration)
c.dataMu.Lock()
defer c.dataMu.Unlock()
if item, ok := c.data[key]; ok {
c.data[key] = memCacheItem{
c.data[key] = adapterMemoryItem{
v: item.v,
e: newExpireTime,
}
c.eventList.PushBack(&memCacheEvent{
c.eventList.PushBack(&adapterMemoryEvent{
k: key,
e: newExpireTime,
})
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond, nil
}
return -1
return -1, nil
}
// GetExpire retrieves and returns the expiration of <key>.
// GetExpire retrieves and returns the expiration of <key> in the cache.
//
// It returns 0 if the <key> does not expire.
// It returns -1 if the <key> does not exist in the cache.
func (c *memCache) GetExpire(key interface{}) time.Duration {
func (c *adapterMemory) GetExpire(key interface{}) (time.Duration, error) {
c.dataMu.RLock()
defer c.dataMu.RUnlock()
if item, ok := c.data[key]; ok {
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond
return time.Duration(item.e-gtime.TimestampMilli()) * time.Millisecond, nil
}
return -1
return -1, nil
}
// SetIfNotExist sets cache with <key>-<value> pair which is expired after <duration>
// if <key> does not exist in the cache. It returns true the <key> dose not exist in the
// cache and it sets <value> successfully to the cache, or else it returns false.
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if its
// result is nil.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil.
func (c *adapterMemory) SetIfNotExist(key interface{}, value interface{}, duration time.Duration) (bool, error) {
isContained, err := c.Contains(key)
if err != nil {
return false, err
}
if !isContained {
_, err := c.doSetWithLockCheck(key, value, duration)
if err != nil {
return false, err
}
return true, nil
}
return false, nil
}
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the keys of <data> if <duration> < 0 or given <value> is nil.
func (c *adapterMemory) Sets(data map[interface{}]interface{}, duration time.Duration) error {
expireTime := c.getInternalExpire(duration)
for k, v := range data {
c.dataMu.Lock()
c.data[k] = adapterMemoryItem{
v: v,
e: expireTime,
}
c.dataMu.Unlock()
c.eventList.PushBack(&adapterMemoryEvent{
k: k,
e: expireTime,
})
}
return nil
}
// Get retrieves and returns the associated value of given <key>.
// It returns nil if it does not exist or its value is nil.
func (c *adapterMemory) Get(key interface{}) (interface{}, error) {
c.dataMu.RLock()
item, ok := c.data[key]
c.dataMu.RUnlock()
if ok && !item.IsExpired() {
// Adding to LRU history if LRU feature is enabled.
if c.cap > 0 {
c.lruGetList.PushBack(key)
}
return item.v, nil
}
return nil, nil
}
// GetOrSet retrieves and returns the value of <key>, or sets <key>-<value> pair and
// returns <value> if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
func (c *adapterMemory) GetOrSet(key interface{}, value interface{}, duration time.Duration) (interface{}, error) {
v, err := c.Get(key)
if err != nil {
return nil, err
}
if v == nil {
return c.doSetWithLockCheck(key, value, duration)
} else {
return v, nil
}
}
// GetOrSetFunc retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It deletes the <key> if <duration> < 0 or given <value> is nil, but it does nothing
// if <value> is a function and the function result is nil.
func (c *adapterMemory) GetOrSetFunc(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
v, err := c.Get(key)
if err != nil {
return nil, err
}
if v == nil {
value, err := f()
if err != nil {
return nil, err
}
if value == nil {
return nil, nil
}
return c.doSetWithLockCheck(key, value, duration)
} else {
return v, nil
}
}
// GetOrSetFuncLock retrieves and returns the value of <key>, or sets <key> with result of
// function <f> and returns its result if <key> does not exist in the cache. The key-value
// pair expires after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
//
// Note that the function <f> should be executed within writing mutex lock for concurrent
// safety purpose.
func (c *adapterMemory) GetOrSetFuncLock(key interface{}, f func() (interface{}, error), duration time.Duration) (interface{}, error) {
v, err := c.Get(key)
if err != nil {
return nil, err
}
if v == nil {
return c.doSetWithLockCheck(key, f, duration)
} else {
return v, nil
}
}
// Contains returns true if <key> exists in the cache, or else returns false.
func (c *adapterMemory) Contains(key interface{}) (bool, error) {
v, err := c.Get(key)
if err != nil {
return false, err
}
return v != nil, nil
}
// Remove deletes the one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the deleted last item.
func (c *adapterMemory) Remove(keys ...interface{}) (value interface{}, err error) {
c.dataMu.Lock()
defer c.dataMu.Unlock()
for _, key := range keys {
item, ok := c.data[key]
if ok {
value = item.v
delete(c.data, key)
c.eventList.PushBack(&adapterMemoryEvent{
k: key,
e: gtime.TimestampMilli() - 1000,
})
}
}
return value, nil
}
// Data returns a copy of all key-value pairs in the cache as map type.
func (c *adapterMemory) Data() (map[interface{}]interface{}, error) {
m := make(map[interface{}]interface{})
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
m[k] = v.v
}
}
c.dataMu.RUnlock()
return m, nil
}
// Keys returns all keys in the cache as slice.
func (c *adapterMemory) Keys() ([]interface{}, error) {
keys := make([]interface{}, 0)
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
keys = append(keys, k)
}
}
c.dataMu.RUnlock()
return keys, nil
}
// Values returns all values in the cache as slice.
func (c *adapterMemory) Values() ([]interface{}, error) {
values := make([]interface{}, 0)
c.dataMu.RLock()
for _, v := range c.data {
if !v.IsExpired() {
values = append(values, v.v)
}
}
c.dataMu.RUnlock()
return values, nil
}
// Size returns the size of the cache.
func (c *adapterMemory) Size() (size int, err error) {
c.dataMu.RLock()
size = len(c.data)
c.dataMu.RUnlock()
return size, nil
}
// Clear clears all data of the cache.
// Note that this function is sensitive and should be carefully used.
func (c *adapterMemory) Clear() error {
c.dataMu.Lock()
defer c.dataMu.Unlock()
c.data = make(map[interface{}]adapterMemoryItem)
return nil
}
// Close closes the cache.
func (c *adapterMemory) Close() error {
if c.cap > 0 {
c.lru.Close()
}
c.closed.Set(true)
return nil
}
// doSetWithLockCheck sets cache with <key>-<value> pair if <key> does not exist in the
// cache, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if its
// result is nil.
// The parameter <value> can be type of <func() interface{}>, but it dose nothing if the
// function result is nil.
//
// It doubly checks the <key> whether exists in the cache using mutex writing lock
// before setting it to the cache.
func (c *memCache) doSetWithLockCheck(key interface{}, value interface{}, duration time.Duration) interface{} {
func (c *adapterMemory) doSetWithLockCheck(key interface{}, value interface{}, duration time.Duration) (interface{}, error) {
expireTimestamp := c.getInternalExpire(duration)
c.dataMu.Lock()
defer c.dataMu.Unlock()
if v, ok := c.data[key]; ok && !v.IsExpired() {
return v.v
return v.v, nil
}
if f, ok := value.(func() interface{}); ok {
value = f()
if value == nil {
return nil
if f, ok := value.(func() (interface{}, error)); ok {
v, err := f()
if err != nil {
return nil, err
}
if v == nil {
return nil, nil
} else {
value = v
}
}
c.data[key] = memCacheItem{v: value, e: expireTimestamp}
c.eventList.PushBack(&memCacheEvent{k: key, e: expireTimestamp})
return value
c.data[key] = adapterMemoryItem{v: value, e: expireTimestamp}
c.eventList.PushBack(&adapterMemoryEvent{k: key, e: expireTimestamp})
return value, nil
}
// getInternalExpire converts and returns the expire time with given expired duration in milliseconds.
func (c *memCache) getInternalExpire(duration time.Duration) int64 {
func (c *adapterMemory) getInternalExpire(duration time.Duration) int64 {
if duration == 0 {
return gDEFAULT_MAX_EXPIRE
} else {
@ -195,12 +427,12 @@ func (c *memCache) getInternalExpire(duration time.Duration) int64 {
}
// makeExpireKey groups the <expire> in milliseconds to its according seconds.
func (c *memCache) makeExpireKey(expire int64) int64 {
func (c *adapterMemory) makeExpireKey(expire int64) int64 {
return int64(math.Ceil(float64(expire/1000)+1) * 1000)
}
// getExpireSet returns the expire set for given <expire> in seconds.
func (c *memCache) getExpireSet(expire int64) (expireSet *gset.Set) {
func (c *adapterMemory) getExpireSet(expire int64) (expireSet *gset.Set) {
c.expireSetMu.RLock()
expireSet, _ = c.expireSets[expire]
c.expireSetMu.RUnlock()
@ -209,7 +441,7 @@ func (c *memCache) getExpireSet(expire int64) (expireSet *gset.Set) {
// getOrNewExpireSet returns the expire set for given <expire> in seconds.
// It creates and returns a new set for <expire> if it does not exist.
func (c *memCache) getOrNewExpireSet(expire int64) (expireSet *gset.Set) {
func (c *adapterMemory) getOrNewExpireSet(expire int64) (expireSet *gset.Set) {
if expireSet = c.getExpireSet(expire); expireSet == nil {
expireSet = gset.New(true)
c.expireSetMu.Lock()
@ -223,202 +455,17 @@ func (c *memCache) getOrNewExpireSet(expire int64) (expireSet *gset.Set) {
return
}
// SetIfNotExist sets cache with <key>-<value> pair if <key> does not exist in the cache,
// which is expired after <duration>. It does not expire if <duration> == 0.
func (c *memCache) SetIfNotExist(key interface{}, value interface{}, duration time.Duration) bool {
if !c.Contains(key) {
c.doSetWithLockCheck(key, value, duration)
return true
}
return false
}
// Sets batch sets cache with key-value pairs by <data>, which is expired after <duration>.
//
// It does not expire if <duration> == 0.
func (c *memCache) Sets(data map[interface{}]interface{}, duration time.Duration) {
expireTime := c.getInternalExpire(duration)
for k, v := range data {
c.dataMu.Lock()
c.data[k] = memCacheItem{
v: v,
e: expireTime,
}
c.dataMu.Unlock()
c.eventList.PushBack(&memCacheEvent{
k: k,
e: expireTime,
})
}
}
// Get returns the value of <key>.
// It returns nil if it does not exist or its value is nil.
func (c *memCache) Get(key interface{}) interface{} {
c.dataMu.RLock()
item, ok := c.data[key]
c.dataMu.RUnlock()
if ok && !item.IsExpired() {
// Adding to LRU history if LRU feature is enabled.
if c.cap > 0 {
c.lruGetList.PushBack(key)
}
return item.v
}
return nil
}
// GetVar retrieves and returns the value of <key> as gvar.Var.
func (c *memCache) GetVar(key interface{}) *gvar.Var {
return gvar.New(c.Get(key))
}
// GetOrSet returns the value of <key>, or sets <key>-<value> pair and returns <value> if <key>
// does not exist in the cache. The key-value pair expires after <duration>. It does not expire
// if <duration> == 0.
func (c *memCache) GetOrSet(key interface{}, value interface{}, duration time.Duration) interface{} {
if v := c.Get(key); v == nil {
return c.doSetWithLockCheck(key, value, duration)
} else {
return v
}
}
// GetOrSetFunc returns the value of <key>, or sets <key> with result of function <f>
// and returns its result if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
func (c *memCache) GetOrSetFunc(key interface{}, f func() interface{}, duration time.Duration) interface{} {
if v := c.Get(key); v == nil {
value := f()
if value == nil {
return nil
}
return c.doSetWithLockCheck(key, value, duration)
} else {
return v
}
}
// GetOrSetFuncLock returns the value of <key>, or sets <key> with result of function <f>
// and returns its result if <key> does not exist in the cache. The key-value pair expires
// after <duration>.
//
// It does not expire if <duration> == 0.
// It does nothing if function <f> returns nil.
//
// Note that the function <f> is executed within writing mutex lock.
func (c *memCache) GetOrSetFuncLock(key interface{}, f func() interface{}, duration time.Duration) interface{} {
if v := c.Get(key); v == nil {
return c.doSetWithLockCheck(key, f, duration)
} else {
return v
}
}
// Contains returns true if <key> exists in the cache, or else returns false.
func (c *memCache) Contains(key interface{}) bool {
return c.Get(key) != nil
}
// Remove deletes the one or more keys from cache, and returns its value.
// If multiple keys are given, it returns the value of the deleted last item.
func (c *memCache) Remove(keys ...interface{}) (value interface{}) {
c.dataMu.Lock()
defer c.dataMu.Unlock()
for _, key := range keys {
item, ok := c.data[key]
if ok {
value = item.v
delete(c.data, key)
c.eventList.PushBack(&memCacheEvent{
k: key,
e: gtime.TimestampMilli() - 1000,
})
}
}
return
}
// Removes deletes <keys> in the cache.
// Deprecated, use Remove instead.
func (c *memCache) Removes(keys []interface{}) {
c.Remove(keys...)
}
// Data returns a copy of all key-value pairs in the cache as map type.
func (c *memCache) Data() map[interface{}]interface{} {
m := make(map[interface{}]interface{})
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
m[k] = v.v
}
}
c.dataMu.RUnlock()
return m
}
// Keys returns all keys in the cache as slice.
func (c *memCache) Keys() []interface{} {
keys := make([]interface{}, 0)
c.dataMu.RLock()
for k, v := range c.data {
if !v.IsExpired() {
keys = append(keys, k)
}
}
c.dataMu.RUnlock()
return keys
}
// KeyStrings returns all keys in the cache as string slice.
func (c *memCache) KeyStrings() []string {
return gconv.Strings(c.Keys())
}
// Values returns all values in the cache as slice.
func (c *memCache) Values() []interface{} {
values := make([]interface{}, 0)
c.dataMu.RLock()
for _, v := range c.data {
if !v.IsExpired() {
values = append(values, v.v)
}
}
c.dataMu.RUnlock()
return values
}
// Size returns the size of the cache.
func (c *memCache) Size() (size int) {
c.dataMu.RLock()
size = len(c.data)
c.dataMu.RUnlock()
return
}
// Close closes the cache.
func (c *memCache) Close() {
if c.cap > 0 {
c.lru.Close()
}
c.closed.Set(true)
}
// syncEventAndClearExpired does the asynchronous task loop:
// 1. Asynchronously process the data in the event list,
// and synchronize the results to the <expireTimes> and <expireSets> properties.
// 2. Clean up the expired key-value pair data.
func (c *memCache) syncEventAndClearExpired() {
func (c *adapterMemory) syncEventAndClearExpired() {
if c.closed.Val() {
gtimer.Exit()
return
}
var (
event *memCacheEvent
event *adapterMemoryEvent
oldExpireTime int64
newExpireTime int64
)
@ -430,7 +477,7 @@ func (c *memCache) syncEventAndClearExpired() {
if v == nil {
break
}
event = v.(*memCacheEvent)
event = v.(*adapterMemoryEvent)
// Fetching the old expire set.
c.expireTimeMu.RLock()
oldExpireTime = c.expireTimes[event.k]
@ -487,7 +534,7 @@ func (c *memCache) syncEventAndClearExpired() {
// clearByKey deletes the key-value pair with given <key>.
// The parameter <force> specifies whether doing this deleting forcibly.
func (c *memCache) clearByKey(key interface{}, force ...bool) {
func (c *adapterMemory) clearByKey(key interface{}, force ...bool) {
c.dataMu.Lock()
// Doubly check before really deleting it from cache.
if item, ok := c.data[key]; (ok && item.IsExpired()) || (len(force) > 0 && force[0]) {

View File

@ -11,7 +11,7 @@ import (
)
// IsExpired checks whether <item> is expired.
func (item *memCacheItem) IsExpired() bool {
func (item *adapterMemoryItem) IsExpired() bool {
// Note that it should use greater than or equal judgement here
// imagining that the cache time is only 1 millisecond.
if item.e >= gtime.TimestampMilli() {

View File

@ -17,17 +17,17 @@ import (
// LRU cache object.
// It uses list.List from stdlib for its underlying doubly linked list.
type memCacheLru struct {
cache *memCache // Parent cache object.
data *gmap.Map // Key mapping to the item of the list.
list *glist.List // Key list.
rawList *glist.List // History for key adding.
closed *gtype.Bool // Closed or not.
type adapterMemoryLru struct {
cache *adapterMemory // Parent cache object.
data *gmap.Map // Key mapping to the item of the list.
list *glist.List // Key list.
rawList *glist.List // History for key adding.
closed *gtype.Bool // Closed or not.
}
// newMemCacheLru creates and returns a new LRU object.
func newMemCacheLru(cache *memCache) *memCacheLru {
lru := &memCacheLru{
func newMemCacheLru(cache *adapterMemory) *adapterMemoryLru {
lru := &adapterMemoryLru{
cache: cache,
data: gmap.New(true),
list: glist.New(true),
@ -39,12 +39,12 @@ func newMemCacheLru(cache *memCache) *memCacheLru {
}
// Close closes the LRU object.
func (lru *memCacheLru) Close() {
func (lru *adapterMemoryLru) Close() {
lru.closed.Set(true)
}
// Remove deletes the <key> FROM <lru>.
func (lru *memCacheLru) Remove(key interface{}) {
func (lru *adapterMemoryLru) Remove(key interface{}) {
if v := lru.data.Get(key); v != nil {
lru.data.Remove(key)
lru.list.Remove(v.(*glist.Element))
@ -52,17 +52,17 @@ func (lru *memCacheLru) Remove(key interface{}) {
}
// Size returns the size of <lru>.
func (lru *memCacheLru) Size() int {
func (lru *adapterMemoryLru) Size() int {
return lru.data.Size()
}
// Push pushes <key> to the tail of <lru>.
func (lru *memCacheLru) Push(key interface{}) {
func (lru *adapterMemoryLru) Push(key interface{}) {
lru.rawList.PushBack(key)
}
// Pop deletes and returns the key from tail of <lru>.
func (lru *memCacheLru) Pop() interface{} {
func (lru *adapterMemoryLru) Pop() interface{} {
if v := lru.list.PopBack(); v != nil {
lru.data.Remove(v)
return v
@ -71,7 +71,7 @@ func (lru *memCacheLru) Pop() interface{} {
}
// Print is used for test only.
//func (lru *memCacheLru) Print() {
//func (lru *adapterMemoryLru) Print() {
// for _, v := range lru.list.FrontAll() {
// fmt.Printf("%v ", v)
// }
@ -80,7 +80,7 @@ func (lru *memCacheLru) Pop() interface{} {
// SyncAndClear synchronizes the keys from <rawList> to <list> and <data>
// using Least Recently Used algorithm.
func (lru *memCacheLru) SyncAndClear() {
func (lru *adapterMemoryLru) SyncAndClear() {
if lru.closed.Val() {
gtimer.Exit()
return

View File

@ -7,31 +7,55 @@
package gcache
import (
"sync/atomic"
"time"
"unsafe"
"github.com/gogf/gf/container/gvar"
"github.com/gogf/gf/os/gtimer"
"github.com/gogf/gf/util/gconv"
"time"
)
// Cache struct.
type Cache struct {
*memCache
Adapter // Adapter for cache features.
}
// New creates and returns a new cache object.
// New creates and returns a new cache object using default memory adapter.
// Note that the LRU feature is only available using memory adapter.
func New(lruCap ...int) *Cache {
memAdapter := newAdapterMemory(lruCap...)
c := &Cache{
memCache: newMemCache(lruCap...),
Adapter: memAdapter,
}
gtimer.AddSingleton(time.Second, c.syncEventAndClearExpired)
// Here may be a "timer leak" if adapter is manually changed from memory adapter.
// Do not worry about this, as adapter is less changed and it dose nothing if it's not used.
gtimer.AddSingleton(time.Second, memAdapter.syncEventAndClearExpired)
return c
}
// Clear clears all data of the cache.
func (c *Cache) Clear() {
// atomic swap to ensure atomicity.
old := atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&c.memCache)), unsafe.Pointer(newMemCache()))
// close the old cache object.
(*memCache)(old).Close()
// SetAdapter changes the adapter for this cache.
// Be very note that, this setting function is not concurrent-safe, which means you should not call
// this setting function concurrently in multiple goroutines.
func (c *Cache) SetAdapter(adapter Adapter) {
c.Adapter = adapter
}
// GetVar retrieves and returns the value of <key> as gvar.Var.
func (c *Cache) GetVar(key interface{}) (*gvar.Var, error) {
v, err := c.Get(key)
return gvar.New(v), err
}
// Removes deletes <keys> in the cache.
// Deprecated, use Remove instead.
func (c *Cache) Removes(keys []interface{}) error {
_, err := c.Remove(keys...)
return err
}
// KeyStrings returns all keys in the cache as string slice.
func (c *Cache) KeyStrings() ([]string, error) {
keys, err := c.Keys()
if err != nil {
return nil, err
}
return gconv.Strings(keys), nil
}

View File

@ -25,8 +25,10 @@ func TestCache_GCache_Set(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
gcache.Set(1, 11, 0)
defer gcache.Removes(g.Slice{1, 2, 3})
t.Assert(gcache.Get(1), 11)
t.Assert(gcache.Contains(1), true)
v, _ := gcache.Get(1)
t.Assert(v, 11)
b, _ := gcache.Contains(1)
t.Assert(b, true)
})
}
@ -34,44 +36,57 @@ func TestCache_Set(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
c := gcache.New()
defer c.Close()
c.Set(1, 11, 0)
t.Assert(c.Get(1), 11)
t.Assert(c.Contains(1), true)
t.Assert(c.Set(1, 11, 0), nil)
v, _ := c.Get(1)
t.Assert(v, 11)
b, _ := c.Contains(1)
t.Assert(b, true)
})
}
func TestCache_GetVar(t *testing.T) {
c := gcache.New()
defer c.Close()
gtest.C(t, func(t *gtest.T) {
c := gcache.New()
defer c.Close()
c.Set(1, 11, 0)
t.Assert(c.Get(1), 11)
t.Assert(c.Contains(1), true)
t.Assert(c.GetVar(1).Int(), 11)
t.Assert(c.GetVar(2).Int(), 0)
t.Assert(c.GetVar(2).IsNil(), true)
t.Assert(c.GetVar(2).IsEmpty(), true)
t.Assert(c.Set(1, 11, 0), nil)
v, _ := c.Get(1)
t.Assert(v, 11)
b, _ := c.Contains(1)
t.Assert(b, true)
})
gtest.C(t, func(t *gtest.T) {
v, _ := c.GetVar(1)
t.Assert(v.Int(), 11)
v, _ = c.GetVar(2)
t.Assert(v.Int(), 0)
t.Assert(v.IsNil(), true)
t.Assert(v.IsEmpty(), true)
})
}
func TestCache_Set_Expire(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(2, 22, 100*time.Millisecond)
t.Assert(cache.Get(2), 22)
t.Assert(cache.Set(2, 22, 100*time.Millisecond), nil)
v, _ := cache.Get(2)
t.Assert(v, 22)
time.Sleep(200 * time.Millisecond)
t.Assert(cache.Get(2), nil)
v, _ = cache.Get(2)
t.Assert(v, nil)
time.Sleep(3 * time.Second)
t.Assert(cache.Size(), 0)
cache.Close()
n, _ := cache.Size()
t.Assert(n, 0)
t.Assert(cache.Close(), nil)
})
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(1, 11, 100*time.Millisecond)
t.Assert(cache.Get(1), 11)
t.Assert(cache.Set(1, 11, 100*time.Millisecond), nil)
v, _ := cache.Get(1)
t.Assert(v, 11)
time.Sleep(200 * time.Millisecond)
t.Assert(cache.Get(1), nil)
v, _ = cache.Get(1)
t.Assert(v, nil)
})
}
@ -80,20 +95,22 @@ func TestCache_Update_GetExpire(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
key := guid.S()
gcache.Set(key, 11, 3*time.Second)
expire1 := gcache.GetExpire(key)
expire1, _ := gcache.GetExpire(key)
gcache.Update(key, 12)
expire2 := gcache.GetExpire(key)
t.Assert(gcache.GetVar(key), 12)
expire2, _ := gcache.GetExpire(key)
v, _ := gcache.GetVar(key)
t.Assert(v, 12)
t.Assert(math.Ceil(expire1.Seconds()), math.Ceil(expire2.Seconds()))
})
// gcache.Cache
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(1, 11, 3*time.Second)
expire1 := cache.GetExpire(1)
expire1, _ := cache.GetExpire(1)
cache.Update(1, 12)
expire2 := cache.GetExpire(1)
t.Assert(cache.GetVar(1), 12)
expire2, _ := cache.GetExpire(1)
v, _ := cache.GetVar(1)
t.Assert(v, 12)
t.Assert(math.Ceil(expire1.Seconds()), math.Ceil(expire2.Seconds()))
})
}
@ -104,34 +121,43 @@ func TestCache_UpdateExpire(t *testing.T) {
key := guid.S()
gcache.Set(key, 11, 3*time.Second)
defer gcache.Remove(key)
oldExpire := gcache.GetExpire(key)
oldExpire, _ := gcache.GetExpire(key)
newExpire := 10 * time.Second
gcache.UpdateExpire(key, newExpire)
t.AssertNE(gcache.GetExpire(key), oldExpire)
t.Assert(math.Ceil(gcache.GetExpire(key).Seconds()), 10)
e, _ := gcache.GetExpire(key)
t.AssertNE(e, oldExpire)
e, _ = gcache.GetExpire(key)
t.Assert(math.Ceil(e.Seconds()), 10)
})
// gcache.Cache
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Set(1, 11, 3*time.Second)
oldExpire := cache.GetExpire(1)
oldExpire, _ := cache.GetExpire(1)
newExpire := 10 * time.Second
cache.UpdateExpire(1, newExpire)
t.AssertNE(cache.GetExpire(1), oldExpire)
t.Assert(math.Ceil(cache.GetExpire(1).Seconds()), 10)
e, _ := cache.GetExpire(1)
t.AssertNE(e, oldExpire)
e, _ = cache.GetExpire(1)
t.Assert(math.Ceil(e.Seconds()), 10)
})
}
func TestCache_Keys_Values(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
c := gcache.New()
for i := 0; i < 10; i++ {
cache.Set(i, i*10, 0)
t.Assert(c.Set(i, i*10, 0), nil)
}
t.Assert(len(cache.Keys()), 10)
t.Assert(len(cache.Values()), 10)
t.AssertIN(0, cache.Keys())
t.AssertIN(90, cache.Values())
var (
keys, _ = c.Keys()
values, _ = c.Values()
)
t.Assert(len(keys), 10)
t.Assert(len(values), 10)
t.AssertIN(0, keys)
t.AssertIN(90, values)
})
}
@ -141,22 +167,30 @@ func TestCache_LRU(t *testing.T) {
for i := 0; i < 10; i++ {
cache.Set(i, i, 0)
}
t.Assert(cache.Size(), 10)
t.Assert(cache.Get(6), 6)
n, _ := cache.Size()
t.Assert(n, 10)
v, _ := cache.Get(6)
t.Assert(v, 6)
time.Sleep(4 * time.Second)
t.Assert(cache.Size(), 2)
t.Assert(cache.Get(6), 6)
t.Assert(cache.Get(1), nil)
cache.Close()
n, _ = cache.Size()
t.Assert(n, 2)
v, _ = cache.Get(6)
t.Assert(v, 6)
v, _ = cache.Get(1)
t.Assert(v, nil)
t.Assert(cache.Close(), nil)
})
}
func TestCache_LRU_expire(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New(2)
cache.Set(1, nil, 1000)
t.Assert(cache.Size(), 1)
t.Assert(cache.Get(1), nil)
t.Assert(cache.Set(1, nil, 1000), nil)
n, _ := cache.Size()
t.Assert(n, 1)
v, _ := cache.Get(1)
t.Assert(v, nil)
})
}
@ -164,17 +198,22 @@ func TestCache_SetIfNotExist(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.SetIfNotExist(1, 11, 0)
t.Assert(cache.Get(1), 11)
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.SetIfNotExist(1, 22, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
cache.SetIfNotExist(2, 22, 0)
t.Assert(cache.Get(2), 22)
v, _ = cache.Get(2)
t.Assert(v, 22)
gcache.Removes(g.Slice{1, 2, 3})
gcache.SetIfNotExist(1, 11, 0)
t.Assert(gcache.Get(1), 11)
v, _ = gcache.Get(1)
t.Assert(v, 11)
gcache.SetIfNotExist(1, 22, 0)
t.Assert(gcache.Get(1), 11)
v, _ = gcache.Get(1)
t.Assert(v, 11)
})
}
@ -182,11 +221,13 @@ func TestCache_Sets(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(cache.Get(1), 11)
v, _ := cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
@ -194,63 +235,82 @@ func TestCache_GetOrSet(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.GetOrSet(1, 11, 0)
t.Assert(cache.Get(1), 11)
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.GetOrSet(1, 111, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.GetOrSet(1, 11, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.GetOrSet(1, 111, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
func TestCache_GetOrSetFunc(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.GetOrSetFunc(1, func() interface{} {
return 11
cache.GetOrSetFunc(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(cache.Get(1), 11)
cache.GetOrSetFunc(1, func() interface{} {
return 111
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.GetOrSetFunc(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.GetOrSetFunc(1, func() interface{} {
return 11
gcache.GetOrSetFunc(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(gcache.Get(1), 11)
gcache.GetOrSetFunc(1, func() interface{} {
return 111
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.GetOrSetFunc(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
func TestCache_GetOrSetFuncLock(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
cache := gcache.New()
cache.GetOrSetFuncLock(1, func() interface{} {
return 11
cache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(cache.Get(1), 11)
cache.GetOrSetFuncLock(1, func() interface{} {
return 111
v, _ := cache.Get(1)
t.Assert(v, 11)
cache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(cache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.Removes(g.Slice{1, 2, 3})
gcache.GetOrSetFuncLock(1, func() interface{} {
return 11
gcache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 11, nil
}, 0)
t.Assert(gcache.Get(1), 11)
gcache.GetOrSetFuncLock(1, func() interface{} {
return 111
v, _ = cache.Get(1)
t.Assert(v, 11)
gcache.GetOrSetFuncLock(1, func() (interface{}, error) {
return 111, nil
}, 0)
t.Assert(gcache.Get(1), 11)
v, _ = cache.Get(1)
t.Assert(v, 11)
})
}
@ -259,7 +319,8 @@ func TestCache_Clear(t *testing.T) {
cache := gcache.New()
cache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
cache.Clear()
t.Assert(cache.Size(), 0)
n, _ := cache.Size()
t.Assert(n, 0)
})
}
@ -298,47 +359,57 @@ func TestCache_Basic(t *testing.T) {
{
cache := gcache.New()
cache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(cache.Contains(1), true)
t.Assert(cache.Get(1), 11)
data := cache.Data()
b, _ := cache.Contains(1)
t.Assert(b, true)
v, _ := cache.Get(1)
t.Assert(v, 11)
data, _ := cache.Data()
t.Assert(data[1], 11)
t.Assert(data[2], 22)
t.Assert(data[3], nil)
t.Assert(cache.Size(), 2)
keys := cache.Keys()
n, _ := cache.Size()
t.Assert(n, 2)
keys, _ := cache.Keys()
t.Assert(gset.NewFrom(g.Slice{1, 2}).Equal(gset.NewFrom(keys)), true)
keyStrs := cache.KeyStrings()
keyStrs, _ := cache.KeyStrings()
t.Assert(gset.NewFrom(g.Slice{"1", "2"}).Equal(gset.NewFrom(keyStrs)), true)
values := cache.Values()
values, _ := cache.Values()
t.Assert(gset.NewFrom(g.Slice{11, 22}).Equal(gset.NewFrom(values)), true)
removeData1 := cache.Remove(1)
removeData1, _ := cache.Remove(1)
t.Assert(removeData1, 11)
t.Assert(cache.Size(), 1)
n, _ = cache.Size()
t.Assert(n, 1)
cache.Removes(g.Slice{2})
t.Assert(cache.Size(), 0)
n, _ = cache.Size()
t.Assert(n, 0)
}
gcache.Remove(g.Slice{1, 2, 3}...)
{
gcache.Sets(g.MapAnyAny{1: 11, 2: 22}, 0)
t.Assert(gcache.Contains(1), true)
t.Assert(gcache.Get(1), 11)
data := gcache.Data()
b, _ := gcache.Contains(1)
t.Assert(b, true)
v, _ := gcache.Get(1)
t.Assert(v, 11)
data, _ := gcache.Data()
t.Assert(data[1], 11)
t.Assert(data[2], 22)
t.Assert(data[3], nil)
t.Assert(gcache.Size(), 2)
keys := gcache.Keys()
n, _ := gcache.Size()
t.Assert(n, 2)
keys, _ := gcache.Keys()
t.Assert(gset.NewFrom(g.Slice{1, 2}).Equal(gset.NewFrom(keys)), true)
keyStrs := gcache.KeyStrings()
keyStrs, _ := gcache.KeyStrings()
t.Assert(gset.NewFrom(g.Slice{"1", "2"}).Equal(gset.NewFrom(keyStrs)), true)
values := gcache.Values()
values, _ := gcache.Values()
t.Assert(gset.NewFrom(g.Slice{11, 22}).Equal(gset.NewFrom(values)), true)
removeData1 := gcache.Remove(1)
removeData1, _ := gcache.Remove(1)
t.Assert(removeData1, 11)
t.Assert(gcache.Size(), 1)
n, _ = gcache.Size()
t.Assert(n, 1)
gcache.Removes(g.Slice{2})
t.Assert(gcache.Size(), 0)
n, _ = gcache.Size()
t.Assert(n, 0)
}
})
}

View File

@ -39,7 +39,7 @@ func GetBytesWithCache(path string, duration ...time.Duration) []byte {
if len(duration) > 0 {
expire = duration[0]
}
r := gcache.GetOrSetFuncLock(key, func() interface{} {
r, _ := gcache.GetOrSetFuncLock(key, func() (interface{}, error) {
b := GetBytes(path)
if b != nil {
// Adding this <path> to gfsnotify,
@ -49,7 +49,7 @@ func GetBytesWithCache(path string, duration ...time.Duration) []byte {
gfsnotify.Exit()
})
}
return b
return b, nil
}, expire)
if r != nil {
return r.([]byte)

View File

@ -12,6 +12,7 @@ import (
"fmt"
"github.com/gogf/gf/container/gset"
"github.com/gogf/gf/internal/intlog"
"sync"
"time"
"github.com/fsnotify/fsnotify"
@ -67,20 +68,12 @@ const (
)
var (
mu sync.Mutex // Mutex for concurrent safety of defaultWatcher.
defaultWatcher *Watcher // Default watcher.
callbackIdMap = gmap.NewIntAnyMap(true) // Id to callback mapping.
callbackIdGenerator = gtype.NewInt() // Atomic id generator for callback.
)
func init() {
var err error
defaultWatcher, err = New()
if err != nil {
// Default watcher object must be created, or else it panics.
panic(fmt.Sprintf(`creating default fsnotify watcher failed: %s`, err.Error()))
}
}
// New creates and returns a new watcher.
// Note that the watcher number is limited by the file handle setting of the system.
// Eg: fs.inotify.max_user_instances system variable in linux systems.
@ -106,7 +99,11 @@ func New() (*Watcher, error) {
// Add monitors <path> using default watcher with callback function <callbackFunc>.
// The optional parameter <recursive> specifies whether monitoring the <path> recursively, which is true in default.
func Add(path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) {
return defaultWatcher.Add(path, callbackFunc, recursive...)
w, err := getDefaultWatcher()
if err != nil {
return nil, err
}
return w.Add(path, callbackFunc, recursive...)
}
// AddOnce monitors <path> using default watcher with callback function <callbackFunc> only once using unique name <name>.
@ -115,16 +112,28 @@ func Add(path string, callbackFunc func(event *Event), recursive ...bool) (callb
//
// The optional parameter <recursive> specifies whether monitoring the <path> recursively, which is true in default.
func AddOnce(name, path string, callbackFunc func(event *Event), recursive ...bool) (callback *Callback, err error) {
return defaultWatcher.AddOnce(name, path, callbackFunc, recursive...)
w, err := getDefaultWatcher()
if err != nil {
return nil, err
}
return w.AddOnce(name, path, callbackFunc, recursive...)
}
// Remove removes all monitoring callbacks of given <path> from watcher recursively.
func Remove(path string) error {
return defaultWatcher.Remove(path)
w, err := getDefaultWatcher()
if err != nil {
return err
}
return w.Remove(path)
}
// RemoveCallback removes specified callback with given id from watcher.
func RemoveCallback(callbackId int) error {
w, err := getDefaultWatcher()
if err != nil {
return err
}
callback := (*Callback)(nil)
if r := callbackIdMap.Get(callbackId); r != nil {
callback = r.(*Callback)
@ -132,7 +141,7 @@ func RemoveCallback(callbackId int) error {
if callback == nil {
return errors.New(fmt.Sprintf(`callback for id %d not found`, callbackId))
}
defaultWatcher.RemoveCallback(callbackId)
w.RemoveCallback(callbackId)
return nil
}
@ -141,3 +150,16 @@ func RemoveCallback(callbackId int) error {
func Exit() {
panic(callbackExitEventPanicStr)
}
// getDefaultWatcher creates and returns the default watcher.
// This is used for lazy initialization purpose.
func getDefaultWatcher() (*Watcher, error) {
mu.Lock()
defer mu.Unlock()
if defaultWatcher != nil {
return defaultWatcher, nil
}
var err error
defaultWatcher, err = New()
return defaultWatcher, err
}

View File

@ -23,14 +23,14 @@ func (w *Watcher) startWatchLoop() {
// Event listening.
case ev := <-w.watcher.Events:
// Filter the repeated event in custom duration.
w.cache.SetIfNotExist(ev.String(), func() interface{} {
w.cache.SetIfNotExist(ev.String(), func() (interface{}, error) {
w.events.Push(&Event{
event: ev,
Path: ev.Name,
Op: Op(ev.Op),
Watcher: w,
})
return struct{}{}
return struct{}{}, nil
}, repeatEventFilterDuration)
case err := <-w.watcher.Errors:

View File

@ -40,7 +40,7 @@ func (s *Session) init() {
if s.id != "" {
var err error
// Retrieve memory session data from manager.
if r := s.manager.sessionData.Get(s.id); r != nil {
if r, _ := s.manager.sessionData.Get(s.id); r != nil {
s.data = r.(*gmap.StrAnyMap)
intlog.Print("session init data:", s.data)
}
@ -50,11 +50,6 @@ func (s *Session) init() {
intlog.Errorf("session restoring failed for id '%s': %v", s.id, err)
}
}
// If it's an invalid or expired session id,
// it should create a new session id.
if s.data == nil {
s.id = ""
}
}
// Use custom session id creating function.
if s.id == "" && s.idFunc != nil {

View File

@ -14,7 +14,7 @@ import (
// Time is a wrapper for time.Time for additional features.
type Time struct {
TimeWrapper
wrapper
}
// apiUnixNano is an interface definition commonly for custom time.Time wrapper.
@ -28,7 +28,6 @@ func New(param ...interface{}) *Time {
if len(param) > 0 {
switch r := param[0].(type) {
case time.Time:
r.Nanosecond()
return NewFromTime(r)
case *time.Time:
return NewFromTime(*r)
@ -47,21 +46,21 @@ func New(param ...interface{}) *Time {
}
}
return &Time{
TimeWrapper{time.Time{}},
wrapper{time.Time{}},
}
}
// Now creates and returns a time object of now.
func Now() *Time {
return &Time{
TimeWrapper{time.Now()},
wrapper{time.Now()},
}
}
// NewFromTime creates and returns a Time object with given time.Time object.
func NewFromTime(t time.Time) *Time {
return &Time{
TimeWrapper{t},
wrapper{t},
}
}
@ -112,7 +111,7 @@ func NewFromTimeStamp(timestamp int64) *Time {
sec = timestamp
}
return &Time{
TimeWrapper{time.Unix(sec, nano)},
wrapper{time.Unix(sec, nano)},
}
}
@ -202,31 +201,31 @@ func (t *Time) Clone() *Time {
// Add adds the duration to current time.
func (t *Time) Add(d time.Duration) *Time {
t.Time = t.Time.Add(d)
return t
newTime := t.Clone()
newTime.Time = newTime.Time.Add(d)
return newTime
}
// AddStr parses the given duration as string and adds it to current time.
func (t *Time) AddStr(duration string) error {
func (t *Time) AddStr(duration string) (*Time, error) {
if d, err := time.ParseDuration(duration); err != nil {
return err
return nil, err
} else {
t.Time = t.Time.Add(d)
return t.Add(d), nil
}
return nil
}
// ToLocation converts current time to specified location.
func (t *Time) ToLocation(location *time.Location) *Time {
t.Time = t.Time.In(location)
return t
newTime := t.Clone()
newTime.Time = newTime.Time.In(location)
return newTime
}
// ToZone converts current time to specified zone like: Asia/Shanghai.
func (t *Time) ToZone(zone string) (*Time, error) {
if l, err := time.LoadLocation(zone); err == nil {
t.Time = t.Time.In(l)
return t, nil
return t.ToLocation(l), nil
} else {
return nil, err
}
@ -234,8 +233,9 @@ func (t *Time) ToZone(zone string) (*Time, error) {
// UTC converts current time to UTC timezone.
func (t *Time) UTC() *Time {
t.Time = t.Time.UTC()
return t
newTime := t.Clone()
newTime.Time = newTime.Time.UTC()
return newTime
}
// ISO8601 formats the time as ISO8601 and returns it as string.
@ -250,14 +250,16 @@ func (t *Time) RFC822() string {
// Local converts the time to local timezone.
func (t *Time) Local() *Time {
t.Time = t.Time.Local()
return t
newTime := t.Clone()
newTime.Time = newTime.Time.Local()
return newTime
}
// AddDate adds year, month and day to the time.
func (t *Time) AddDate(years int, months int, days int) *Time {
t.Time = t.Time.AddDate(years, months, days)
return t
newTime := t.Clone()
newTime.Time = newTime.Time.AddDate(years, months, days)
return newTime
}
// Round returns the result of rounding t to the nearest multiple of d (since the zero time).
@ -269,8 +271,9 @@ func (t *Time) AddDate(years int, months int, days int) *Time {
// time. Thus, Round(Hour) may return a time with a non-zero
// minute, depending on the time's Location.
func (t *Time) Round(d time.Duration) *Time {
t.Time = t.Time.Round(d)
return t
newTime := t.Clone()
newTime.Time = newTime.Time.Round(d)
return newTime
}
// Truncate returns the result of rounding t down to a multiple of d (since the zero time).
@ -281,8 +284,9 @@ func (t *Time) Round(d time.Duration) *Time {
// time. Thus, Truncate(Hour) may return a time with a non-zero
// minute, depending on the time's Location.
func (t *Time) Truncate(d time.Duration) *Time {
t.Time = t.Time.Truncate(d)
return t
newTime := t.Clone()
newTime.Time = newTime.Time.Truncate(d)
return newTime
}
// Equal reports whether t and u represent the same time instant.

View File

@ -10,14 +10,14 @@ import (
"time"
)
// TimeWrapper is a wrapper for stdlib struct time.Time.
// wrapper is a wrapper for stdlib struct time.Time.
// It's used for overwriting some functions of time.Time, for example: String.
type TimeWrapper struct {
type wrapper struct {
time.Time
}
// String overwrites the String function of time.Time.
func (t TimeWrapper) String() string {
func (t wrapper) String() string {
if t.IsZero() {
return ""
}

View File

@ -75,7 +75,7 @@ func Test_String(t *testing.T) {
t2 := *t1
t.Assert(t2.String(), "2006-01-02 15:04:05")
t.Assert(fmt.Sprintf("%s", t2), "{2006-01-02 15:04:05}")
t.Assert(fmt.Sprintf("{%s}", t2.String()), "{2006-01-02 15:04:05}")
})
}
@ -188,7 +188,7 @@ func Test_ToTime(t *testing.T) {
func Test_Add(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timeTemp := gtime.NewFromStr("2006-01-02 15:04:05")
timeTemp.Add(time.Second)
timeTemp = timeTemp.Add(time.Second)
t.Assert(timeTemp.Format("Y-m-d H:i:s"), "2006-01-02 15:04:06")
})
}
@ -196,15 +196,14 @@ func Test_Add(t *testing.T) {
func Test_ToZone(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timeTemp := gtime.Now()
//
timeTemp.ToZone("America/Los_Angeles")
timeTemp, _ = timeTemp.ToZone("America/Los_Angeles")
t.Assert(timeTemp.Time.Location().String(), "America/Los_Angeles")
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
t.Error("test fail")
}
timeTemp.ToLocation(loc)
timeTemp = timeTemp.ToLocation(loc)
t.Assert(timeTemp.Time.Location().String(), "Asia/Shanghai")
timeTemp1, _ := timeTemp.ToZone("errZone")
@ -217,7 +216,7 @@ func Test_ToZone(t *testing.T) {
func Test_AddDate(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timeTemp := gtime.NewFromStr("2006-01-02 15:04:05")
timeTemp.AddDate(1, 2, 3)
timeTemp = timeTemp.AddDate(1, 2, 3)
t.Assert(timeTemp.Format("Y-m-d H:i:s"), "2007-03-05 15:04:05")
})
}
@ -244,7 +243,7 @@ func Test_Round(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timeTemp := gtime.Now()
timeTemp1 := timeTemp.Time
timeTemp.Round(time.Hour)
timeTemp = timeTemp.Round(time.Hour)
t.Assert(timeTemp.UnixNano(), timeTemp1.Round(time.Hour).UnixNano())
})
}
@ -253,7 +252,7 @@ func Test_Truncate(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
timeTemp := gtime.Now()
timeTemp1 := timeTemp.Time
timeTemp.Truncate(time.Hour)
timeTemp = timeTemp.Truncate(time.Hour)
t.Assert(timeTemp.UnixNano(), timeTemp1.Truncate(time.Hour).UnixNano())
})
}

View File

@ -24,8 +24,7 @@ import (
)
var (
numberSequence = regexp.MustCompile(`([a-zA-Z])(\d+)([a-zA-Z]?)`)
numberReplacement = []byte(`$1 $2 $3`)
numberSequence = regexp.MustCompile(`([a-zA-Z]{0,1})(\d+)([a-zA-Z]{0,1})`)
firstCamelCaseStart = regexp.MustCompile(`([A-Z]+)([A-Z]?[_a-z\d]+)|$`)
firstCamelCaseEnd = regexp.MustCompile(`([\w\W]*?)([_]?[A-Z]+)$`)
)
@ -139,9 +138,21 @@ func DelimitedScreamingCase(s string, del uint8, screaming bool) string {
}
func addWordBoundariesToNumbers(s string) string {
b := []byte(s)
b = numberSequence.ReplaceAll(b, numberReplacement)
return string(b)
r := numberSequence.ReplaceAllFunc([]byte(s), func(bytes []byte) []byte {
var result []byte
match := numberSequence.FindSubmatch(bytes)
if len(match[1]) > 0 {
result = append(result, match[1]...)
result = append(result, []byte(" ")...)
}
result = append(result, match[2]...)
if len(match[3]) > 0 {
result = append(result, []byte(" ")...)
result = append(result, match[3]...)
}
return result
})
return string(r)
}
// Converts a string to CamelCase

View File

@ -130,6 +130,7 @@ func Test_SnakeScreamingCase(t *testing.T) {
func Test_KebabCase(t *testing.T) {
cases := [][]string{
{"testCase", "test-case"},
{"optimization1.0.0", "optimization-1-0-0"},
}
for _, i := range cases {
in := i[0]

View File

@ -42,7 +42,15 @@ func doMapConvert(value interface{}, recursive bool, tags ...string) map[string]
if value == nil {
return nil
}
newTags := StructTagPriority
switch len(tags) {
case 0:
// No need handle.
case 1:
newTags = append(strings.Split(tags[0], ","), StructTagPriority...)
default:
newTags = append(tags, StructTagPriority...)
}
// Assert the common combination of types, and finally it uses reflection.
dataMap := make(map[string]interface{})
switch r := value.(type) {
@ -66,7 +74,7 @@ func doMapConvert(value interface{}, recursive bool, tags ...string) map[string]
}
case map[interface{}]interface{}:
for k, v := range r {
dataMap[String(k)] = v
dataMap[String(k)] = doMapConvertForMapOrStructValue(false, v, recursive, newTags...)
}
case map[interface{}]string:
for k, v := range r {
@ -109,10 +117,18 @@ func doMapConvert(value interface{}, recursive bool, tags ...string) map[string]
dataMap[k] = v
}
case map[string]interface{}:
return r
if recursive {
// A copy of current map.
for k, v := range r {
dataMap[k] = doMapConvertForMapOrStructValue(false, v, recursive, newTags...)
}
} else {
// It returns the map directly without any changing.
return r
}
case map[int]interface{}:
for k, v := range r {
dataMap[String(k)] = v
dataMap[String(k)] = doMapConvertForMapOrStructValue(false, v, recursive, newTags...)
}
case map[int]string:
for k, v := range r {
@ -151,117 +167,189 @@ func doMapConvert(value interface{}, recursive bool, tags ...string) map[string]
dataMap[String(rv.Index(i).Interface())] = nil
}
}
case reflect.Map:
ks := rv.MapKeys()
for _, k := range ks {
dataMap[String(k.Interface())] = rv.MapIndex(k).Interface()
case reflect.Map, reflect.Struct:
convertedValue := doMapConvertForMapOrStructValue(true, value, recursive, newTags...)
if m, ok := convertedValue.(map[string]interface{}); ok {
return m
}
case reflect.Struct:
// Map converting interface check.
if v, ok := value.(apiMapStrAny); ok {
return v.MapStrAny()
}
// Using reflect for converting.
var (
rtField reflect.StructField
rvField reflect.Value
rt = rv.Type()
name = ""
tagArray = StructTagPriority
return nil
default:
return nil
}
}
return dataMap
}
func doMapConvertForMapOrStructValue(isRoot bool, value interface{}, recursive bool, tags ...string) interface{} {
if isRoot == false && recursive == false {
return value
}
var rv reflect.Value
if v, ok := value.(reflect.Value); ok {
rv = v
value = v.Interface()
} else {
rv = reflect.ValueOf(value)
}
kind := rv.Kind()
// If it is a pointer, we should find its real data type.
for kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()
}
switch kind {
case reflect.Map:
var (
mapKeys = rv.MapKeys()
dataMap = make(map[string]interface{})
)
for _, k := range mapKeys {
dataMap[String(k.Interface())] = doMapConvertForMapOrStructValue(
false,
rv.MapIndex(k).Interface(),
recursive,
tags...,
)
switch len(tags) {
case 0:
// No need handle.
case 1:
tagArray = append(strings.Split(tags[0], ","), StructTagPriority...)
default:
tagArray = append(tags, StructTagPriority...)
}
if len(dataMap) == 0 {
return value
}
return dataMap
case reflect.Struct:
// Map converting interface check.
if v, ok := value.(apiMapStrAny); ok {
m := v.MapStrAny()
if recursive {
for k, v := range m {
m[k] = doMapConvertForMapOrStructValue(false, v, recursive, tags...)
}
}
for i := 0; i < rv.NumField(); i++ {
rtField = rt.Field(i)
rvField = rv.Field(i)
// Only convert the public attributes.
fieldName := rtField.Name
if !utils.IsLetterUpper(fieldName[0]) {
return m
}
// Using reflect for converting.
var (
rtField reflect.StructField
rvField reflect.Value
dataMap = make(map[string]interface{}) // result map.
rt = rv.Type() // attribute value type.
name = "" // name may be the tag name or the struct attribute name.
)
for i := 0; i < rv.NumField(); i++ {
rtField = rt.Field(i)
rvField = rv.Field(i)
// Only convert the public attributes.
fieldName := rtField.Name
if !utils.IsLetterUpper(fieldName[0]) {
continue
}
name = ""
fieldTag := rtField.Tag
for _, tag := range tags {
if name = fieldTag.Get(tag); name != "" {
break
}
}
if name == "" {
name = fieldName
} else {
// Support json tag feature: -, omitempty
name = strings.TrimSpace(name)
if name == "-" {
continue
}
name = ""
fieldTag := rtField.Tag
for _, tag := range tagArray {
if name = fieldTag.Get(tag); name != "" {
break
}
}
if name == "" {
name = fieldName
} else {
// Support json tag feature: -, omitempty
name = strings.TrimSpace(name)
if name == "-" {
continue
}
array := strings.Split(name, ",")
if len(array) > 1 {
switch strings.TrimSpace(array[1]) {
case "omitempty":
if empty.IsEmpty(rvField.Interface()) {
continue
} else {
name = strings.TrimSpace(array[0])
}
default:
array := strings.Split(name, ",")
if len(array) > 1 {
switch strings.TrimSpace(array[1]) {
case "omitempty":
if empty.IsEmpty(rvField.Interface()) {
continue
} else {
name = strings.TrimSpace(array[0])
}
default:
name = strings.TrimSpace(array[0])
}
}
if recursive {
}
if recursive {
// Do map converting recursively.
var (
rvAttrField = rvField
rvAttrKind = rvField.Kind()
)
if rvAttrKind == reflect.Ptr {
rvAttrField = rvField.Elem()
rvAttrKind = rvAttrField.Kind()
}
switch rvAttrKind {
case reflect.Struct:
var (
rvAttrField = rvField
rvAttrKind = rvField.Kind()
hasNoTag = name == fieldName
rvAttrInterface = rvAttrField.Interface()
)
if rvAttrKind == reflect.Ptr {
rvAttrField = rvField.Elem()
rvAttrKind = rvAttrField.Kind()
}
if rvAttrKind == reflect.Struct {
var (
hasNoTag = name == fieldName
rvAttrInterface = rvAttrField.Interface()
)
if hasNoTag && rtField.Anonymous {
// It means this attribute field has no tag.
// Overwrite the attribute with sub-struct attribute fields.
for k, v := range doMapConvert(rvAttrInterface, recursive, tags...) {
if hasNoTag && rtField.Anonymous {
// It means this attribute field has no tag.
// Overwrite the attribute with sub-struct attribute fields.
anonymousValue := doMapConvertForMapOrStructValue(false, rvAttrInterface, recursive, tags...)
if m, ok := anonymousValue.(map[string]interface{}); ok {
for k, v := range m {
dataMap[k] = v
}
} else {
// It means this attribute field has desired tag.
if m := doMapConvert(rvAttrInterface, recursive, tags...); len(m) > 0 {
dataMap[name] = m
} else {
dataMap[name] = rv.Field(i).Interface()
}
dataMap[name] = rvAttrInterface
}
} else {
if rvField.IsValid() {
dataMap[name] = rv.Field(i).Interface()
} else {
dataMap[name] = nil
}
// It means this attribute field has desired tag.
dataMap[name] = doMapConvertForMapOrStructValue(false, rvAttrInterface, recursive, tags...)
}
} else {
// The struct attribute is type of slice.
case reflect.Array, reflect.Slice:
length := rvField.Len()
if length == 0 {
dataMap[name] = rvField.Interface()
break
}
array := make([]interface{}, length)
for i := 0; i < length; i++ {
array[i] = doMapConvertForMapOrStructValue(false, rvField.Index(i), recursive, tags...)
}
dataMap[name] = array
default:
if rvField.IsValid() {
dataMap[name] = rv.Field(i).Interface()
} else {
dataMap[name] = nil
}
}
} else {
// No recursive map value converting
if rvField.IsValid() {
dataMap[name] = rv.Field(i).Interface()
} else {
dataMap[name] = nil
}
}
default:
return nil
}
if len(dataMap) == 0 {
return value
}
return dataMap
// The given value is type of slice.
case reflect.Array, reflect.Slice:
length := rv.Len()
if length == 0 {
break
}
array := make([]interface{}, rv.Len())
for i := 0; i < length; i++ {
array[i] = doMapConvertForMapOrStructValue(false, rv.Index(i), recursive, tags...)
}
return array
}
return dataMap
return value
}
// MapStrStr converts <value> to map[string]string.

View File

@ -31,6 +31,7 @@ func SliceStructDeep(params interface{}, pointer interface{}, mapping ...map[str
}
// Maps converts <i> 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 {
return nil

View File

@ -10,6 +10,7 @@ import (
"fmt"
"github.com/gogf/gf/errors/gerror"
"github.com/gogf/gf/internal/empty"
"github.com/gogf/gf/internal/json"
"reflect"
"regexp"
"strings"
@ -50,7 +51,8 @@ func StructDeep(params interface{}, pointer interface{}, mapping ...map[string]s
// doStruct is the core internal converting function for any data to struct recursively or not.
func doStruct(params interface{}, pointer interface{}, recursive bool, mapping ...map[string]string) (err error) {
if params == nil {
return gerror.New("params cannot be nil")
// If <params> is nil, no conversion.
return nil
}
if pointer == nil {
return gerror.New("object pointer cannot be nil")
@ -62,6 +64,18 @@ func doStruct(params interface{}, pointer interface{}, recursive bool, mapping .
}
}()
// If given <params> is JSON, it then uses json.Unmarshal doing the converting.
switch r := params.(type) {
case []byte:
if json.Valid(r) {
return json.Unmarshal(r, pointer)
}
case string:
if paramsBytes := []byte(r); json.Valid(paramsBytes) {
return json.Unmarshal(paramsBytes, pointer)
}
}
// UnmarshalValue.
// Assign value with interface UnmarshalValue.
// Note that only pointer can implement interface UnmarshalValue.
@ -73,7 +87,7 @@ func doStruct(params interface{}, pointer interface{}, recursive bool, mapping .
// DO NOT use MapDeep here.
paramsMap := Map(params)
if paramsMap == nil {
return gerror.Newf("invalid params: %v", params)
return gerror.Newf("convert params to map failed: %v", params)
}
// Using reflect to do the converting,
@ -258,8 +272,11 @@ func bindVarToStructAttr(elem reflect.Value, name string, value interface{}, rec
return nil
}
defer func() {
if recover() != nil {
if e := recover(); e != nil {
err = bindVarToReflectValue(structFieldValue, value, recursive, mapping...)
if err != nil {
err = gerror.Wrapf(err, `error binding value to attribute "%s"`, name)
}
}
}()
if empty.IsNil(value) {
@ -282,8 +299,7 @@ func bindVarToReflectValue(structFieldValue reflect.Value, value interface{}, re
v.Set(value)
return nil
} else if v, ok := structFieldValue.Interface().(apiUnmarshalValue); ok {
err = v.UnmarshalValue(value)
if err == nil {
if err = v.UnmarshalValue(value); err == nil {
return err
}
}

View File

@ -8,6 +8,7 @@ package gconv
import (
"github.com/gogf/gf/errors/gerror"
"github.com/gogf/gf/internal/json"
"reflect"
)
@ -23,14 +24,15 @@ func StructsDeep(params interface{}, pointer interface{}, mapping ...map[string]
// doStructs converts any slice to given struct slice.
//
// The parameter <params> should be type of slice.
// It automatically checks and converts json string to []map if <params> is string/[]byte.
//
// The parameter <pointer> should be type of pointer to slice of struct.
// Note that if <pointer> is a pointer to another pointer of type of slice of struct,
// it will create the struct/pointer internally.
func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...map[string]string) (err error) {
if params == nil {
return gerror.New("params cannot be nil")
// If <params> is nil, no conversion.
return nil
}
if pointer == nil {
return gerror.New("object pointer cannot be nil")
@ -41,6 +43,18 @@ func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...ma
err = gerror.NewfSkip(1, "%v", e)
}
}()
// If given <params> is JSON, it then uses json.Unmarshal doing the converting.
switch r := params.(type) {
case []byte:
if json.Valid(r) {
return json.Unmarshal(r, pointer)
}
case string:
if paramsBytes := []byte(r); json.Valid(paramsBytes) {
return json.Unmarshal(paramsBytes, pointer)
}
}
// Pointer type check.
pointerRv, ok := pointer.(reflect.Value)
if !ok {
pointerRv = reflect.ValueOf(pointer)
@ -48,57 +62,46 @@ func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...ma
return gerror.Newf("pointer should be type of pointer, but got: %v", kind)
}
}
params = Maps(params)
var (
reflectValue = reflect.ValueOf(params)
reflectKind = reflectValue.Kind()
)
for reflectKind == reflect.Ptr {
reflectValue = reflectValue.Elem()
reflectKind = reflectValue.Kind()
}
switch reflectKind {
case reflect.Slice, reflect.Array:
// If <params> is an empty slice, no conversion.
if reflectValue.Len() == 0 {
return nil
}
var (
array = reflect.MakeSlice(pointerRv.Type().Elem(), reflectValue.Len(), reflectValue.Len())
itemType = array.Index(0).Type()
)
for i := 0; i < reflectValue.Len(); i++ {
if itemType.Kind() == reflect.Ptr {
// Slice element is type pointer.
e := reflect.New(itemType.Elem()).Elem()
if deep {
if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
return err
}
} else {
if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
return err
}
}
array.Index(i).Set(e.Addr())
} else {
// Slice element is not type of pointer.
e := reflect.New(itemType).Elem()
if deep {
if err = StructDeep(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
return err
}
} else {
if err = Struct(reflectValue.Index(i).Interface(), e, mapping...); err != nil {
return err
}
}
array.Index(i).Set(e)
}
}
pointerRv.Elem().Set(array)
// Converting <params> to map slice.
paramsMaps := Maps(params)
// If <params> is an empty slice, no conversion.
if len(paramsMaps) == 0 {
return nil
default:
return gerror.Newf("params should be type of slice, but got: %v", reflectKind)
}
var (
array = reflect.MakeSlice(pointerRv.Type().Elem(), len(paramsMaps), len(paramsMaps))
itemType = array.Index(0).Type()
)
for i := 0; i < len(paramsMaps); i++ {
if itemType.Kind() == reflect.Ptr {
// Slice element is type pointer.
e := reflect.New(itemType.Elem()).Elem()
if deep {
if err = StructDeep(paramsMaps[i], e, mapping...); err != nil {
return err
}
} else {
if err = Struct(paramsMaps[i], e, mapping...); err != nil {
return err
}
}
array.Index(i).Set(e.Addr())
} else {
// Slice element is not type of pointer.
e := reflect.New(itemType).Elem()
if deep {
if err = StructDeep(paramsMaps[i], e, mapping...); err != nil {
return err
}
} else {
if err = Struct(paramsMaps[i], e, mapping...); err != nil {
return err
}
}
array.Index(i).Set(e)
}
}
pointerRv.Elem().Set(array)
return nil
}

View File

@ -981,3 +981,23 @@ func Test_Struct_To_Struct(t *testing.T) {
t.Assert(TestA.Date, TestB.Date)
})
}
func Test_Struct_WithJson(t *testing.T) {
type A struct {
Name string
}
type B struct {
A
Score int
}
gtest.C(t, func(t *gtest.T) {
b1 := &B{}
b1.Name = "john"
b1.Score = 100
b, _ := json.Marshal(b1)
b2 := &B{}
err := gconv.Struct(b, b2)
t.Assert(err, nil)
t.Assert(b2, b1)
})
}

View File

@ -8,6 +8,7 @@
package gutil
import (
"fmt"
"github.com/gogf/gf/internal/empty"
)
@ -16,11 +17,24 @@ func Throw(exception interface{}) {
panic(exception)
}
// Try implements try... logistics using internal panic...recover.
// It returns error if any exception occurs, or else it returns nil.
func Try(try func()) (err error) {
defer func() {
if e := recover(); e != nil {
err = fmt.Errorf(`%v`, e)
}
}()
try()
return
}
// TryCatch implements try...catch... logistics using internal panic...recover.
func TryCatch(try func(), catch ...func(exception interface{})) {
// It automatically calls function <catch> if any exception occurs ans passes the exception as an error.
func TryCatch(try func(), catch ...func(exception error)) {
defer func() {
if e := recover(); e != nil && len(catch) > 0 {
catch[0](e)
catch[0](fmt.Errorf(`%v`, e))
}
}()
try()

View File

@ -15,6 +15,16 @@ import (
"reflect"
)
// apiString is used for type assert api for String().
type apiString interface {
String() string
}
// apiMapStrAny is the interface support for converting struct parameter to map.
type apiMapStrAny interface {
MapStrAny() map[string]interface{}
}
// Dump prints variables <i...> to stdout with more manually readable.
func Dump(i ...interface{}) {
s := Export(i...)
@ -27,11 +37,16 @@ func Dump(i ...interface{}) {
func Export(i ...interface{}) string {
buffer := bytes.NewBuffer(nil)
for _, v := range i {
if b, ok := v.([]byte); ok {
buffer.Write(b)
} else {
rv := reflect.ValueOf(v)
kind := rv.Kind()
switch r := v.(type) {
case []byte:
buffer.Write(r)
case string:
buffer.WriteString(r)
default:
var (
rv = reflect.ValueOf(v)
kind = rv.Kind()
)
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()
@ -39,8 +54,14 @@ func Export(i ...interface{}) string {
switch kind {
case reflect.Slice, reflect.Array:
v = gconv.Interfaces(v)
case reflect.Map, reflect.Struct:
case reflect.Map:
v = gconv.Map(v)
case reflect.Struct:
if r, ok := v.(apiMapStrAny); ok {
v = r.MapStrAny()
} else if r, ok := v.(apiString); ok {
v = r.String()
}
}
encoder := json.NewEncoder(buffer)
encoder.SetEscapeHTML(false)

View File

@ -25,7 +25,7 @@ func Benchmark_TryCatch(b *testing.B) {
for i := 0; i < b.N; i++ {
TryCatch(func() {
}, func(err interface{}) {
}, func(err error) {
})
}

View File

@ -21,6 +21,15 @@ func Test_Dump(t *testing.T) {
})
}
func Test_Try(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
s := `gutil Try test`
t.Assert(gutil.Try(func() {
panic(s)
}), s)
})
}
func Test_TryCatch(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
gutil.TryCatch(func() {
@ -32,21 +41,19 @@ func Test_TryCatch(t *testing.T) {
gutil.TryCatch(func() {
panic("gutil TryCatch test")
}, func(err interface{}) {
}, func(err error) {
t.Assert(err, "gutil TryCatch test")
})
})
}
func Test_IsEmpty(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
t.Assert(gutil.IsEmpty(1), false)
})
}
func Test_Throw(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
defer func() {
t.Assert(recover(), "gutil Throw test")

View File

@ -8,7 +8,7 @@ package gvalid
import (
"errors"
"github.com/gogf/gf/encoding/gjson"
"github.com/gogf/gf/internal/json"
"github.com/gogf/gf/net/gipv4"
"github.com/gogf/gf/net/gipv6"
"github.com/gogf/gf/os/gtime"
@ -172,9 +172,8 @@ func doCheck(key string, value interface{}, rules string, messages interface{},
for index := 0; index < len(ruleItems); {
var (
err error
item = ruleItems[index]
match = false
results = ruleRegex.FindStringSubmatch(item)
results = ruleRegex.FindStringSubmatch(ruleItems[index])
ruleKey = strings.TrimSpace(results[1])
rulePattern = strings.TrimSpace(results[2])
)
@ -191,7 +190,7 @@ func doCheck(key string, value interface{}, rules string, messages interface{},
if len(params) > 0 {
dataMap = gconv.Map(params[0])
}
if err := f(value, message, dataMap); err != nil {
if err := f(ruleItems[index], value, message, dataMap); err != nil {
match = false
errorMsgArray[ruleKey] = err.Error()
} else {
@ -453,7 +452,7 @@ func doCheckBuildInRules(
// Json.
case "json":
if _, err := gjson.Decode([]byte(valueStr)); err == nil {
if json.Valid([]byte(valueStr)) {
match = true
}

View File

@ -87,20 +87,27 @@ func CheckMap(params interface{}, rules interface{}, messages ...CustomMsg) *Err
if v, ok := data[key]; ok {
value = v
}
// It checks each rule and its value in loop.
if e := doCheck(key, value, rule, customMsgs[key], data); e != nil {
_, item := e.FirstItem()
// ===========================================================
// If value is nil or empty string and has no required* rules,
// clear the error message.
// Only in map and struct validations, if value is nil or empty
// string and has no required* rules, it clears the error message.
// ===========================================================
if gconv.String(value) == "" {
required := false
// rule => error
for k := range item {
// Default required rules.
if _, ok := mustCheckRulesEvenValueEmpty[k]; ok {
required = true
break
}
// Custom rules are also required in default.
if _, ok := customRuleFuncMap[k]; ok {
required = true
break
}
}
if !required {
continue

View File

@ -149,20 +149,27 @@ func CheckStruct(object interface{}, rules interface{}, messages ...CustomMsg) *
if v, ok := params[key]; ok {
value = v
}
// It checks each rule and its value in loop.
if e := doCheck(key, value, rule, customMessage[key], params); e != nil {
_, item := e.FirstItem()
// ===========================================================
// If value is nil or empty string and has no required* rules,
// clear the error message.
// Only in map and struct validations, if value is nil or empty
// string and has no required* rules, it clears the error message.
// ===========================================================
if value == nil || gconv.String(value) == "" {
required := false
// rule => error
for k := range item {
// Default required rules.
if _, ok := mustCheckRulesEvenValueEmpty[k]; ok {
required = true
break
}
// Custom rules are also required in default.
if _, ok := customRuleFuncMap[k]; ok {
required = true
break
}
}
if !required {
continue

Some files were not shown because too many files have changed in this diff Show More