improve package gvalid

This commit is contained in:
John Guo
2021-05-19 13:29:40 +08:00
parent ea0340db8e
commit 420e0b9ca4
8 changed files with 152 additions and 100 deletions

View File

@ -0,0 +1,28 @@
package main
import (
"context"
"github.com/gogf/gf/frame/g"
"github.com/gogf/gf/util/gconv"
"github.com/gogf/gf/util/gvalid"
)
func main() {
type User struct {
Name string `v:"required#请输入用户姓名"`
Type int `v:"required#请选择用户类型"`
}
data := g.Map{
"name": "john",
}
user := User{}
if err := gconv.Scan(data, &user); err != nil {
panic(err)
}
err := gvalid.CheckStructWithData(context.TODO(), user, data, nil)
// 也可以使用
// err := g.Validator().Data(data).CheckStruct(user)
if err != nil {
g.Dump(err.Items())
}
}

View File

@ -70,14 +70,14 @@ type apiNoValidation interface {
}
const (
// regular expression pattern for single validation rule.
singleRulePattern = `^([\w-]+):{0,1}(.*)`
invalidRulesErrKey = "invalid_rules"
invalidParamsErrKey = "invalid_params"
invalidObjectErrKey = "invalid_object"
// no validation tag name for struct attribute.
noValidationTagName = "nv"
singleRulePattern = `^([\w-]+):{0,1}(.*)` // regular expression pattern for single validation rule.
internalRulesErrRuleName = "InvalidRules" // rule name for internal invalid rules validation error.
internalParamsErrRuleName = "InvalidParams" // rule name for internal invalid params validation error.
internalObjectErrRuleName = "InvalidObject" // rule name for internal invalid object validation error.
internalErrorMapKey = "__InternalError__" // error map key for internal errors.
internalDefaultRuleName = "__default__" // default rule name for i18n error message format if no i18n message found for specified error rule.
ruleMessagePrefixForI18n = "gf.gvalid.rule." // prefix string for each rule configuration in i18n content.
noValidationTagName = "nv" // no validation tag name for struct attribute.
)
var (
@ -87,9 +87,9 @@ var (
// all internal error keys.
internalErrKeyMap = map[string]string{
invalidRulesErrKey: invalidRulesErrKey,
invalidParamsErrKey: invalidParamsErrKey,
invalidObjectErrKey: invalidObjectErrKey,
internalRulesErrRuleName: internalRulesErrRuleName,
internalParamsErrRuleName: internalParamsErrRuleName,
internalObjectErrRuleName: internalObjectErrRuleName,
}
// regular expression object for single rule
// which is compiled just once and of repeatable usage.
@ -168,6 +168,54 @@ var (
"off": {},
"no": {},
}
// defaultMessages is the default error messages.
// Note that these messages are synchronized from ./i18n/en/validation.toml .
defaultMessages = map[string]string{
"required": "The :attribute field is required",
"required-if": "The :attribute field is required",
"required-unless": "The :attribute field is required",
"required-with": "The :attribute field is required",
"required-with-all": "The :attribute field is required",
"required-without": "The :attribute field is required",
"required-without-all": "The :attribute field is required",
"date": "The :attribute value is not a valid date",
"date-format": "The :attribute value does not match the format :format",
"email": "The :attribute value must be a valid email address",
"phone": "The :attribute value must be a valid phone number",
"telephone": "The :attribute value must be a valid telephone number",
"passport": "The :attribute value is not a valid passport format",
"password": "The :attribute value is not a valid passport format",
"password2": "The :attribute value is not a valid passport format",
"password3": "The :attribute value is not a valid passport format",
"postcode": "The :attribute value is not a valid passport format",
"resident-id": "The :attribute value is not a valid resident id number",
"bank-card": "The :attribute value must be a valid bank card number",
"qq": "The :attribute value must be a valid QQ number",
"ip": "The :attribute value must be a valid IP address",
"ipv4": "The :attribute value must be a valid IPv4 address",
"ipv6": "The :attribute value must be a valid IPv6 address",
"mac": "The :attribute value must be a valid MAC address",
"url": "The :attribute value must be a valid URL address",
"domain": "The :attribute value must be a valid domain format",
"length": "The :attribute value length must be between :min and :max",
"min-length": "The :attribute value length must be equal or greater than :min",
"max-length": "The :attribute value length must be equal or lesser than :max",
"between": "The :attribute value must be between :min and :max",
"min": "The :attribute value must be equal or greater than :min",
"max": "The :attribute value must be equal or lesser than :max",
"json": "The :attribute value must be a valid JSON string",
"xml": "The :attribute value must be a valid XML string",
"array": "The :attribute value must be an array",
"integer": "The :attribute value must be an integer",
"float": "The :attribute value must be a float",
"boolean": "The :attribute value field must be true or false",
"same": "The :attribute value must be the same as field :field",
"different": "The :attribute value must be different from field :field",
"in": "The :attribute value is not in acceptable range",
"not-in": "The :attribute value is not in acceptable range",
"regex": "The :attribute value is invalid",
internalDefaultRuleName: "The :attribute value is invalid",
}
)
// CheckValue checks single value with specified rules.

View File

@ -20,6 +20,7 @@ type Error interface {
FirstItem() (key string, messages map[string]string)
FirstRule() (rule string, err string)
FirstString() (err string)
Items() (items []map[string]map[string]string)
Map() map[string]string
Maps() map[string]map[string]string
String() string
@ -30,7 +31,7 @@ type Error interface {
type validationError struct {
rules []string // Rules by sequence, which is used for keeping error sequence.
errors map[string]map[string]string // Error map:map[field]map[rule]message
firstKey string // The first error rule key(nil in default).
firstKey string // The first error rule key(empty in default).
firstItem map[string]string // The first error rule value(nil in default).
}
@ -54,7 +55,7 @@ func newError(rules []string, errors map[string]map[string]string) *validationEr
// newErrorStr creates and returns a validation error by string.
func newErrorStr(key, err string) *validationError {
return newError(nil, map[string]map[string]string{
"__gvalid__": {
internalErrorMapKey: {
key: err,
},
})
@ -77,6 +78,34 @@ func (e *validationError) Maps() map[string]map[string]string {
return e.errors
}
// Items retrieves and returns error items array in sequence if possible,
// or else it returns error items with no sequence .
func (e *validationError) Items() (items []map[string]map[string]string) {
if e == nil {
return []map[string]map[string]string{}
}
items = make([]map[string]map[string]string, 0)
// By sequence.
if len(e.rules) > 0 {
for _, v := range e.rules {
name, _, _ := parseSequenceTag(v)
if errorItemMap, ok := e.errors[name]; ok {
items = append(items, map[string]map[string]string{
name: errorItemMap,
})
}
}
return items
}
// No sequence.
for name, errorRuleMap := range e.errors {
items = append(items, map[string]map[string]string{
name: errorRuleMap,
})
}
return
}
// FirstItem returns the field name and error messages for the first validation rule error.
func (e *validationError) FirstItem() (key string, messages map[string]string) {
if e == nil {
@ -89,10 +118,10 @@ func (e *validationError) FirstItem() (key string, messages map[string]string) {
if len(e.rules) > 0 {
for _, v := range e.rules {
name, _, _ := parseSequenceTag(v)
if m, ok := e.errors[name]; ok {
if errorItemMap, ok := e.errors[name]; ok {
e.firstKey = name
e.firstItem = m
return name, m
e.firstItem = errorItemMap
return name, errorItemMap
}
}
}
@ -113,21 +142,21 @@ func (e *validationError) FirstRule() (rule string, err string) {
// By sequence.
if len(e.rules) > 0 {
for _, v := range e.rules {
name, rule, _ := parseSequenceTag(v)
if m, ok := e.errors[name]; ok {
for _, rule := range strings.Split(rule, "|") {
array := strings.Split(rule, ":")
rule = strings.TrimSpace(array[0])
if err, ok := m[rule]; ok {
return rule, err
name, ruleStr, _ := parseSequenceTag(v)
if errorItemMap, ok := e.errors[name]; ok {
for _, ruleItem := range strings.Split(ruleStr, "|") {
array := strings.Split(ruleItem, ":")
ruleItem = strings.TrimSpace(array[0])
if err, ok = errorItemMap[ruleItem]; ok {
return ruleStr, err
}
}
}
}
}
// No sequence.
for _, m := range e.errors {
for k, v := range m {
for _, errorItemMap := range e.errors {
for k, v := range errorItemMap {
return k, v
}
}
@ -178,18 +207,18 @@ func (e *validationError) Strings() (errs []string) {
// By sequence.
if len(e.rules) > 0 {
for _, v := range e.rules {
name, rule, _ := parseSequenceTag(v)
if m, ok := e.errors[name]; ok {
name, ruleStr, _ := parseSequenceTag(v)
if errorItemMap, ok := e.errors[name]; ok {
// validation error checks.
for _, rule := range strings.Split(rule, "|") {
rule = strings.TrimSpace(strings.Split(rule, ":")[0])
if err, ok := m[rule]; ok {
for _, ruleItem := range strings.Split(ruleStr, "|") {
ruleItem = strings.TrimSpace(strings.Split(ruleItem, ":")[0])
if err, ok := errorItemMap[ruleItem]; ok {
errs = append(errs, err)
}
}
// internal error checks.
for k, _ := range internalErrKeyMap {
if err, ok := m[k]; ok {
if err, ok := errorItemMap[k]; ok {
errs = append(errs, err)
}
}
@ -198,8 +227,8 @@ func (e *validationError) Strings() (errs []string) {
return errs
}
// No sequence.
for _, m := range e.errors {
for _, err := range m {
for _, errorItemMap := range e.errors {
for _, err := range errorItemMap {
errs = append(errs, err)
}
}

View File

@ -71,8 +71,8 @@ func (v *Validator) doCheckValue(key string, value interface{}, rules string, me
ruleItems = append(ruleItems[:i], ruleItems[i+1:]...)
} else {
return newErrorStr(
invalidRulesErrKey,
invalidRulesErrKey+": "+rules,
internalRulesErrRuleName,
internalRulesErrRuleName+": "+rules,
)
}
} else {

View File

@ -73,7 +73,7 @@ func (v *Validator) doCheckMap(params interface{}) Error {
data := gconv.Map(params)
if data == nil {
return newErrorStr(
"invalid_params",
internalParamsErrRuleName,
"invalid params type: convert to map failed",
)
}

View File

@ -26,7 +26,7 @@ func (v *Validator) doCheckStruct(object interface{}) Error {
)
fieldMap, err := structs.FieldMap(object, aliasNameTagPriority, true)
if err != nil {
return newErrorStr("invalid_object", err.Error())
return newErrorStr(internalObjectErrRuleName, err.Error())
}
// It checks the struct recursively the its attribute is an embedded struct.
for _, field := range fieldMap {
@ -49,7 +49,7 @@ func (v *Validator) doCheckStruct(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("invalid_object", err.Error())
return newErrorStr(internalObjectErrRuleName, err.Error())
}
// If there's no struct tag and validation rules, it does nothing and returns quickly.
if len(tagField) == 0 && v.messages == nil {

View File

@ -6,59 +6,6 @@
package gvalid
const (
ruleMessagePrefixForI18n = "gf.gvalid.rule."
)
// defaultMessages is the default error messages.
// Note that these messages are synchronized from ./i18n/en/validation.toml .
var defaultMessages = map[string]string{
"required": "The :attribute field is required",
"required-if": "The :attribute field is required",
"required-unless": "The :attribute field is required",
"required-with": "The :attribute field is required",
"required-with-all": "The :attribute field is required",
"required-without": "The :attribute field is required",
"required-without-all": "The :attribute field is required",
"date": "The :attribute value is not a valid date",
"date-format": "The :attribute value does not match the format :format",
"email": "The :attribute value must be a valid email address",
"phone": "The :attribute value must be a valid phone number",
"telephone": "The :attribute value must be a valid telephone number",
"passport": "The :attribute value is not a valid passport format",
"password": "The :attribute value is not a valid passport format",
"password2": "The :attribute value is not a valid passport format",
"password3": "The :attribute value is not a valid passport format",
"postcode": "The :attribute value is not a valid passport format",
"resident-id": "The :attribute value is not a valid resident id number",
"bank-card": "The :attribute value must be a valid bank card number",
"qq": "The :attribute value must be a valid QQ number",
"ip": "The :attribute value must be a valid IP address",
"ipv4": "The :attribute value must be a valid IPv4 address",
"ipv6": "The :attribute value must be a valid IPv6 address",
"mac": "The :attribute value must be a valid MAC address",
"url": "The :attribute value must be a valid URL address",
"domain": "The :attribute value must be a valid domain format",
"length": "The :attribute value length must be between :min and :max",
"min-length": "The :attribute value length must be equal or greater than :min",
"max-length": "The :attribute value length must be equal or lesser than :max",
"between": "The :attribute value must be between :min and :max",
"min": "The :attribute value must be equal or greater than :min",
"max": "The :attribute value must be equal or lesser than :max",
"json": "The :attribute value must be a valid JSON string",
"xml": "The :attribute value must be a valid XML string",
"array": "The :attribute value must be an array",
"integer": "The :attribute value must be an integer",
"float": "The :attribute value must be a float",
"boolean": "The :attribute value field must be true or false",
"same": "The :attribute value must be the same as field :field",
"different": "The :attribute value must be different from field :field",
"in": "The :attribute value is not in acceptable range",
"not-in": "The :attribute value is not in acceptable range",
"regex": "The :attribute value is invalid",
"__default__": "The :attribute value is invalid",
}
// getErrorMessageByRule retrieves and returns the error message for specified rule.
// It firstly retrieves the message from custom message map, and then checks i18n manager,
// it returns the default error message if it's not found in custom message map or i18n manager.
@ -79,9 +26,9 @@ func (v *Validator) getErrorMessageByRule(ruleKey string, customMsgMap map[strin
}
// If there's no configured rule message, it uses default one.
if content == "" {
content = v.i18nManager.GetContent(v.ctx, `gf.gvalid.rule.__default__`)
content = v.i18nManager.GetContent(v.ctx, ruleMessagePrefixForI18n+internalDefaultRuleName)
if content == "" {
content = defaultMessages["__default__"]
content = defaultMessages[internalDefaultRuleName]
}
}
return content

View File

@ -27,9 +27,9 @@ func Test_Check(t *testing.T) {
err1 := gvalid.CheckValue(context.TODO(), val1, rule, nil)
err2 := gvalid.CheckValue(context.TODO(), val2, rule, nil)
err3 := gvalid.CheckValue(context.TODO(), val3, rule, nil)
t.Assert(err1, "invalid_rules: abc:6,16")
t.Assert(err2, "invalid_rules: abc:6,16")
t.Assert(err3, "invalid_rules: abc:6,16")
t.Assert(err1, "InvalidRules: abc:6,16")
t.Assert(err2, "InvalidRules: abc:6,16")
t.Assert(err3, "InvalidRules: abc:6,16")
})
}
@ -995,9 +995,9 @@ func Test_InternalError_String(t *testing.T) {
aa := a{Name: "2"}
err := gvalid.CheckStruct(context.TODO(), &aa, nil)
t.Assert(err.String(), "invalid_rules: hh")
t.Assert(err.Strings(), g.Slice{"invalid_rules: hh"})
t.Assert(err.FirstString(), "invalid_rules: hh")
t.Assert(gerror.Current(err), "invalid_rules: hh")
t.Assert(err.String(), "InvalidRules: hh")
t.Assert(err.Strings(), g.Slice{"InvalidRules: hh"})
t.Assert(err.FirstString(), "InvalidRules: hh")
t.Assert(gerror.Current(err), "InvalidRules: hh")
})
}