diff --git a/g/container/gvar/gvar.go b/g/container/gvar/gvar.go index dd5064782..7e1f9c492 100644 --- a/g/container/gvar/gvar.go +++ b/g/container/gvar/gvar.go @@ -18,7 +18,7 @@ import ( type Var struct { value interface{} // 变量值 - safe bool // 当为true时,value为 *gtype.Interface 类型 + safe bool // 当为true时, value为 *gtype.Interface 类型 } // 创建一个动态变量,value参数可以为nil diff --git a/g/database/gdb/gdb_unit_init_test.go b/g/database/gdb/gdb_unit_init_test.go index dba6bc9b0..bb919ee39 100644 --- a/g/database/gdb/gdb_unit_init_test.go +++ b/g/database/gdb/gdb_unit_init_test.go @@ -1,3 +1,9 @@ +// Copyright 2019 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 gdb_test import ( diff --git a/g/database/gdb/gdb_unit_method_test.go b/g/database/gdb/gdb_unit_method_test.go index b38569509..8bc04ca87 100644 --- a/g/database/gdb/gdb_unit_method_test.go +++ b/g/database/gdb/gdb_unit_method_test.go @@ -1,4 +1,8 @@ -// 方法操作 +// Copyright 2019 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 gdb_test diff --git a/g/database/gdb/gdb_unit_model_test.go b/g/database/gdb/gdb_unit_model_test.go index 1b4864f3d..504fd8c89 100644 --- a/g/database/gdb/gdb_unit_model_test.go +++ b/g/database/gdb/gdb_unit_model_test.go @@ -1,4 +1,8 @@ -// 链式操作 +// Copyright 2019 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 gdb_test diff --git a/g/database/gdb/gdb_unit_transaction_test.go b/g/database/gdb/gdb_unit_transaction_test.go index ccd2a52df..07ea76c88 100644 --- a/g/database/gdb/gdb_unit_transaction_test.go +++ b/g/database/gdb/gdb_unit_transaction_test.go @@ -1,4 +1,8 @@ -// 事务操作 +// Copyright 2019 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 gdb_test diff --git a/g/database/gredis/gredis.go b/g/database/gredis/gredis.go index c665c9a60..c4e375dbd 100644 --- a/g/database/gredis/gredis.go +++ b/g/database/gredis/gredis.go @@ -11,30 +11,36 @@ package gredis import ( - "time" - "github.com/gogf/gf/third/github.com/gomodule/redigo/redis" - "github.com/gogf/gf/g/container/gmap" "fmt" + "github.com/gogf/gf/g/container/gmap" + "github.com/gogf/gf/third/github.com/gomodule/redigo/redis" + "time" ) const ( - gDEFAULT_POOL_MAX_IDLE = 1 - gDEFAULT_POOL_MAX_ACTIVE = 10 - gDEFAULT_POOL_IDLE_TIMEOUT = 180 * time.Second - gDEFAULT_POOL_MAX_LIFE_TIME = 60 * time.Second + gDEFAULT_POOL_IDLE_TIMEOUT = 60 * time.Second + gDEFAULT_POOL_MAX_LIFE_TIME = 60 * time.Second ) -// Redis客户端 +// Redis客户端(管理连接池) type Redis struct { pool *redis.Pool } +// Redis连接对象(连接池中的单个连接) +type Conn redis.Conn + + // Redis服务端但节点连接配置信息 type Config struct { - Host string // IP/域名 - Port int // 端口 - Db int // db - Pass string // 密码 + Host string // 地址 + Port int // 端口 + Db int // 数据库 + Pass string // 授权密码 + MaxIdle int // 最大允许空闲存在的连接数(默认为0表示不存在闲置连接) + MaxActive int // 最大连接数量限制(默认为0表示不限制) + IdleTimeout time.Duration // 连接最大空闲时间(默认为60秒,不允许设置为0) + MaxConnLifetime time.Duration // 连接最长存活时间(默认为60秒,不允许设置为0) } // Redis链接池统计信息 @@ -45,81 +51,108 @@ type PoolStats struct { // 连接池map var pools = gmap.NewStringInterfaceMap() +// New creates a redis client object with given configuration. +// Redis client maintains a connection pool automatically. +// // 创建redis操作对象. func New(config Config) *Redis { - r := &Redis{} - poolKey := fmt.Sprintf("%s:%d,%d", config.Host, config.Port, config.Db) - if v := pools.Get(poolKey); v == nil { - pool := &redis.Pool { - MaxIdle : gDEFAULT_POOL_MAX_IDLE, - MaxActive : gDEFAULT_POOL_MAX_ACTIVE, - IdleTimeout : gDEFAULT_POOL_IDLE_TIMEOUT, - MaxConnLifetime : gDEFAULT_POOL_MAX_LIFE_TIME, - Dial : func() (redis.Conn, error) { - c, err := redis.Dial("tcp", fmt.Sprintf("%s:%d", config.Host, config.Port)) - if err != nil { - return nil, err - } - if len(config.Pass) > 0 { - if _, err := c.Do("AUTH", config.Pass); err != nil { + if config.IdleTimeout == 0 { + config.IdleTimeout = gDEFAULT_POOL_IDLE_TIMEOUT + } + if config.MaxConnLifetime == 0 { + config.MaxConnLifetime = gDEFAULT_POOL_MAX_LIFE_TIME + } + return &Redis{ + pool : pools.GetOrSetFuncLock(fmt.Sprintf("%v", config), func() interface{} { + return &redis.Pool { + IdleTimeout : config.IdleTimeout, + MaxConnLifetime : config.MaxConnLifetime, + Dial : func() (redis.Conn, error) { + c, err := redis.Dial("tcp", fmt.Sprintf("%s:%d", config.Host, config.Port)) + if err != nil { return nil, err } - } - if _, err := c.Do("SELECT", config.Db); err != nil { - return nil, err - } - return c, nil - }, - // 用来测试连接是否可用 - TestOnBorrow: func(c redis.Conn, t time.Time) error { - _, err := c.Do("PING") - return err - }, - } - pools.Set(poolKey, pool) - r.pool = pool - } else { - r.pool = v.(*redis.Pool) + // 密码设置 + if len(config.Pass) > 0 { + if _, err := c.Do("AUTH", config.Pass); err != nil { + return nil, err + } + } + // 数据库设置 + if _, err := c.Do("SELECT", config.Db); err != nil { + return nil, err + } + return c, nil + }, + // 用来测试连接是否可用 + TestOnBorrow: func(c redis.Conn, t time.Time) error { + _, err := c.Do("PING") + return err + }, + } + }).(*redis.Pool), } - return r } +// Close closes the redis connection pool, +// it will release all connections reserved by this pool. +// // 关闭redis管理对象,将会关闭底层的 func (r *Redis) Close() error { return r.pool.Close() } -// 获得一个原生的redis连接对象,用于自定义连接操作, -// 但是需要注意的是如果不再使用该连接对象时,需要手动Close连接,否则会造成连接数超限。 -func (r *Redis) GetConn() redis.Conn { - return r.pool.Get() +// See GetConn. +func (r *Redis) Conn() Conn { + return r.GetConn() } +// GetConn returns a raw connection object, which expose more methods communication with server. +// You should call Close function manually if you do not use this connection any more. +// +// 获得一个原生的redis连接对象,用于自定义连接操作, +// 但是需要注意的是如果不再使用该连接对象时,需要手动Close连接,否则会造成连接数超限。 +func (r *Redis) GetConn() Conn { + return r.pool.Get().(Conn) +} + +// SetMaxIdle sets the MaxIdle attribute of the connection pool. +// // 设置属性 - MaxIdle func (r *Redis) SetMaxIdle(value int) { r.pool.MaxIdle = value } +// SetMaxIdle sets the MaxActive attribute of the connection pool. +// // 设置属性 - MaxActive func (r *Redis) SetMaxActive(value int) { r.pool.MaxActive = value } +// SetMaxIdle sets the IdleTimeout attribute of the connection pool. +// // 设置属性 - IdleTimeout func (r *Redis) SetIdleTimeout(value time.Duration) { r.pool.IdleTimeout = value } +// SetMaxIdle sets the MaxConnLifetime attribute of the connection pool. +// // 设置属性 - MaxConnLifetime func (r *Redis) SetMaxConnLifetime(value time.Duration) { r.pool.MaxConnLifetime = value } +// Stats returns pool's statistics. +// // 获取当前连接池统计信息 func (r *Redis) Stats() *PoolStats { return &PoolStats{r.pool.Stats()} } +// Do sends a command to the server and returns the received reply. +// // 执行同步命令 - Do func (r *Redis) Do(command string, args ...interface{}) (interface{}, error) { conn := r.pool.Get() @@ -127,6 +160,8 @@ func (r *Redis) Do(command string, args ...interface{}) (interface{}, error) { return conn.Do(command, args...) } +// Send writes the command to the client's output buffer. +// // 执行异步命令 - Send func (r *Redis) Send(command string, args ...interface{}) error { conn := r.pool.Get() @@ -134,3 +169,4 @@ func (r *Redis) Send(command string, args ...interface{}) error { return conn.Send(command, args...) } + diff --git a/g/database/gredis/gredis_unit_test.go b/g/database/gredis/gredis_unit_test.go new file mode 100644 index 000000000..53e9589e2 --- /dev/null +++ b/g/database/gredis/gredis_unit_test.go @@ -0,0 +1,93 @@ +// Copyright 2019 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 gredis_test + +import ( + "github.com/gogf/gf/g/database/gredis" + "github.com/gogf/gf/g/test/gtest" + "testing" + "time" +) + +var ( + config = gredis.Config{ + Host : "127.0.0.1", + Port : 6379, + Db : 1, + } +) + +func Test_NewClose(t *testing.T) { + gtest.Case(t, func() { + redis := gredis.New(config) + gtest.AssertNE(redis, nil) + err := redis.Close() + gtest.Assert(err, nil) + }) +} + +func Test_Do(t *testing.T) { + gtest.Case(t, func() { + redis := gredis.New(config) + defer redis.Close() + _, err := redis.Do("SET", "k", "v") + gtest.Assert(err, nil) + + r, err := redis.Do("GET", "k") + gtest.Assert(err, nil) + gtest.Assert(r, []byte("v")) + + _, err = redis.Do("DEL", "k") + gtest.Assert(err, nil) + r, err = redis.Do("GET", "k") + gtest.Assert(err, nil) + gtest.Assert(r, nil) + }) +} + +func Test_Send(t *testing.T) { + gtest.Case(t, func() { + redis := gredis.New(config) + defer redis.Close() + err := redis.Send("SET", "k", "v") + gtest.Assert(err, nil) + + r, err := redis.Do("GET", "k") + gtest.Assert(err, nil) + gtest.Assert(r, []byte("v")) + }) +} + +func Test_Stats(t *testing.T) { + gtest.Case(t, func() { + redis := gredis.New(config) + defer redis.Close() + redis.SetMaxIdle(2) + redis.SetMaxActive(100) + redis.SetIdleTimeout(500*time.Millisecond) + redis.SetMaxConnLifetime(500*time.Millisecond) + + array := make([]gredis.Conn, 0) + for i := 0; i < 10; i++ { + array = append(array, redis.Conn()) + } + stats := redis.Stats() + gtest.Assert(stats.ActiveCount, 10) + gtest.Assert(stats.IdleCount, 0) + for i := 0; i < 10; i++ { + array[i].Close() + } + stats = redis.Stats() + gtest.Assert(stats.ActiveCount, 2) + gtest.Assert(stats.IdleCount, 2) + //time.Sleep(3000*time.Millisecond) + //stats = redis.Stats() + //fmt.Println(stats) + //gtest.Assert(stats.ActiveCount, 0) + //gtest.Assert(stats.IdleCount, 0) + }) +} diff --git a/g/encoding/gjson/gjson.go b/g/encoding/gjson/gjson.go index 6e375d5d4..01d1b1938 100644 --- a/g/encoding/gjson/gjson.go +++ b/g/encoding/gjson/gjson.go @@ -9,7 +9,6 @@ package gjson import ( "encoding/json" - "errors" "fmt" "github.com/gogf/gf/g/encoding/gtoml" "github.com/gogf/gf/g/encoding/gxml" @@ -20,6 +19,7 @@ import ( "github.com/gogf/gf/g/text/gregex" "github.com/gogf/gf/g/text/gstr" "github.com/gogf/gf/g/util/gconv" + "reflect" "strconv" "strings" "time" @@ -486,16 +486,28 @@ done: // 数据结构转换,map参数必须转换为map[string]interface{},数组参数必须转换为[]interface{} func (j *Json) convertValue(value interface{}) interface{} { switch value.(type) { - case map[string]interface{}: - return value - case []interface{}: - return value - default: - // 这里效率会比较低,当然比直接用反射也不会差到哪儿去 - // 为了操作的灵活性,牺牲了一定的效率 - b, _ := Encode(value) - v, _ := Decode(b) - return v + case map[string]interface{}: + return value + case []interface{}: + return value + default: + rv := reflect.ValueOf(value) + kind := rv.Kind() + if kind == reflect.Ptr { + rv = rv.Elem() + kind = rv.Kind() + } + switch kind { + case reflect.Array: return gconv.Interfaces(value) + case reflect.Slice: return gconv.Interfaces(value) + case reflect.Map: return gconv.Map(value) + case reflect.Struct: return gconv.Map(value) + default: + // 最后使用JSON编解码 + b, _ := Encode(value) + v, _ := Decode(b) + return v + } } } @@ -503,23 +515,23 @@ func (j *Json) convertValue(value interface{}) interface{} { // 返回修改后的父级指针 func (j *Json) setPointerWithValue(pointer *interface{}, key string, value interface{}) *interface{} { switch (*pointer).(type) { - case map[string]interface{}: - (*pointer).(map[string]interface{})[key] = value - return &value - case []interface{}: - n, _ := strconv.Atoi(key) - if len((*pointer).([]interface{})) > n { - (*pointer).([]interface{})[n] = value - return &(*pointer).([]interface{})[n] - } else { - s := make([]interface{}, n + 1) - copy(s, (*pointer).([]interface{})) - s[n] = value - *pointer = s - return &s[n] - } - default: - *pointer = value + case map[string]interface{}: + (*pointer).(map[string]interface{})[key] = value + return &value + case []interface{}: + n, _ := strconv.Atoi(key) + if len((*pointer).([]interface{})) > n { + (*pointer).([]interface{})[n] = value + return &(*pointer).([]interface{})[n] + } else { + s := make([]interface{}, n + 1) + copy(s, (*pointer).([]interface{})) + s[n] = value + *pointer = s + return &s[n] + } + default: + *pointer = value } return pointer } @@ -539,7 +551,7 @@ func (j *Json) Get(pattern...string) interface{} { if j.vc { result = j.getPointerByPattern(queryPattern) } else { - result = j.getPointerByPatternWithoutSplitCharViolenceCheck(queryPattern) + result = j.getPointerByPatternWithoutViolenceCheck(queryPattern) } if result != nil { return *result @@ -552,17 +564,18 @@ func (j *Json) Contains(pattern...string) bool { return j.Get(pattern...) != nil } -// 计算指定pattern的元素长度(pattern对应数据类型为map[string]interface{}/[]interface{}时有效) +// 计算指定pattern的元素长度(pattern对应数据类型为map/slice时有效)。 +// 当pattern对应的数据类型非map/slice时,返回-1。 func (j *Json) Len(pattern string) int { p := j.getPointerByPattern(pattern) if p != nil { switch (*p).(type) { - case map[string]interface{}: - return len((*p).(map[string]interface{})) - case []interface{}: - return len((*p).([]interface{})) - default: - return -1 + case map[string]interface{}: + return len((*p).(map[string]interface{})) + case []interface{}: + return len((*p).([]interface{})) + default: + return -1 } } return -1 @@ -570,14 +583,27 @@ func (j *Json) Len(pattern string) int { // 指定pattern追加元素 func (j *Json) Append(pattern string, value interface{}) error { - length := j.Len(pattern) - if length != -1 { - return j.Set(fmt.Sprintf("%s.%d", pattern, length), value) + p := j.getPointerByPattern(pattern) + if p == nil { + return j.Set(fmt.Sprintf("%s.0", pattern), value) } - return errors.New(fmt.Sprintf("cannot find item for pattern: %s", pattern)) + switch (*p).(type) { + case []interface{}: + return j.Set(fmt.Sprintf("%s.%d", pattern, len((*p).([]interface{}))), value) + } + return fmt.Errorf("invalid variable type of %s", pattern) } -// 根据pattern层级查找**变量指针** +// 根据pattern获取对应元素项的指针 +func (j *Json) getPointerByPattern(pattern string) *interface{} { + if j.vc { + return j.getPointerByPatternWithViolenceCheck(pattern) + } else { + return j.getPointerByPatternWithoutViolenceCheck(pattern) + } +} + +// 根据pattern层级查找**变量指针**, 执行冲突检测。 // 检索方式:例如检索 a.a.a ,值为1 // 1. 检索 a.a.a.a 是否存在对应map的键名; // 2. 检索 a.a.a 是否存在对应map的键名; @@ -589,7 +615,10 @@ func (j *Json) Append(pattern string, value interface{}) error { // 8. 在m2中检索 a.a 否存在对应map的键名; // 9. 在m2中检索 a 否存在对应map的键名,检索到有值,值为1; // 这样检索的复杂度很高,主要是为了避免键名中存在分隔符号(默认为".")的情况,避免歧义。 -func (j *Json) getPointerByPattern(pattern string) *interface{} { +func (j *Json) getPointerByPatternWithViolenceCheck(pattern string) *interface{} { + if !j.vc { + return j.getPointerByPatternWithoutViolenceCheck(pattern) + } index := len(pattern) start := 0 length := 0 @@ -625,7 +654,10 @@ func (j *Json) getPointerByPattern(pattern string) *interface{} { } // 层级检索,内部不执行分隔符冲突检查,检索效率会有所提高,但是冲突需要开发者自己根据自定义的分隔符来进行解决 -func (j *Json) getPointerByPatternWithoutSplitCharViolenceCheck(pattern string) *interface{} { +func (j *Json) getPointerByPatternWithoutViolenceCheck(pattern string) *interface{} { + if j.vc { + return j.getPointerByPatternWithViolenceCheck(pattern) + } pointer := j.p if len(pattern) == 0 { return pointer @@ -645,21 +677,21 @@ func (j *Json) getPointerByPatternWithoutSplitCharViolenceCheck(pattern string) return nil } -// 判断给定的key在当前的pointer下是否有值,并返回对应的pointer +// 判断给定的key在当前的pointer下是否有值,并返回对应的pointer, // 注意这里返回的指针都是临时变量的内存地址 func (j *Json) checkPatternByPointer(key string, pointer *interface{}) *interface{} { switch (*pointer).(type) { - case map[string]interface{}: - if v, ok := (*pointer).(map[string]interface{})[key]; ok { - return &v - } - case []interface{}: - if gstr.IsNumeric(key) { - n, err := strconv.Atoi(key) - if err == nil && len((*pointer).([]interface{})) > n { - return &(*pointer).([]interface{})[n] + case map[string]interface{}: + if v, ok := (*pointer).(map[string]interface{})[key]; ok { + return &v + } + case []interface{}: + if gstr.IsNumeric(key) { + n, err := strconv.Atoi(key) + if err == nil && len((*pointer).([]interface{})) > n { + return &(*pointer).([]interface{})[n] + } } - } } return nil } diff --git a/g/encoding/gjson/gjson_bench_test.go b/g/encoding/gjson/gjson_bench_test.go new file mode 100644 index 000000000..ea49701fe --- /dev/null +++ b/g/encoding/gjson/gjson_bench_test.go @@ -0,0 +1,30 @@ +// Copyright 2017 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 gjson_test + +import ( + "github.com/gogf/gf/g/encoding/gjson" + "testing" +) + +func Benchmark_Set1(b *testing.B) { + for i := 0; i < b.N; i++ { + p := gjson.New(map[string]string{ + "k1" : "v1", + "k2" : "v2", + }) + p.Set("k1.k11", []int{1,2,3}) + } +} + +func Benchmark_Set2(b *testing.B) { + for i := 0; i < b.N; i++ { + p := gjson.New([]string{"a"}) + p.Set("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", []int{1,2,3}) + } +} + diff --git a/g/encoding/gjson/gjson_unit_test.go b/g/encoding/gjson/gjson_unit_test.go new file mode 100644 index 000000000..c1d777d70 --- /dev/null +++ b/g/encoding/gjson/gjson_unit_test.go @@ -0,0 +1,280 @@ +// Copyright 2017 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 gjson_test + +import ( + "bytes" + "github.com/gogf/gf/g" + "github.com/gogf/gf/g/encoding/gjson" + "github.com/gogf/gf/g/test/gtest" + "testing" +) + +func Test_Set1(t *testing.T) { + e := []byte(`{"k1":{"k11":[1,2,3]},"k2":"v2"}`) + p := gjson.New(map[string]string{ + "k1" : "v1", + "k2" : "v2", + }) + p.Set("k1.k11", []int{1,2,3}) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, []byte(`{"k1":{"k11":[1,2,3]},"k2":"v2"}`)) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set2(t *testing.T) { + e := []byte(`[[null,1]]`) + p := gjson.New([]string{"a"}) + p.Set("0.1", 1) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set3(t *testing.T) { + e := []byte(`{"kv":{"k1":"v1"}}`) + p := gjson.New([]string{"a"}) + p.Set("kv", map[string]string { + "k1" : "v1", + }) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set4(t *testing.T) { + e := []byte(`["a",[{"k1":"v1"}]]`) + p := gjson.New([]string{"a"}) + p.Set("1.0", map[string]string{ + "k1" : "v1", + }) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set5(t *testing.T) { + e := []byte(`[[[[[[[[[[[[[[[[[[[[[1,2,3]]]]]]]]]]]]]]]]]]]]]`) + p := gjson.New([]string{"a"}) + p.Set("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", []int{1,2,3}) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set6(t *testing.T) { + e := []byte(`["a",[1,2,3]]`) + p := gjson.New([]string{"a"}) + p.Set("1", []int{1,2,3}) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set7(t *testing.T) { + e := []byte(`{"0":[null,[1,2,3]],"k1":"v1","k2":"v2"}`) + p := gjson.New(map[string]string{ + "k1" : "v1", + "k2" : "v2", + }) + p.Set("0.1", []int{1,2,3}) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set8(t *testing.T) { + e := []byte(`{"0":[[[[[[null,[1,2,3]]]]]]],"k1":"v1","k2":"v2"}`) + p := gjson.New(map[string]string{ + "k1" : "v1", + "k2" : "v2", + }) + p.Set("0.0.0.0.0.0.1", []int{1,2,3}) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set9(t *testing.T) { + e := []byte(`{"k1":[null,[1,2,3]],"k2":"v2"}`) + p := gjson.New(map[string]string{ + "k1" : "v1", + "k2" : "v2", + }) + p.Set("k1.1", []int{1,2,3}) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + + +func Test_Set10(t *testing.T) { + e := []byte(`{"a":{"b":{"c":1}}}`) + p := gjson.New(nil) + p.Set("a.b.c", 1) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + + +func Test_Set11(t *testing.T) { + e := []byte(`{"a":{"b":{}}}`) + p, _ := gjson.LoadContent([]byte(`{"a":{"b":{"c":1}}}`), "json") + p.Remove("a.b.c") + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set12(t *testing.T) { + e := []byte(`[0,1]`) + p := gjson.New(nil) + p.Set("0", 0) + p.Set("1", 1) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set13(t *testing.T) { + e := []byte(`{"array":[0,1]}`) + p := gjson.New(nil) + p.Set("array.0", 0) + p.Set("array.1", 1) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Set14(t *testing.T) { + e := []byte(`{"f":{"a":1}}`) + p := gjson.New(nil) + p.Set("f", "m") + p.Set("f.a", 1) + if c, err := p.ToJson(); err == nil { + + if bytes.Compare(c, e) != 0 { + t.Error("expect:", string(e)) + } + } else { + t.Error(err) + } +} + +func Test_Len(t *testing.T) { + gtest.Case(t, func() { + p := gjson.New(nil) + p.Append("a", 1) + p.Append("a", 2) + gtest.Assert(p.Len("a"), 2) + }) + gtest.Case(t, func() { + p := gjson.New(nil) + p.Append("a.b", 1) + p.Append("a.c", 2) + gtest.Assert(p.Len("a"), 2) + }) + gtest.Case(t, func() { + p := gjson.New(nil) + p.Set("a", 1) + gtest.Assert(p.Len("a"), -1) + }) +} + +func Test_Append(t *testing.T) { + gtest.Case(t, func() { + p := gjson.New(nil) + p.Append("a", 1) + p.Append("a", 2) + gtest.Assert(p.Get("a"), g.Slice{1, 2}) + }) + gtest.Case(t, func() { + p := gjson.New(nil) + p.Append("a.b", 1) + p.Append("a.c", 2) + gtest.Assert(p.Get("a"), g.Map{ + "b" : g.Slice{1}, + "c" : g.Slice{2}, + }) + }) + gtest.Case(t, func() { + p := gjson.New(nil) + p.Set("a", 1) + err := p.Append("a", 2) + gtest.AssertNE(err, nil) + gtest.Assert(p.Get("a"), 1) + }) +} + + + diff --git a/g/encoding/gparser/gparser_test.go b/g/encoding/gparser/gparser_test.go index cd99ca67a..8ce1c8822 100644 --- a/g/encoding/gparser/gparser_test.go +++ b/g/encoding/gparser/gparser_test.go @@ -10,9 +10,8 @@ package gparser_test import ( "bytes" - "testing" "github.com/gogf/gf/g/encoding/gparser" - "fmt" + "testing" ) func Test_Set1(t *testing.T) { @@ -23,7 +22,7 @@ func Test_Set1(t *testing.T) { }) p.Set("k1.k11", []int{1,2,3}) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, []byte(`{"k1":{"k11":[1,2,3]},"k2":"v2"}`)) != 0 { t.Error("expect:", string(e)) } @@ -37,7 +36,7 @@ func Test_Set2(t *testing.T) { p := gparser.New([]string{"a"}) p.Set("0.1", 1) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -53,7 +52,7 @@ func Test_Set3(t *testing.T) { "k1" : "v1", }) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -69,7 +68,7 @@ func Test_Set4(t *testing.T) { "k1" : "v1", }) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -83,7 +82,7 @@ func Test_Set5(t *testing.T) { p := gparser.New([]string{"a"}) p.Set("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0", []int{1,2,3}) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -97,7 +96,7 @@ func Test_Set6(t *testing.T) { p := gparser.New([]string{"a"}) p.Set("1", []int{1,2,3}) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -114,7 +113,7 @@ func Test_Set7(t *testing.T) { }) p.Set("0.1", []int{1,2,3}) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -131,7 +130,7 @@ func Test_Set8(t *testing.T) { }) p.Set("0.0.0.0.0.0.1", []int{1,2,3}) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -148,7 +147,7 @@ func Test_Set9(t *testing.T) { }) p.Set("k1.1", []int{1,2,3}) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -163,7 +162,7 @@ func Test_Set10(t *testing.T) { p := gparser.New(nil) p.Set("a.b.c", 1) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -178,7 +177,7 @@ func Test_Set11(t *testing.T) { p, _ := gparser.LoadContent([]byte(`{"a":{"b":{"c":1}}}`), "json") p.Remove("a.b.c") if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -193,7 +192,7 @@ func Test_Set12(t *testing.T) { p.Set("0", 0) p.Set("1", 1) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } @@ -208,7 +207,7 @@ func Test_Set13(t *testing.T) { p.Set("array.0", 0) p.Set("array.1", 1) if c, err := p.ToJson(); err == nil { - fmt.Println(string(c)) + if bytes.Compare(c, e) != 0 { t.Error("expect:", string(e)) } diff --git a/g/frame/gins/gins.go b/g/frame/gins/gins.go index 5bb8f08a3..c77a2da3f 100644 --- a/g/frame/gins/gins.go +++ b/g/frame/gins/gins.go @@ -21,8 +21,10 @@ import ( "github.com/gogf/gf/g/os/gfsnotify" "github.com/gogf/gf/g/os/glog" "github.com/gogf/gf/g/os/gview" + "github.com/gogf/gf/g/text/gstr" "github.com/gogf/gf/g/util/gconv" "github.com/gogf/gf/g/text/gregex" + "time" ) const ( @@ -163,18 +165,34 @@ func Database(name...string) gdb.DB { if value, ok := nodem["priority"]; ok { node.Priority = gconv.Int(value) } + // Deprecated if value, ok := nodem["linkinfo"]; ok { node.Linkinfo = gconv.String(value) } + if value, ok := nodem["linkInfo"]; ok { + node.Linkinfo = gconv.String(value) + } + // Deprecated if value, ok := nodem["max-idle"]; ok { node.MaxIdleConnCount = gconv.Int(value) } + if value, ok := nodem["maxIdle"]; ok { + node.MaxIdleConnCount = gconv.Int(value) + } + // Deprecated if value, ok := nodem["max-open"]; ok { node.MaxOpenConnCount = gconv.Int(value) } + if value, ok := nodem["maxOpen"]; ok { + node.MaxOpenConnCount = gconv.Int(value) + } + // Deprecated if value, ok := nodem["max-lifetime"]; ok { node.MaxConnLifetime = gconv.Int(value) } + if value, ok := nodem["maxLifetime"]; ok { + node.MaxConnLifetime = gconv.Int(value) + } cg = append(cg, node) } } @@ -208,11 +226,34 @@ func Redis(name...string) *gredis.Redis { key := fmt.Sprintf("%s.%s", gFRAME_CORE_COMPONENT_NAME_REDIS, group) result := instances.GetOrSetFuncLock(key, func() interface{} { if m := config.GetMap("redis"); m != nil { - // host:port[,db[,pass]] + // host:port[,db,pass?maxIdle=x&maxActive=x&idleTimeout=x&maxConnLifetime=x] if v, ok := m[group]; ok { - line := gconv.String(v) - array, _ := gregex.MatchString(`(.+):(\d+),{0,1}(\d*),{0,1}(.*)`, line) - if len(array) > 4 { + line := gconv.String(v) + array, _ := gregex.MatchString(`(.+):(\d+),{0,1}(\d*),{0,1}(.*)\?(.+)`, line) + if len(array) == 6 { + parse, _ := gstr.Parse(array[5]) + config := gredis.Config{ + Host : array[1], + Port : gconv.Int(array[2]), + Db : gconv.Int(array[3]), + Pass : array[4], + } + if v, ok := parse["maxIdle"]; ok { + config.MaxIdle = gconv.Int(v) + } + if v, ok := parse["maxActive"]; ok { + config.MaxActive = gconv.Int(v) + } + if v, ok := parse["idleTimeout"]; ok { + config.IdleTimeout = gconv.TimeDuration(v)*time.Second + } + if v, ok := parse["maxConnLifetime"]; ok { + config.MaxConnLifetime = gconv.TimeDuration(v)*time.Second + } + return gredis.New(config) + } + array, _ = gregex.MatchString(`(.+):(\d+),{0,1}(\d*),{0,1}(.*)`, line) + if len(array) == 5 { return gredis.New(gredis.Config{ Host : array[1], Port : gconv.Int(array[2]), diff --git a/g/frame/gins/gins_redis_test.go b/g/frame/gins/gins_redis_test.go index a6f961b4f..8585af5b0 100644 --- a/g/frame/gins/gins_redis_test.go +++ b/g/frame/gins/gins_redis_test.go @@ -7,7 +7,6 @@ package gins_test import ( - "fmt" "github.com/gogf/gf/g/frame/gins" "github.com/gogf/gf/g/os/gfile" "github.com/gogf/gf/g/test/gtest" @@ -48,6 +47,7 @@ test = "v=3" [redis] default = "127.0.0.1:6379,0" cache = "127.0.0.1:6379,1" + disk = "127.0.0.1:6379,1,?maxIdle=1&maxActive=10&idleTimeout=10&maxConnLifetime=10" ` path := "config.toml" err := gfile.PutContents(path, config) @@ -59,12 +59,14 @@ test = "v=3" time.Sleep(500*time.Millisecond) gtest.Case(t, func() { - fmt.Println("gins Test_Redis", gins.Config().Get("test")) + //fmt.Println("gins Test_Redis", gins.Config().Get("test")) redisDefault := gins.Redis() redisCache := gins.Redis("cache") + redisDisk := gins.Redis("disk") gtest.AssertNE(redisDefault, nil) gtest.AssertNE(redisCache, nil) + gtest.AssertNE(redisDisk, nil) r, err := redisDefault.Do("PING") gtest.Assert(err, nil) @@ -73,6 +75,12 @@ test = "v=3" r, err = redisCache.Do("PING") gtest.Assert(err, nil) gtest.Assert(r, "PONG") + + _, err = redisDisk.Do("SET", "k", "v") + gtest.Assert(err, nil) + r, err = redisDisk.Do("GET", "k") + gtest.Assert(err, nil) + gtest.Assert(r, []byte("v")) }) } diff --git a/g/net/ghttp/ghttp_client_request_api.go b/g/net/ghttp/ghttp_client_api.go similarity index 100% rename from g/net/ghttp/ghttp_client_request_api.go rename to g/net/ghttp/ghttp_client_api.go diff --git a/g/net/ghttp/ghttp_client_config.go b/g/net/ghttp/ghttp_client_config.go new file mode 100644 index 000000000..8d88fba7d --- /dev/null +++ b/g/net/ghttp/ghttp_client_config.go @@ -0,0 +1,96 @@ +// Copyright 2017 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. + +// HTTP客户端请求. + +package ghttp + +import ( + "github.com/gogf/gf/g/text/gregex" + "strings" + "time" +) + +// 是否模拟浏览器模式(自动保存提交COOKIE) +func (c *Client) SetBrowserMode(enabled bool) { + c.browserMode = enabled +} + +// 设置HTTP Header +func (c *Client) SetHeader(key, value string) { + c.header[key] = value +} + +// 通过字符串设置HTTP Header +func (c *Client) SetHeaderRaw(header string) { + for _, line := range strings.Split(strings.TrimSpace(header), "\n") { + array, _ := gregex.MatchString(`^([\w\-]+):\s*(.+)`, line) + if len(array) >= 3 { + c.header[array[1]] = array[2] + } + } +} + +// 设置COOKIE +func (c *Client) SetCookie(key, value string) { + c.cookies[key] = value +} + +// 使用Map设置COOKIE +func (c *Client) SetCookieMap(cookieMap map[string]string) { + for k, v := range cookieMap { + c.cookies[k] = v + } +} + +// 设置请求的URL前缀 +func (c *Client) SetPrefix(prefix string) { + c.prefix = prefix +} + +// 设置请求过期时间 +func (c *Client) SetTimeOut(t time.Duration) { + c.Timeout = t +} + +// 设置HTTP访问账号密码 +func (c *Client) SetBasicAuth(user, pass string) { + c.authUser = user + c.authPass = pass +} + +// 设置失败重试次数及间隔,失败仅针对网络请求失败情况。 +// 重试间隔时间单位为秒。 +func (c *Client) SetRetry(retryCount int, retryInterval int) { + c.retryCount = retryCount + c.retryInterval = retryInterval +} + +// 链式操作, See SetBrowserMode +func (c *Client) BrowserMode(enabled bool) *Client { + c.browserMode = enabled + return c +} + +// 链式操作, See SetTimeOut +func (c *Client) TimeOut(t time.Duration) *Client { + c.Timeout = t + return c +} + +// 链式操作, See SetBasicAuth +func (c *Client) BasicAuth(user, pass string) *Client { + c.authUser = user + c.authPass = pass + return c +} + +// 链式操作, See SetRetry +func (c *Client) Retry(retryCount int, retryInterval int) *Client { + c.retryCount = retryCount + c.retryInterval = retryInterval + return c +} \ No newline at end of file diff --git a/g/net/ghttp/ghttp_client_request_client.go b/g/net/ghttp/ghttp_client_request.go similarity index 81% rename from g/net/ghttp/ghttp_client_request_client.go rename to g/net/ghttp/ghttp_client_request.go index 4b3854b23..730bfdaf5 100644 --- a/g/net/ghttp/ghttp_client_request_client.go +++ b/g/net/ghttp/ghttp_client_request.go @@ -9,90 +9,58 @@ package ghttp import ( - "encoding/json" - "github.com/gogf/gf/g/text/gregex" - "time" "bytes" - "strings" - "net/http" - "mime/multipart" - "os" - "io" - "github.com/gogf/gf/g/os/gfile" + "encoding/json" "errors" "fmt" + "github.com/gogf/gf/g/os/gfile" + "io" + "mime/multipart" + "net/http" + "os" + "strings" + "time" ) // http客户端 type Client struct { - http.Client // 底层http client对象 - header map[string]string // HEADER信息Map - cookies map[string]string // 自定义COOKIE - prefix string // 设置请求的URL前缀 - authUser string // HTTP基本权限设置:名称 - authPass string // HTTP基本权限设置:密码 - browserMode bool // 是否模拟浏览器模式(自动保存提交COOKIE) + http.Client // 底层http client对象 + header map[string]string // HEADER信息Map + cookies map[string]string // 自定义COOKIE + prefix string // 设置请求的URL前缀 + authUser string // HTTP基本权限设置:名称 + authPass string // HTTP基本权限设置:密码 + browserMode bool // 是否模拟浏览器模式(自动保存提交COOKIE) + retryCount int // 失败重试次数(网络失败情况下) + retryInterval int // 失败重试间隔 } // http客户端对象指针 -func NewClient() (*Client) { +func NewClient() *Client { return &Client{ Client : http.Client { Transport: &http.Transport { DisableKeepAlives: true, }, }, - header : make(map[string]string), - cookies : make(map[string]string), + header : make(map[string]string), + cookies : make(map[string]string), } } -// 是否模拟浏览器模式(自动保存提交COOKIE) -func (c *Client) SetBrowserMode(enabled bool) { - c.browserMode = enabled -} - -// 设置HTTP Header -func (c *Client) SetHeader(key, value string) { - c.header[key] = value -} - -// 通过字符串设置HTTP Header -func (c *Client) SetHeaderRaw(header string) { - for _, line := range strings.Split(strings.TrimSpace(header), "\n") { - array, _ := gregex.MatchString(`^([\w\-]+):\s*(.+)`, line) - if len(array) >= 3 { - c.header[array[1]] = array[2] - } +// 克隆当前客户端对象,复制属性。 +func (c *Client) Clone() *Client { + newClient := NewClient() + *newClient = *c + newClient.header = make(map[string]string) + newClient.cookies = make(map[string]string) + for k, v := range c.header { + newClient.header[k] = v } -} - -// 设置COOKIE -func (c *Client) SetCookie(key, value string) { - c.cookies[key] = value -} - -// 使用Map设置COOKIE -func (c *Client) SetCookieMap(cookieMap map[string]string) { - for k, v := range cookieMap { - c.cookies[k] = v + for k, v := range c.cookies { + newClient.cookies[k] = v } -} - -// 设置请求的URL前缀 -func (c *Client) SetPrefix(prefix string) { - c.prefix = prefix -} - -// 设置请求过期时间 -func (c *Client) SetTimeOut(t time.Duration) { - c.Timeout = t -} - -// 设置HTTP访问账号密码 -func (c *Client) SetBasicAuth(user, pass string) { - c.authUser = user - c.authPass = pass + return newClient } // GET请求 @@ -189,9 +157,18 @@ func (c *Client) Post(url string, data...string) (*ClientResponse, error) { req.SetBasicAuth(c.authUser, c.authPass) } // 执行请求 - resp, err := c.Do(req) - if err != nil { - return nil, err + resp := (*http.Response)(nil) + for { + if r, err := c.Do(req); err != nil { + if c.retryCount > 0 { + c.retryCount-- + } else { + return nil, err + } + } else { + resp = r + break + } } r := &ClientResponse{ cookies : make(map[string]string), @@ -311,9 +288,18 @@ func (c *Client) DoRequest(method, url string, data...string) (*ClientResponse, } } // 执行请求 - resp, err := c.Do(req) - if err != nil { - return nil, err + resp := (*http.Response)(nil) + for { + if r, err := c.Do(req); err != nil { + if c.retryCount > 0 { + c.retryCount-- + } else { + return nil, err + } + } else { + resp = r + break + } } r := &ClientResponse{ cookies : make(map[string]string), diff --git a/g/net/ghttp/ghttp_server_router.go b/g/net/ghttp/ghttp_server_router.go index 96d26ecd6..c367e371f 100644 --- a/g/net/ghttp/ghttp_server_router.go +++ b/g/net/ghttp/ghttp_server_router.go @@ -37,7 +37,7 @@ func (s *Server)parsePattern(pattern string) (domain, method, path string, err e } } if path == "" { - err = errors.New("invalid pattern") + err = errors.New("invalid pattern: URI should not be empty") } // 去掉末尾的"/"符号,与路由匹配时处理一致 if path != "/" { @@ -72,11 +72,11 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ... strin } domain, method, uri, err := s.parsePattern(pattern) if err != nil { - glog.Error("invalid pattern:", pattern) + glog.Error("invalid pattern:", pattern, err) return } if len(uri) == 0 || uri[0] != '/' { - glog.Error("invalid pattern:", pattern) + glog.Error("invalid pattern:", pattern, "URI should lead with '/'") return } // 注册地址记录及重复注册判断 @@ -84,7 +84,7 @@ func (s *Server) setHandler(pattern string, handler *handlerItem, hook ... strin caller := s.getHandlerRegisterCallerLine(handler) if len(hook) == 0 { if item, ok := s.routesMap[regkey]; ok { - glog.Errorfln(`duplicated route registry "%s", already registered in %s`, pattern, item[0].file) + glog.Errorfln(`duplicated route registry "%s", already registered at %s`, pattern, item[0].file) return } } diff --git a/g/os/glog/glog_logger_chaining.go b/g/os/glog/glog_logger_chaining.go index a17b82db7..7b6ddf3eb 100644 --- a/g/os/glog/glog_logger_chaining.go +++ b/g/os/glog/glog_logger_chaining.go @@ -12,7 +12,7 @@ import ( ) // To is a chaining function, -// which redirects current logging content output to the sepecified . +// which redirects current logging content output to the specified . // // 链式操作,设置下一次写入日志内容的Writer func (l *Logger) To(writer io.Writer) *Logger { diff --git a/g/text/gstr/gstr_parse.go b/g/text/gstr/gstr_parse.go index e18135739..951055dfe 100644 --- a/g/text/gstr/gstr_parse.go +++ b/g/text/gstr/gstr_parse.go @@ -12,92 +12,21 @@ import ( "strings" ) -// Parses the string into variables. -// f1=m&f2=n -> map[f1:m f2:n] -// f[a]=m&f[b]=n -> map[f:map[a:m b:n]] +// Parses the string into map[string]interface{}. +// +// f1=m&f2=n -> map[f1:m f2:n] +// f[a]=m&f[b]=n -> map[f:map[a:m b:n]] // f[a][a]=m&f[a][b]=n -> map[f:map[a:map[a:m b:n]]] -// f[]=m&f[]=n -> map[f:[m n]] -// f[a][]=m&f[a][]=n -> map[f:map[a:[m n]]] -// f[][]=m&f[][]=n -> map[f:[map[]]] // Currently does not support nested slice. -// f=m&f[a]=n -> error // This is not the same as PHP. -// a .[[b=c -> map[a___[b:c] +// f[]=m&f[]=n -> map[f:[m n]] +// f[a][]=m&f[a][]=n -> map[f:map[a:[m n]]] +// f[][]=m&f[][]=n -> map[f:[map[]]] // Currently does not support nested slice. +// f=m&f[a]=n -> error +// a .[[b=c -> map[a___[b:c] // // 将字符串解析成Map。 -func Parse(encodedString string, result map[string]interface{}) error { - // build nested map. - var build func(map[string]interface{}, []string, interface{}) error - build = func(result map[string]interface{}, keys []string, value interface{}) error { - length := len(keys) - // trim '," - key := strings.Trim(keys[0], "'\"") - if length == 1 { - result[key] = value - return nil - } - - // The end is slice. like f[], f[a][] - if keys[1] == "" && length == 2 { - // todo nested slice - if key == "" { - return nil - } - val, ok := result[key] - if !ok { - result[key] = []interface{}{value} - return nil - } - children, ok := val.([]interface{}) - if !ok { - return fmt.Errorf("expected type '[]interface{}' for key '%s', but got '%T'", key, val) - } - result[key] = append(children, value) - return nil - } - - // The end is slice + map. like f[][a] - if keys[1] == "" && length > 2 && keys[2] != "" { - val, ok := result[key] - if !ok { - result[key] = []interface{}{} - val = result[key] - } - children, ok := val.([]interface{}) - if !ok { - return fmt.Errorf("expected type '[]interface{}' for key '%s', but got '%T'", key, val) - } - if l := len(children); l > 0 { - if child, ok := children[l-1].(map[string]interface{}); ok { - if _, ok := child[keys[2]]; !ok { - build(child, keys[2:], value) - return nil - } - } - } - child := map[string]interface{}{} - build(child, keys[2:], value) - result[key] = append(children, child) - - return nil - } - - // map. like f[a], f[a][b] - val, ok := result[key] - if !ok { - result[key] = map[string]interface{}{} - val = result[key] - } - children, ok := val.(map[string]interface{}) - if !ok { - return fmt.Errorf("expected type 'map[string]interface{}' for key '%s', but got '%T'", key, val) - } - if err := build(children, keys[1:], value); err != nil { - return err - } - return nil - } - - // split encodedString. - parts := strings.Split(encodedString, "&") +func Parse(s string) (result map[string]interface{}, err error) { + result = make(map[string]interface{}) + parts := strings.Split(s, "&") for _, part := range parts { pos := strings.Index(part, "=") if pos <= 0 { @@ -105,7 +34,7 @@ func Parse(encodedString string, result map[string]interface{}) error { } key, err := url.QueryUnescape(part[:pos]) if err != nil { - return err + return nil, err } for key[0] == ' ' { key = key[1:] @@ -115,9 +44,8 @@ func Parse(encodedString string, result map[string]interface{}) error { } value, err := url.QueryUnescape(part[pos+1:]) if err != nil { - return err + return nil, err } - // split into multiple keys var keys []string left := 0 @@ -127,11 +55,11 @@ func Parse(encodedString string, result map[string]interface{}) error { } else if k == ']' { if left > 0 { if len(keys) == 0 { - keys = append(keys, key[:left]) + keys = append(keys, key[ : left]) } - keys = append(keys, key[left+1:i]) + keys = append(keys, key[left + 1 : i]) left = 0 - if i+1 < len(key) && key[i+1] != '[' { + if i+1 < len(key) && key[i + 1] != '[' { break } } @@ -149,7 +77,7 @@ func Parse(encodedString string, result map[string]interface{}) error { first += string(chr) } if chr == '[' { - first += keys[0][i+1:] + first += keys[0][i + 1: ] break } } @@ -157,9 +85,78 @@ func Parse(encodedString string, result map[string]interface{}) error { // build nested map if err := build(result, keys, value); err != nil { - return err + return nil, err } } + return result, nil +} +// build nested map. +func build(result map[string]interface{}, keys []string, value interface{}) error { + length := len(keys) + // trim '," + key := strings.Trim(keys[0], "'\"") + if length == 1 { + result[key] = value + return nil + } + + // The end is slice. like f[], f[a][] + if keys[1] == "" && length == 2 { + // todo nested slice + if key == "" { + return nil + } + val, ok := result[key] + if !ok { + result[key] = []interface{}{value} + return nil + } + children, ok := val.([]interface{}) + if !ok { + return fmt.Errorf("expected type '[]interface{}' for key '%s', but got '%T'", key, val) + } + result[key] = append(children, value) + return nil + } + + // The end is slice + map. like f[][a] + if keys[1] == "" && length > 2 && keys[2] != "" { + val, ok := result[key] + if !ok { + result[key] = []interface{}{} + val = result[key] + } + children, ok := val.([]interface{}) + if !ok { + return fmt.Errorf("expected type '[]interface{}' for key '%s', but got '%T'", key, val) + } + if l := len(children); l > 0 { + if child, ok := children[l-1].(map[string]interface{}); ok { + if _, ok := child[keys[2]]; !ok { + build(child, keys[2:], value) + return nil + } + } + } + child := map[string]interface{}{} + build(child, keys[2:], value) + result[key] = append(children, child) + return nil + } + + // map. like f[a], f[a][b] + val, ok := result[key] + if !ok { + result[key] = map[string]interface{}{} + val = result[key] + } + children, ok := val.(map[string]interface{}) + if !ok { + return fmt.Errorf("expected type 'map[string]interface{}' for key '%s', but got '%T'", key, val) + } + if err := build(children, keys[1:], value); err != nil { + return err + } return nil } \ No newline at end of file diff --git a/g/text/gstr/gstr_z_bench_test.go b/g/text/gstr/gstr_z_bench_test.go index 49d037d79..404ce372f 100644 --- a/g/text/gstr/gstr_z_bench_test.go +++ b/g/text/gstr/gstr_z_bench_test.go @@ -9,6 +9,7 @@ package gstr_test import ( + "github.com/gogf/gf/g/text/gstr" "testing" ) @@ -31,4 +32,22 @@ func Benchmark_BytesToString(b *testing.B) { } } +} + +func Benchmark_Parse1(b *testing.B) { + for i := 0; i < b.N; i++ { + gstr.Parse("a=1&b=2") + } +} + +func Benchmark_Parse2(b *testing.B) { + for i := 0; i < b.N; i++ { + gstr.Parse("m[]=1&m[]=2") + } +} + +func Benchmark_Parse3(b *testing.B) { + for i := 0; i < b.N; i++ { + gstr.Parse("m[a1][b1][c1][d1]=1&m[a2][b2]=2&m[a3][b3][c3]=3") + } } \ No newline at end of file diff --git a/g/text/gstr/gstr_z_unit_parse_test.go b/g/text/gstr/gstr_z_unit_parse_test.go new file mode 100644 index 000000000..63f56f057 --- /dev/null +++ b/g/text/gstr/gstr_z_unit_parse_test.go @@ -0,0 +1,75 @@ +// Copyright 2019 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. + +// go test *.go -bench=".*" + +package gstr_test + +import ( + "github.com/gogf/gf/g" + "github.com/gogf/gf/g/test/gtest" + "github.com/gogf/gf/g/text/gstr" + "testing" +) + +func Test_Parse(t *testing.T) { + gtest.Case(t, func() { + // slice + m, err := gstr.Parse("a[]=1&a[]=2") + gtest.Assert(err, nil) + gtest.Assert(m, g.Map{ + "a" : g.Slice{"1", "2"}, + }) + // map + m, err = gstr.Parse("a=1&b=2&c=3") + gtest.Assert(err, nil) + gtest.Assert(m, g.Map{ + "a" : "1", + "b" : "2", + "c" : "3", + }) + // map + m, err = gstr.Parse("m[a]=1&m[b]=2&m[c]=3") + gtest.Assert(err, nil) + gtest.Assert(m, g.Map{ + "m" : g.Map{ + "a" : "1", + "b" : "2", + "c" : "3", + }, + }) + // map - slice + m, err = gstr.Parse("m[a][]=1&m[a][]=2") + gtest.Assert(err, nil) + gtest.Assert(m, g.Map{ + "m" : g.Map{ + "a" : g.Slice{"1", "2"}, + }, + }) + // map - complicated + m, err = gstr.Parse("m[a1][b1][c1][d1]=1&m[a2][b2]=2&m[a3][b3][c3]=3") + gtest.Assert(err, nil) + gtest.Assert(m, g.Map{ + "m" : g.Map{ + "a1" : g.Map{ + "b1" : g.Map{ + "c1" : g.Map{ + "d1" : "1", + }, + }, + }, + "a2" : g.Map{ + "b2" : "2", + }, + "a3" : g.Map{ + "b3" : g.Map{ + "c3" : "3", + }, + }, + }, + }) + }) +} diff --git a/geg/other/test.go b/geg/other/test.go index 4d249296d..faa56f5db 100644 --- a/geg/other/test.go +++ b/geg/other/test.go @@ -1,12 +1,13 @@ package main import ( - "fmt" - "github.com/gogf/gf/g/os/gtime" - "github.com/gogf/gf/g/util/gconv" + "github.com/gogf/gf/g" + "github.com/gogf/gf/g/text/gregex" ) func main() { - //t := gconv.GTime("2010-10-10 00:00:01") - fmt.Println(gconv.String(gtime.Millisecond())) + s := "127.0.0.1:6379,1,nhytaf176tg?maxIdle=1&maxActive=0&idleTimeout=60&maxConnLifetime=60" + array, err := gregex.MatchString(`(.+):(\d+),{0,1}(\d*),{0,1}(.*)\?(.+)`, s) + g.Dump(err) + g.Dump(array) } \ No newline at end of file