Compare commits

...

21 Commits

Author SHA1 Message Date
da2ec21571 fix issue 2020-10-27 10:41:18 +08:00
ebb8d8a2f7 fix issue in 2020-10-27 10:40:47 +08:00
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
51 changed files with 908 additions and 239 deletions

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

@ -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)
@ -171,6 +172,7 @@ type Core struct {
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.
}

View File

@ -11,6 +11,7 @@ import (
"database/sql"
"errors"
"fmt"
"github.com/gogf/gf/text/gstr"
"reflect"
"strings"
@ -455,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 {
@ -602,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 {
@ -829,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

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

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

View File

@ -15,10 +15,10 @@ 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{"deleted_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.
@ -31,49 +31,66 @@ func (m *Model) Unscoped() *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
}
@ -110,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 ""
@ -129,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.

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

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

@ -136,7 +136,7 @@ func Current(err error) error {
if e, ok := err.(ApiLevel); ok {
return e.Current()
}
return nil
return err
}
// Next returns the next level error.

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

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

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

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

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

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

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

@ -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"
@ -63,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.
@ -259,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) {
@ -283,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"
)
@ -42,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)
@ -49,6 +62,7 @@ func doStructs(params interface{}, pointer interface{}, deep bool, mapping ...ma
return gerror.Newf("pointer should be type of pointer, but got: %v", kind)
}
}
// Converting <params> to map slice.
paramsMaps := Maps(params)
// If <params> is an empty slice, no conversion.
if len(paramsMaps) == 0 {

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

View File

@ -16,6 +16,7 @@ type RuleFunc func(rule string, value interface{}, message string, params map[st
var (
// customRuleFuncMap stores the custom rule functions.
// map[Rule]RuleFunc
customRuleFuncMap = make(map[string]RuleFunc)
)

View File

@ -106,3 +106,48 @@ func Test_CustomRule2(t *testing.T) {
t.Assert(err, nil)
})
}
func Test_CustomRule_AllowEmpty(t *testing.T) {
rule := "allow-empty-str"
err := gvalid.RegisterRule(rule, func(rule string, value interface{}, message string, params map[string]interface{}) error {
s := gconv.String(value)
if len(s) == 0 || s == "gf" {
return nil
}
return errors.New(message)
})
gtest.Assert(err, nil)
// Check.
gtest.C(t, func(t *gtest.T) {
errStr := "error"
t.Assert(gvalid.Check("", rule, errStr).String(), "")
t.Assert(gvalid.Check("gf", rule, errStr).String(), "")
t.Assert(gvalid.Check("gf2", rule, errStr).String(), errStr)
})
// Error with struct validation.
gtest.C(t, func(t *gtest.T) {
type T struct {
Value string `v:"uid@allow-empty-str#自定义错误"`
Data string `p:"data"`
}
st := &T{
Value: "",
Data: "123456",
}
err := gvalid.CheckStruct(st, nil)
t.Assert(err.String(), "")
})
// No error with struct validation.
gtest.C(t, func(t *gtest.T) {
type T struct {
Value string `v:"uid@allow-empty-str#自定义错误"`
Data string `p:"data"`
}
st := &T{
Value: "john",
Data: "123456",
}
err := gvalid.CheckStruct(st, nil)
t.Assert(err.String(), "自定义错误")
})
}

View File

@ -1,4 +1,4 @@
package gf
const VERSION = "v1.13.7"
const VERSION = "v1.14.2"
const AUTHORS = "john<john@goframe.org>"