From e0db3c87cfdde93f44564ca9d28ed649950dd11f Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 14:31:16 +0800 Subject: [PATCH 1/4] improve RuleFunc for package gvalid --- util/gvalid/gvalid_custom_rule.go | 46 ++++++++++++++----- util/gvalid/gvalid_validator_check_value.go | 8 +++- util/gvalid/gvalid_z_example_test.go | 14 +++--- util/gvalid/gvalid_z_unit_custom_rule_test.go | 44 +++++++++--------- 4 files changed, 69 insertions(+), 43 deletions(-) diff --git a/util/gvalid/gvalid_custom_rule.go b/util/gvalid/gvalid_custom_rule.go index 418e9f95f..63b1020b4 100644 --- a/util/gvalid/gvalid_custom_rule.go +++ b/util/gvalid/gvalid_custom_rule.go @@ -6,16 +6,29 @@ package gvalid -import "context" +import ( + "context" + "github.com/gogf/gf/v2/container/gvar" +) // RuleFunc is the custom function for data validation. -// -// The parameter `rule` specifies the validation rule string, like "required", "between:1,100", etc. -// The parameter `value` specifies the value for this rule to validate. -// The parameter `message` specifies the custom error message or configured i18n message for this rule. -// The parameter `data` specifies the `data` which is passed to the Validator. It might be a type of map/struct or a nil value. -// You can ignore the parameter `data` if you do not really need it in your custom validation rule. -type RuleFunc func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error +type RuleFunc func(ctx context.Context, in RuleFuncInput) error + +// RuleFuncInput holds the input parameters that passed to custom rule function RuleFunc. +type RuleFuncInput struct { + // Rule specifies the validation rule string, like "required", "between:1,100", etc. + Rule string + + // Message specifies the custom error message or configured i18n message for this rule. + Message string + + // Value specifies the value for this rule to validate. + Value *gvar.Var + + // Data specifies the `data` which is passed to the Validator. It might be a type of map/struct or a nil value. + // You can ignore the parameter `Data` if you do not really need it in your custom validation rule. + Data *gvar.Var +} var ( // customRuleFuncMap stores the custom rule functions. @@ -24,13 +37,22 @@ var ( ) // RegisterRule registers custom validation rule and function for package. -// It returns error if there's already the same rule registered previously. func RegisterRule(rule string, f RuleFunc) error { customRuleFuncMap[rule] = f return nil } -// DeleteRule deletes custom defined validation rule and its function from global package. -func DeleteRule(rule string) { - delete(customRuleFuncMap, rule) +// RegisterRuleByMap registers custom validation rules using map for package. +func RegisterRuleByMap(m map[string]RuleFunc) error { + for k, v := range m { + customRuleFuncMap[k] = v + } + return nil +} + +// DeleteRule deletes custom defined validation one or more rules and associated functions from global package. +func DeleteRule(rules ...string) { + for _, rule := range rules { + delete(customRuleFuncMap, rule) + } } diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 36176f361..182a10d10 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -8,6 +8,7 @@ package gvalid import ( "context" + "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gstr" @@ -133,7 +134,12 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if customRuleFunc != nil { // It checks custom validation rules with most priority. message := v.getErrorMessageByRule(ctx, ruleKey, customMsgMap) - if err := customRuleFunc(ctx, ruleItems[index], input.Value, message, input.DataRaw); err != nil { + if err := customRuleFunc(ctx, RuleFuncInput{ + Rule: ruleItems[index], + Message: message, + Value: gvar.New(input.Value), + Data: gvar.New(input.DataRaw), + }); err != nil { match = false errorMsgArray[ruleKey] = err.Error() } else { diff --git a/util/gvalid/gvalid_z_example_test.go b/util/gvalid/gvalid_z_example_test.go index f163b1b56..c9c8c0128 100644 --- a/util/gvalid/gvalid_z_example_test.go +++ b/util/gvalid/gvalid_z_example_test.go @@ -127,17 +127,17 @@ func ExampleRegisterRule() { } rule := "unique-name" - gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { var ( - id = data.(*User).Id - name = gconv.String(value) + id = in.Data.Val().(*User).Id + name = gconv.String(in.Value) ) n, err := g.Model("user").Where("id != ? and name = ?", id, name).Count() if err != nil { return err } if n > 0 { - return errors.New(message) + return errors.New(in.Message) } return nil }) @@ -149,8 +149,8 @@ func ExampleRegisterRule() { func ExampleRegisterRule_OverwriteRequired() { rule := "required" - gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - reflectValue := reflect.ValueOf(value) + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + reflectValue := reflect.ValueOf(in.Value.Val()) if reflectValue.Kind() == reflect.Ptr { reflectValue = reflectValue.Elem() } @@ -171,7 +171,7 @@ func ExampleRegisterRule_OverwriteRequired() { isEmpty = reflectValue.Len() == 0 } if isEmpty { - return errors.New(message) + return errors.New(in.Message) } return nil }) diff --git a/util/gvalid/gvalid_z_unit_custom_rule_test.go b/util/gvalid/gvalid_z_unit_custom_rule_test.go index 75285813c..cd27fb46c 100644 --- a/util/gvalid/gvalid_z_unit_custom_rule_test.go +++ b/util/gvalid/gvalid_z_unit_custom_rule_test.go @@ -12,8 +12,6 @@ import ( "testing" "github.com/gogf/gf/v2/frame/g" - "github.com/gogf/gf/v2/util/gconv" - "github.com/gogf/gf/v2/test/gtest" "github.com/gogf/gf/v2/util/gvalid" ) @@ -22,14 +20,14 @@ func Test_CustomRule1(t *testing.T) { rule := "custom" err := gvalid.RegisterRule( rule, - func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - pass := gconv.String(value) + func(ctx context.Context, in gvalid.RuleFuncInput) error { + pass := in.Value.String() if len(pass) != 6 { - return errors.New(message) + return errors.New(in.Message) } - m := gconv.Map(data) + m := in.Data.Map() if m["data"] != pass { - return errors.New(message) + return errors.New(in.Message) } return nil }, @@ -71,10 +69,10 @@ func Test_CustomRule1(t *testing.T) { func Test_CustomRule2(t *testing.T) { rule := "required-map" - err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - m := gconv.Map(value) + err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + m := in.Value.Map() if len(m) == 0 { - return errors.New(message) + return errors.New(in.Message) } return nil }) @@ -115,12 +113,12 @@ func Test_CustomRule2(t *testing.T) { func Test_CustomRule_AllowEmpty(t *testing.T) { rule := "allow-empty-str" - err := gvalid.RegisterRule(rule, func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - s := gconv.String(value) + err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + s := in.Value.String() if len(s) == 0 || s == "gf" { return nil } - return errors.New(message) + return errors.New(in.Message) }) gtest.Assert(err, nil) // Check. @@ -160,13 +158,13 @@ func Test_CustomRule_AllowEmpty(t *testing.T) { func TestValidator_RuleFunc(t *testing.T) { ruleName := "custom_1" - ruleFunc := func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - pass := gconv.String(value) + ruleFunc := func(ctx context.Context, in gvalid.RuleFuncInput) error { + pass := in.Value.String() if len(pass) != 6 { - return errors.New(message) + return errors.New(in.Message) } - if m := gconv.Map(data); m["data"] != pass { - return errors.New(message) + if m := in.Data.Map(); m["data"] != pass { + return errors.New(in.Message) } return nil } @@ -214,13 +212,13 @@ func TestValidator_RuleFunc(t *testing.T) { func TestValidator_RuleFuncMap(t *testing.T) { ruleName := "custom_1" - ruleFunc := func(ctx context.Context, rule string, value interface{}, message string, data interface{}) error { - pass := gconv.String(value) + ruleFunc := func(ctx context.Context, in gvalid.RuleFuncInput) error { + pass := in.Value.String() if len(pass) != 6 { - return errors.New(message) + return errors.New(in.Message) } - if m := gconv.Map(data); m["data"] != pass { - return errors.New(message) + if m := in.Data.Map(); m["data"] != pass { + return errors.New(in.Message) } return nil } From 02e1d01f29faca1787a22235a68ecacab45a78b6 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 15:22:17 +0800 Subject: [PATCH 2/4] improve RuleFunc for package gvalid --- util/gvalid/gvalid_custom_rule.go | 6 ++---- util/gvalid/gvalid_z_unit_custom_rule_test.go | 10 ++++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/util/gvalid/gvalid_custom_rule.go b/util/gvalid/gvalid_custom_rule.go index 63b1020b4..11fe6da87 100644 --- a/util/gvalid/gvalid_custom_rule.go +++ b/util/gvalid/gvalid_custom_rule.go @@ -37,17 +37,15 @@ var ( ) // RegisterRule registers custom validation rule and function for package. -func RegisterRule(rule string, f RuleFunc) error { +func RegisterRule(rule string, f RuleFunc) { customRuleFuncMap[rule] = f - return nil } // RegisterRuleByMap registers custom validation rules using map for package. -func RegisterRuleByMap(m map[string]RuleFunc) error { +func RegisterRuleByMap(m map[string]RuleFunc) { for k, v := range m { customRuleFuncMap[k] = v } - return nil } // DeleteRule deletes custom defined validation one or more rules and associated functions from global package. diff --git a/util/gvalid/gvalid_z_unit_custom_rule_test.go b/util/gvalid/gvalid_z_unit_custom_rule_test.go index cd27fb46c..21ad7dcce 100644 --- a/util/gvalid/gvalid_z_unit_custom_rule_test.go +++ b/util/gvalid/gvalid_z_unit_custom_rule_test.go @@ -18,7 +18,7 @@ import ( func Test_CustomRule1(t *testing.T) { rule := "custom" - err := gvalid.RegisterRule( + gvalid.RegisterRule( rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { pass := in.Value.String() @@ -32,7 +32,7 @@ func Test_CustomRule1(t *testing.T) { return nil }, ) - gtest.Assert(err, nil) + gtest.C(t, func(t *gtest.T) { err := gvalid.CheckValue(context.TODO(), "123456", rule, "custom message") t.Assert(err.String(), "custom message") @@ -69,14 +69,13 @@ func Test_CustomRule1(t *testing.T) { func Test_CustomRule2(t *testing.T) { rule := "required-map" - err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { m := in.Value.Map() if len(m) == 0 { return errors.New(in.Message) } return nil }) - gtest.Assert(err, nil) // Check. gtest.C(t, func(t *gtest.T) { errStr := "data map should not be empty" @@ -113,14 +112,13 @@ func Test_CustomRule2(t *testing.T) { func Test_CustomRule_AllowEmpty(t *testing.T) { rule := "allow-empty-str" - err := gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { + gvalid.RegisterRule(rule, func(ctx context.Context, in gvalid.RuleFuncInput) error { s := in.Value.String() if len(s) == 0 || s == "gf" { return nil } return errors.New(in.Message) }) - gtest.Assert(err, nil) // Check. gtest.C(t, func(t *gtest.T) { errStr := "error" From e0a0fcbde2da99b778972370d6ae6ab440e176d9 Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 16:06:31 +0800 Subject: [PATCH 3/4] improve cache feature for package gdb --- database/gdb/gdb_model.go | 7 +- database/gdb/gdb_model_cache.go | 36 ++++---- database/gdb/gdb_model_select.go | 8 +- database/gdb/gdb_z_mysql_model_basic_test.go | 90 ++++++++++++++++---- 4 files changed, 102 insertions(+), 39 deletions(-) diff --git a/database/gdb/gdb_model.go b/database/gdb/gdb_model.go index c8a51945d..579130e12 100644 --- a/database/gdb/gdb_model.go +++ b/database/gdb/gdb_model.go @@ -9,10 +9,8 @@ package gdb import ( "context" "fmt" - "github.com/gogf/gf/v2/util/gconv" - "time" - "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/text/gstr" ) @@ -45,8 +43,7 @@ type Model struct { distinct string // Force the query to only return distinct results. lockInfo string // Lock for update or in shared lock. cacheEnabled bool // Enable sql result cache feature. - cacheDuration time.Duration // Cache TTL duration (< 1 for removing cache, >= 0 for saving cache). - cacheName string // Cache name for custom operation. + cacheOption CacheOption // Cache option for query statement. unscoped bool // Disables soft deleting features when select/delete operations. safe bool // If true, it clones and returns a new model object whenever operation done; or else it changes the attribute of current model. onDuplicate interface{} // onDuplicate is used for ON "DUPLICATE KEY UPDATE" statement. diff --git a/database/gdb/gdb_model_cache.go b/database/gdb/gdb_model_cache.go index d505abc3b..d5637518e 100644 --- a/database/gdb/gdb_model_cache.go +++ b/database/gdb/gdb_model_cache.go @@ -11,26 +11,32 @@ import ( "time" ) +type CacheOption struct { + // Duration is the TTL for the cache. + // If the parameter `Duration` < 0, which means it clear the cache with given `Name`. + // If the parameter `Duration` = 0, which means it never expires. + // If the parameter `Duration` > 0, which means it expires after `Duration`. + Duration time.Duration + + // Name is an optional unique name for the cache. + // The Name is used to bind a name to the cache, which means you can later control the cache + // like changing the `duration` or clearing the cache with specified Name. + Name string + + // Force caches the query result whatever the result is nil or not. + // It is used to avoid Cache Penetration. + Force bool +} + // Cache sets the cache feature for the model. It caches the result of the sql, which means // if there's another same sql request, it just reads and returns the result from cache, it // but not committed and executed into the database. // -// If the parameter `duration` < 0, which means it clear the cache with given `name`. -// If the parameter `duration` = 0, which means it never expires. -// If the parameter `duration` > 0, which means it expires after `duration`. -// -// The optional parameter `name` is used to bind a name to the cache, which means you can -// later control the cache like changing the `duration` or clearing the cache with specified -// `name`. -// // Note that, the cache feature is disabled if the model is performing select statement // on a transaction. -func (m *Model) Cache(duration time.Duration, name ...string) *Model { +func (m *Model) Cache(option CacheOption) *Model { model := m.getModel() - model.cacheDuration = duration - if len(name) > 0 { - model.cacheName = name[0] - } + model.cacheOption = option model.cacheEnabled = true return model } @@ -38,9 +44,9 @@ func (m *Model) Cache(duration time.Duration, name ...string) *Model { // checkAndRemoveCache checks and removes the cache in insert/update/delete statement if // cache feature is enabled. func (m *Model) checkAndRemoveCache() { - if m.cacheEnabled && m.cacheDuration < 0 && len(m.cacheName) > 0 { + if m.cacheEnabled && m.cacheOption.Duration < 0 && len(m.cacheOption.Name) > 0 { var ctx = m.GetCtx() - _, err := m.db.GetCache().Remove(ctx, m.cacheName) + _, err := m.db.GetCache().Remove(ctx, m.cacheOption.Name) if err != nil { intlog.Error(ctx, err) } diff --git a/database/gdb/gdb_model_select.go b/database/gdb/gdb_model_select.go index 70f449d44..098f514fb 100644 --- a/database/gdb/gdb_model_select.go +++ b/database/gdb/gdb_model_select.go @@ -437,7 +437,7 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e ) // Retrieve from cache. if m.cacheEnabled && m.tx == nil { - cacheKey = m.cacheName + cacheKey = m.cacheOption.Name if len(cacheKey) == 0 { cacheKey = sql + ", @PARAMS:" + gconv.String(args) } @@ -461,16 +461,16 @@ func (m *Model) doGetAllBySql(sql string, args ...interface{}) (result Result, e ) // Cache the result. if cacheKey != "" && err == nil { - if m.cacheDuration < 0 { + if m.cacheOption.Duration < 0 { if _, err := cacheObj.Remove(ctx, cacheKey); err != nil { intlog.Error(m.GetCtx(), err) } } else { // In case of Cache Penetration. - if result == nil { + if result.IsEmpty() && m.cacheOption.Force { result = Result{} } - if err := cacheObj.Set(ctx, cacheKey, result, m.cacheDuration); err != nil { + if err := cacheObj.Set(ctx, cacheKey, result, m.cacheOption.Duration); err != nil { intlog.Error(m.GetCtx(), err) } } diff --git a/database/gdb/gdb_z_mysql_model_basic_test.go b/database/gdb/gdb_z_mysql_model_basic_test.go index c1e7496ef..2d055a3fc 100644 --- a/database/gdb/gdb_z_mysql_model_basic_test.go +++ b/database/gdb/gdb_z_mysql_model_basic_test.go @@ -2479,7 +2479,11 @@ func Test_Model_Cache(t *testing.T) { defer dropTable(table) gtest.C(t, func(t *gtest.T) { - one, err := db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_1") @@ -2489,63 +2493,107 @@ func Test_Model_Cache(t *testing.T) { t.AssertNil(err) t.Assert(n, 1) - one, err = db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_1") time.Sleep(time.Second * 2) - one, err = db.Model(table).Cache(time.Second, "test1").WherePri(1).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test1", + Force: false, + }).WherePri(1).One() t.AssertNil(err) t.Assert(one["passport"], "user_100") }) gtest.C(t, func(t *gtest.T) { - one, err := db.Model(table).Cache(time.Second, "test2").WherePri(2).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test2", + Force: false, + }).WherePri(2).One() t.AssertNil(err) t.Assert(one["passport"], "user_2") - r, err := db.Model(table).Data("passport", "user_200").Cache(-1, "test2").WherePri(2).Update() + r, err := db.Model(table).Data("passport", "user_200").Cache(gdb.CacheOption{ + Duration: -1, + Name: "test2", + Force: false, + }).WherePri(2).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) t.Assert(n, 1) - one, err = db.Model(table).Cache(time.Second, "test2").WherePri(2).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test2", + Force: false, + }).WherePri(2).One() t.AssertNil(err) t.Assert(one["passport"], "user_200") }) // transaction. gtest.C(t, func(t *gtest.T) { // make cache for id 3 - one, err := db.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_3") - r, err := db.Model(table).Data("passport", "user_300").Cache(time.Second, "test3").WherePri(3).Update() + r, err := db.Model(table).Data("passport", "user_300").Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) t.Assert(n, 1) err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { - one, err := tx.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err := tx.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_300") return nil }) t.AssertNil(err) - one, err = db.Model(table).Cache(time.Second, "test3").WherePri(3).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(3).One() t.AssertNil(err) t.Assert(one["passport"], "user_3") }) gtest.C(t, func(t *gtest.T) { // make cache for id 4 - one, err := db.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err := db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_4") - r, err := db.Model(table).Data("passport", "user_400").Cache(time.Second, "test3").WherePri(4).Update() + r, err := db.Model(table).Data("passport", "user_400").Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test3", + Force: false, + }).WherePri(4).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) @@ -2553,12 +2601,20 @@ func Test_Model_Cache(t *testing.T) { err = db.Transaction(context.TODO(), func(ctx context.Context, tx *gdb.TX) error { // Cache feature disabled. - one, err := tx.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err := tx.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_400") // Update the cache. r, err := tx.Model(table).Data("passport", "user_4000"). - Cache(-1, "test4").WherePri(4).Update() + Cache(gdb.CacheOption{ + Duration: -1, + Name: "test4", + Force: false, + }).WherePri(4).Update() t.AssertNil(err) n, err := r.RowsAffected() t.AssertNil(err) @@ -2567,7 +2623,11 @@ func Test_Model_Cache(t *testing.T) { }) t.AssertNil(err) // Read from db. - one, err = db.Model(table).Cache(time.Second, "test4").WherePri(4).One() + one, err = db.Model(table).Cache(gdb.CacheOption{ + Duration: time.Second, + Name: "test4", + Force: false, + }).WherePri(4).One() t.AssertNil(err) t.Assert(one["passport"], "user_4000") }) From 95a4690e692d24cb8b0421f38964a22afca8b99d Mon Sep 17 00:00:00 2001 From: John Guo Date: Tue, 9 Nov 2021 17:50:14 +0800 Subject: [PATCH 4/4] improve error feature for package gvalid --- internal/utils/utils_is.go | 2 +- util/gutil/gutil_dump.go | 8 +- util/gvalid/gvalid_error.go | 112 ++++++++++-------- util/gvalid/gvalid_validator_check_map.go | 11 +- util/gvalid/gvalid_validator_check_struct.go | 12 +- util/gvalid/gvalid_validator_check_value.go | 54 +++------ util/gvalid/gvalid_z_example_test.go | 4 +- util/gvalid/gvalid_z_unit_basic_all_test.go | 2 +- util/gvalid/gvalid_z_unit_customerror_test.go | 11 +- 9 files changed, 105 insertions(+), 111 deletions(-) diff --git a/internal/utils/utils_is.go b/internal/utils/utils_is.go index b525df226..ceb4f74e8 100644 --- a/internal/utils/utils_is.go +++ b/internal/utils/utils_is.go @@ -13,7 +13,7 @@ import ( // IsNil checks whether `value` is nil. func IsNil(value interface{}) bool { - return value == nil + return empty.IsNil(value) } // IsEmpty checks whether `value` is empty. diff --git a/util/gutil/gutil_dump.go b/util/gutil/gutil_dump.go index 0e331426f..fb41864ae 100644 --- a/util/gutil/gutil_dump.go +++ b/util/gutil/gutil_dump.go @@ -196,8 +196,12 @@ func doExport(value interface{}, indent string, buffer *bytes.Buffer, option doE if structContentStr == "" { structContentStr = "{}" } else { - structContentStr = fmt.Sprintf(`"%s"`, gstr.AddSlashes(structContentStr)) - attributeCountStr = fmt.Sprintf(`%d`, len(structContentStr)-2) + if strings.HasPrefix(structContentStr, `"`) && strings.HasSuffix(structContentStr, `"`) { + attributeCountStr = fmt.Sprintf(`%d`, len(structContentStr)) + } else { + structContentStr = fmt.Sprintf(`"%s"`, gstr.AddSlashes(structContentStr)) + attributeCountStr = fmt.Sprintf(`%d`, len(structContentStr)-2) + } } if option.WithoutType { buffer.WriteString(structContentStr) diff --git a/util/gvalid/gvalid_error.go b/util/gvalid/gvalid_error.go index 08f553fd7..34a740e0a 100644 --- a/util/gvalid/gvalid_error.go +++ b/util/gvalid/gvalid_error.go @@ -19,50 +19,62 @@ type Error interface { Code() gcode.Code Current() error Error() string - FirstItem() (key string, messages map[string]string) - FirstRule() (rule string, err string) - FirstString() (err string) - Items() (items []map[string]map[string]string) - Map() map[string]string - Maps() map[string]map[string]string + FirstItem() (key string, messages map[string]error) + FirstRule() (rule string, err error) + FirstError() (err error) + Items() (items []map[string]map[string]error) + Map() map[string]error + Maps() map[string]map[string]error String() string Strings() (errs []string) } // validationError is the validation error for validation result. type validationError struct { - code gcode.Code // Error code. - rules []fieldRule // Rules by sequence, which is used for keeping error sequence. - errors map[string]map[string]string // Error map:map[field]map[rule]message - firstKey string // The first error rule key(empty in default). - firstItem map[string]string // The first error rule value(nil in default). + code gcode.Code // Error code. + rules []fieldRule // Rules by sequence, which is used for keeping error sequence. + errors map[string]map[string]error // Error map:map[field]map[rule]message + firstKey string // The first error rule key(empty in default). + firstItem map[string]error // The first error rule value(nil in default). } -// newError creates and returns a validation error. -func newError(code gcode.Code, rules []fieldRule, errors map[string]map[string]string) *validationError { - for field, m := range errors { - for k, v := range m { - v = strings.Replace(v, ":attribute", field, -1) - v, _ = gregex.ReplaceString(`\s{2,}`, ` `, v) - v = gstr.Trim(v) - m[k] = v +// newValidationError creates and returns a validation error. +func newValidationError(code gcode.Code, rules []fieldRule, fieldRuleErrorMap map[string]map[string]error) *validationError { + var ( + s string + ) + for field, ruleErrorMap := range fieldRuleErrorMap { + for rule, err := range ruleErrorMap { + if !gerror.HasStack(err) { + s = strings.Replace(err.Error(), ":attribute", field, -1) + s, _ = gregex.ReplaceString(`\s{2,}`, ` `, s) + ruleErrorMap[rule] = gerror.NewOption(gerror.Option{ + Stack: false, + Text: gstr.Trim(s), + Code: code, + }) + } } - errors[field] = m + fieldRuleErrorMap[field] = ruleErrorMap } return &validationError{ code: code, rules: rules, - errors: errors, + errors: fieldRuleErrorMap, } } -// newErrorStr creates and returns a validation error by string. -func newErrorStr(key, err string) *validationError { - return newError(gcode.CodeInternalError, nil, map[string]map[string]string{ - internalErrorMapKey: { - key: err, +// newValidationErrorByStr creates and returns a validation error by string. +func newValidationErrorByStr(key string, err error) *validationError { + return newValidationError( + gcode.CodeInternalError, + nil, + map[string]map[string]error{ + internalErrorMapKey: { + key: err, + }, }, - }) + ) } // Code returns the error code of current validation error. @@ -74,16 +86,16 @@ func (e *validationError) Code() gcode.Code { } // Map returns the first error message as map. -func (e *validationError) Map() map[string]string { +func (e *validationError) Map() map[string]error { if e == nil { - return map[string]string{} + return map[string]error{} } _, m := e.FirstItem() return m } // Maps returns all error messages as map. -func (e *validationError) Maps() map[string]map[string]string { +func (e *validationError) Maps() map[string]map[string]error { if e == nil { return nil } @@ -92,16 +104,16 @@ func (e *validationError) Maps() map[string]map[string]string { // Items retrieves and returns error items array in sequence if possible, // or else it returns error items with no sequence . -func (e *validationError) Items() (items []map[string]map[string]string) { +func (e *validationError) Items() (items []map[string]map[string]error) { if e == nil { - return []map[string]map[string]string{} + return []map[string]map[string]error{} } - items = make([]map[string]map[string]string, 0) + items = make([]map[string]map[string]error, 0) // By sequence. if len(e.rules) > 0 { for _, v := range e.rules { if errorItemMap, ok := e.errors[v.Name]; ok { - items = append(items, map[string]map[string]string{ + items = append(items, map[string]map[string]error{ v.Name: errorItemMap, }) } @@ -110,7 +122,7 @@ func (e *validationError) Items() (items []map[string]map[string]string) { } // No sequence. for name, errorRuleMap := range e.errors { - items = append(items, map[string]map[string]string{ + items = append(items, map[string]map[string]error{ name: errorRuleMap, }) } @@ -118,9 +130,9 @@ func (e *validationError) Items() (items []map[string]map[string]string) { } // FirstItem returns the field name and error messages for the first validation rule error. -func (e *validationError) FirstItem() (key string, messages map[string]string) { +func (e *validationError) FirstItem() (key string, messages map[string]error) { if e == nil { - return "", map[string]string{} + return "", map[string]error{} } if e.firstItem != nil { return e.firstKey, e.firstItem @@ -145,9 +157,9 @@ func (e *validationError) FirstItem() (key string, messages map[string]string) { } // FirstRule returns the first error rule and message string. -func (e *validationError) FirstRule() (rule string, err string) { +func (e *validationError) FirstRule() (rule string, err error) { if e == nil { - return "", "" + return "", nil } // By sequence. if len(e.rules) > 0 { @@ -169,26 +181,22 @@ func (e *validationError) FirstRule() (rule string, err string) { return k, v } } - return "", "" + return "", nil } -// FirstString returns the first error message as string. +// FirstError returns the first error message as string. // Note that the returned message might be different if it has no sequence. -func (e *validationError) FirstString() (err string) { +func (e *validationError) FirstError() (err error) { if e == nil { - return "" + return nil } _, err = e.FirstRule() return } -// Current is alis of FirstString, which implements interface gerror.iCurrent. +// Current is alis of FirstError, which implements interface gerror.iCurrent. func (e *validationError) Current() error { - if e == nil { - return nil - } - _, err := e.FirstRule() - return gerror.NewCode(e.code, err) + return e.FirstError() } // String returns all error messages as string, multiple error messages joined using char ';'. @@ -221,13 +229,13 @@ func (e *validationError) Strings() (errs []string) { for _, ruleItem := range strings.Split(v.Rule, "|") { ruleItem = strings.TrimSpace(strings.Split(ruleItem, ":")[0]) if err, ok := errorItemMap[ruleItem]; ok { - errs = append(errs, err) + errs = append(errs, err.Error()) } } // internal error checks. for k, _ := range internalErrKeyMap { if err, ok := errorItemMap[k]; ok { - errs = append(errs, err) + errs = append(errs, err.Error()) } } } @@ -237,7 +245,7 @@ func (e *validationError) Strings() (errs []string) { // No sequence. for _, errorItemMap := range e.errors { for _, err := range errorItemMap { - errs = append(errs, err) + errs = append(errs, err.Error()) } } return diff --git a/util/gvalid/gvalid_validator_check_map.go b/util/gvalid/gvalid_validator_check_map.go index e3f10fa56..4e5b7ad0d 100644 --- a/util/gvalid/gvalid_validator_check_map.go +++ b/util/gvalid/gvalid_validator_check_map.go @@ -8,6 +8,7 @@ package gvalid import ( "context" + "errors" "github.com/gogf/gf/v2/errors/gcode" "github.com/gogf/gf/v2/util/gconv" "strings" @@ -27,7 +28,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { var ( checkRules = make([]fieldRule, 0) customMessage = make(CustomMsg) // map[RuleKey]ErrorMsg. - errorMaps = make(map[string]map[string]string) + errorMaps = make(map[string]map[string]error) ) switch assertValue := v.rules.(type) { // Sequence tag: []sequence tag @@ -80,9 +81,9 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { } data := gconv.Map(params) if data == nil { - return newErrorStr( + return newValidationErrorByStr( internalParamsErrRuleName, - "invalid params type: convert to map failed", + errors.New("invalid params type: convert to map failed"), ) } if msg, ok := v.messages.(CustomMsg); ok && len(msg) > 0 { @@ -139,7 +140,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { } } if _, ok := errorMaps[checkRuleItem.Name]; !ok { - errorMaps[checkRuleItem.Name] = make(map[string]string) + errorMaps[checkRuleItem.Name] = make(map[string]error) } for ruleKey, errorItemMsgMap := range errorItem { errorMaps[checkRuleItem.Name][ruleKey] = errorItemMsgMap @@ -150,7 +151,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params interface{}) Error { } } if len(errorMaps) > 0 { - return newError(gcode.CodeValidationFailed, checkRules, errorMaps) + return newValidationError(gcode.CodeValidationFailed, checkRules, errorMaps) } return nil } diff --git a/util/gvalid/gvalid_validator_check_struct.go b/util/gvalid/gvalid_validator_check_struct.go index 878e8a728..647a856c1 100644 --- a/util/gvalid/gvalid_validator_check_struct.go +++ b/util/gvalid/gvalid_validator_check_struct.go @@ -23,8 +23,8 @@ func (v *Validator) CheckStruct(ctx context.Context, object interface{}) Error { func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error { var ( - errorMaps = make(map[string]map[string]string) // Returning error. - fieldToAliasNameMap = make(map[string]string) // Field names to alias name map. + errorMaps = make(map[string]map[string]error) // Returning error. + fieldToAliasNameMap = make(map[string]string) // Field names to alias name map. ) fieldMap, err := structs.FieldMap(structs.FieldMapInput{ Pointer: object, @@ -32,7 +32,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error RecursiveOption: structs.RecursiveOptionEmbedded, }) if err != nil { - return newErrorStr(internalObjectErrRuleName, err.Error()) + return newValidationErrorByStr(internalObjectErrRuleName, err) } // It checks the struct recursively if its attribute is an embedded struct. for _, field := range fieldMap { @@ -59,7 +59,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error // It here must use structs.TagFields not structs.FieldMap to ensure error sequence. tagField, err := structs.TagFields(object, structTagPriority) if err != nil { - return newErrorStr(internalObjectErrRuleName, err.Error()) + return newValidationErrorByStr(internalObjectErrRuleName, err) } // If there's no struct tag and validation rules, it does nothing and returns quickly. if len(tagField) == 0 && v.messages == nil { @@ -278,7 +278,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } if _, ok := errorMaps[checkRuleItem.Name]; !ok { - errorMaps[checkRuleItem.Name] = make(map[string]string) + errorMaps[checkRuleItem.Name] = make(map[string]error) } for ruleKey, errorItemMsgMap := range errorItem { errorMaps[checkRuleItem.Name][ruleKey] = errorItemMsgMap @@ -289,7 +289,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object interface{}) Error } } if len(errorMaps) > 0 { - return newError(gcode.CodeValidationFailed, checkRules, errorMaps) + return newValidationError(gcode.CodeValidationFailed, checkRules, errorMaps) } return nil } diff --git a/util/gvalid/gvalid_validator_check_value.go b/util/gvalid/gvalid_validator_check_value.go index 182a10d10..948601395 100644 --- a/util/gvalid/gvalid_validator_check_value.go +++ b/util/gvalid/gvalid_validator_check_value.go @@ -8,9 +8,9 @@ package gvalid import ( "context" + "errors" "github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/errors/gcode" - "github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/text/gstr" "strconv" "strings" @@ -61,7 +61,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E // It converts value to string and then does the validation. var ( // Do not trim it as the space is also part of the value. - errorMsgArray = make(map[string]string) + ruleErrorMap = make(map[string]error) ) // Custom error messages handling. var ( @@ -87,9 +87,9 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E ruleItems[i-1] += "|" + ruleItems[i] ruleItems = append(ruleItems[:i], ruleItems[i+1:]...) } else { - return newErrorStr( + return newValidationErrorByStr( internalRulesErrRuleName, - internalRulesErrRuleName+": "+input.Rule, + errors.New(internalRulesErrRuleName+": "+input.Rule), ) } } else { @@ -141,7 +141,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E Data: gvar.New(input.DataRaw), }); err != nil { match = false - errorMsgArray[ruleKey] = err.Error() + ruleErrorMap[ruleKey] = err } else { match = true } @@ -160,7 +160,7 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E }, ) if !match && err != nil { - errorMsgArray[ruleKey] = err.Error() + ruleErrorMap[ruleKey] = err } } @@ -168,8 +168,8 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E if !match { // It does nothing if the error message for this rule // is already set in previous validation. - if _, ok := errorMsgArray[ruleKey]; !ok { - errorMsgArray[ruleKey] = v.getErrorMessageByRule(ctx, ruleKey, customMsgMap) + if _, ok := ruleErrorMap[ruleKey]; !ok { + ruleErrorMap[ruleKey] = errors.New(v.getErrorMessageByRule(ctx, ruleKey, customMsgMap)) } // If it is with error and there's bail rule, // it then does not continue validating for left rules. @@ -179,12 +179,12 @@ func (v *Validator) doCheckValue(ctx context.Context, input doCheckValueInput) E } index++ } - if len(errorMsgArray) > 0 { - return newError( + if len(ruleErrorMap) > 0 { + return newValidationError( gcode.CodeValidationFailed, []fieldRule{{Name: input.Name, Rule: input.Rule}}, - map[string]map[string]string{ - input.Name: errorMsgArray, + map[string]map[string]error{ + input.Name: ruleErrorMap, }, ) } @@ -223,10 +223,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI "max-length", "size": if msg := v.checkLength(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } else { match = true } @@ -237,10 +234,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI "max", "between": if msg := v.checkRange(ctx, valueStr, input.RuleKey, input.RulePattern, input.CustomMsgMap); msg != "" { - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } else { match = true } @@ -288,10 +282,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI ) msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) msg = strings.Replace(msg, ":format", input.RulePattern, -1) - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } // Values of two fields should be equal as string. @@ -306,10 +297,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI var msg string msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) msg = strings.Replace(msg, ":field", input.RulePattern, -1) - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } // Values of two fields should not be equal as string. @@ -325,10 +313,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI var msg string msg = v.getErrorMessageByRule(ctx, input.RuleKey, input.CustomMsgMap) msg = strings.Replace(msg, ":field", input.RulePattern, -1) - return match, gerror.NewOption(gerror.Option{ - Text: msg, - Code: gcode.CodeValidationFailed, - }) + return match, errors.New(msg) } // Field value should be in range of. @@ -511,10 +496,7 @@ func (v *Validator) doCheckBuildInRules(ctx context.Context, input doCheckBuildI match = gregex.IsMatchString(`^([0-9A-Fa-f]{2}[\-:]){5}[0-9A-Fa-f]{2}$`, valueStr) default: - return match, gerror.NewOption(gerror.Option{ - Text: "Invalid rule name: " + input.RuleKey, - Code: gcode.CodeInvalidParameter, - }) + return match, errors.New("Invalid rule name: " + input.RuleKey) } return match, nil } diff --git a/util/gvalid/gvalid_z_example_test.go b/util/gvalid/gvalid_z_example_test.go index c9c8c0128..b4e3e3eec 100644 --- a/util/gvalid/gvalid_z_example_test.go +++ b/util/gvalid/gvalid_z_example_test.go @@ -33,7 +33,7 @@ func ExampleCheckMap() { if e := gvalid.CheckMap(gctx.New(), params, rules); e != nil { fmt.Println(e.Map()) fmt.Println(e.FirstItem()) - fmt.Println(e.FirstString()) + fmt.Println(e.FirstError()) } // May Output: // map[required:账号不能为空 length:账号长度应当在6到16之间] @@ -55,7 +55,7 @@ func ExampleCheckMap2() { if e := gvalid.CheckMap(gctx.New(), params, rules); e != nil { fmt.Println(e.Map()) fmt.Println(e.FirstItem()) - fmt.Println(e.FirstString()) + fmt.Println(e.FirstError()) } // Output: // map[same:两次密码输入不相等] diff --git a/util/gvalid/gvalid_z_unit_basic_all_test.go b/util/gvalid/gvalid_z_unit_basic_all_test.go index 83741d477..647316230 100755 --- a/util/gvalid/gvalid_z_unit_basic_all_test.go +++ b/util/gvalid/gvalid_z_unit_basic_all_test.go @@ -1037,7 +1037,7 @@ func Test_InternalError_String(t *testing.T) { t.Assert(err.String(), "InvalidRules: hh") t.Assert(err.Strings(), g.Slice{"InvalidRules: hh"}) - t.Assert(err.FirstString(), "InvalidRules: hh") + t.Assert(err.FirstError(), "InvalidRules: hh") t.Assert(gerror.Current(err), "InvalidRules: hh") }) } diff --git a/util/gvalid/gvalid_z_unit_customerror_test.go b/util/gvalid/gvalid_z_unit_customerror_test.go index 51941c171..b1a30be68 100755 --- a/util/gvalid/gvalid_z_unit_customerror_test.go +++ b/util/gvalid/gvalid_z_unit_customerror_test.go @@ -35,9 +35,8 @@ func Test_FirstString(t *testing.T) { rule = "ipv4" val = "0.0.0" err = gvalid.CheckValue(context.TODO(), val, rule, nil) - n = err.FirstString() ) - t.Assert(n, "The value must be a valid IPv4 address") + t.Assert(err.FirstError(), "The value must be a valid IPv4 address") }) } @@ -52,12 +51,12 @@ func Test_CustomError1(t *testing.T) { t.Error("规则校验失败") } else { if v, ok := e.Map()["integer"]; ok { - if strings.Compare(v, msgs["integer"]) != 0 { + if strings.Compare(v.Error(), msgs["integer"]) != 0 { t.Error("错误信息不匹配") } } if v, ok := e.Map()["length"]; ok { - if strings.Compare(v, msgs["length"]) != 0 { + if strings.Compare(v.Error(), msgs["length"]) != 0 { t.Error("错误信息不匹配") } } @@ -72,12 +71,12 @@ func Test_CustomError2(t *testing.T) { t.Error("规则校验失败") } else { if v, ok := e.Map()["integer"]; ok { - if strings.Compare(v, "请输入一个整数") != 0 { + if strings.Compare(v.Error(), "请输入一个整数") != 0 { t.Error("错误信息不匹配") } } if v, ok := e.Map()["length"]; ok { - if strings.Compare(v, "参数长度不对啊老铁") != 0 { + if strings.Compare(v.Error(), "参数长度不对啊老铁") != 0 { t.Error("错误信息不匹配") } }