Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions net/ghttp/ghttp_request_param.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ func (r *Request) doParse(pointer any, requestType int) error {
}
}
// Validation.
if err = gvalid.New().
// The validation rules here are all from struct tag, which are static,
// so the parsed rule value cache is enabled for performance.
if err = gvalid.New(true).
Bail().
Data(pointer).
Assoc(data).
Expand All @@ -129,7 +131,9 @@ func (r *Request) doParse(pointer any, requestType int) error {
return err
}
for i := 0; i < reflectVal2.Len(); i++ {
if err = gvalid.New().
// The validation rules here are all from struct tag, which are static,
// so the parsed rule value cache is enabled for performance.
if err = gvalid.New(true).
Bail().
Data(reflectVal2.Index(i)).
Assoc(j.Get(gconv.String(i)).Map()).
Expand Down
5 changes: 5 additions & 0 deletions util/gtag/gtag_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package gtag

import (
"regexp"
"strings"

"github.com/gogf/gf/v2/errors/gerror"
)
Expand Down Expand Up @@ -56,6 +57,10 @@ func Get(name string) string {
// gtag.Set("demo", "content")
// Parse(`This is {demo}`) -> `This is content`.
func Parse(content string) string {
// Fast path: there is no variable placeholder, which means no need for regexp replacing.
if strings.IndexByte(content, '{') < 0 {
return content
}
return regex.ReplaceAllStringFunc(content, func(s string) string {
if v, ok := data[s[1:len(s)-1]]; ok {
return v
Expand Down
10 changes: 10 additions & 0 deletions util/gtag/gtag_z_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ func Test_Parse(t *testing.T) {
})
}

func Test_Parse_NoPlaceholder(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
// Content without any variable placeholder returns directly.
t.Assert(gtag.Parse(`required|length:1,30#name is required`), `required|length:1,30#name is required`)
// Content with unregistered placeholder keeps unchanged.
content := fmt.Sprintf(`this is {%s}`, guid.S())
t.Assert(gtag.Parse(content), content)
})
}

func Test_SetGlobalEnums(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
oldEnumsJson, err := gtag.GetGlobalEnums()
Expand Down
107 changes: 84 additions & 23 deletions util/gvalid/gvalid.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ package gvalid
import (
"context"
"reflect"
"regexp"
"strings"
"sync"

"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gtag"
)

Expand All @@ -37,22 +38,21 @@ type iNoValidation interface {
}

const (
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 = gtag.NoValidation // no validation tag name for struct attribute.
ruleNameRegex = "regex" // the name for rule "regex"
ruleNameNotRegex = "not-regex" // the name for rule "not-regex"
ruleNameForeach = "foreach" // the name for rule "foreach"
ruleNameBail = "bail" // the name for rule "bail"
ruleNameCi = "ci" // the name for rule "ci"
emptyJsonArrayStr = "[]" // Empty json string for array type.
emptyJsonObjectStr = "{}" // Empty json string for object type.
requiredRulesPrefix = "required" // requiredRulesPrefix specifies the rule prefix that must be validated even the value is empty (nil or empty).
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 = gtag.NoValidation // no validation tag name for struct attribute.
ruleNameRegex = "regex" // the name for rule "regex"
ruleNameNotRegex = "not-regex" // the name for rule "not-regex"
ruleNameForeach = "foreach" // the name for rule "foreach"
ruleNameBail = "bail" // the name for rule "bail"
ruleNameCi = "ci" // the name for rule "ci"
emptyJsonArrayStr = "[]" // Empty json string for array type.
emptyJsonObjectStr = "{}" // Empty json string for object type.
requiredRulesPrefix = "required" // requiredRulesPrefix specifies the rule prefix that must be validated even the value is empty (nil or empty).
)

var (
Expand All @@ -74,9 +74,6 @@ var (
internalParamsErrRuleName: internalParamsErrRuleName,
internalObjectErrRuleName: internalObjectErrRuleName,
}
// regular expression object for single rule
// which is compiled just once and of repeatable usage.
ruleRegex = regexp.MustCompile(singleRulePattern)

// decorativeRuleMap defines all rules that are just marked rules which have neither functional meaning
// nor error messages.
Expand All @@ -87,20 +84,84 @@ var (
}
)

// parsedTagValue is the parsed result of one validation tag value.
type parsedTagValue struct {
field string
rule string
msg string
}

// parsedTagValueCache caches the parsed results of the validation tag values.
// The tag values are usually static for the same struct definitions, so their
// parse results are always the same, which makes it safe to cache. It is
// managed by the Validator cache switch, see function New.
var parsedTagValueCache sync.Map // map[string]parsedTagValue

// ParseTagValue parses one sequence tag to field, rule and error message.
// The sequence tag is like: [alias@]rule[...#msg...]
func ParseTagValue(tag string) (field, rule, msg string) {
field, rule, msg, _ = parseTagValue(tag)
return
}

// parseTagValueCached performs like ParseTagValue, but it caches the parsed
// results of the successful parses.
func parseTagValueCached(tag string) (field, rule, msg string) {
if v, ok := parsedTagValueCache.Load(tag); ok {
var parsed = v.(parsedTagValue)
return parsed.field, parsed.rule, parsed.msg
}
var matched bool
field, rule, msg, matched = parseTagValue(tag)
if matched {
parsedTagValueCache.Store(tag, parsedTagValue{
field: field,
rule: rule,
msg: msg,
})
}
return
}

// parseTagValue parses one sequence tag value, and it also returns whether the
// tag value matches the sequence tag pattern.
func parseTagValue(tag string) (field, rule, msg string, matched bool) {
// Complete sequence tag.
// Example: name@required|length:2,20|password3|same:password1#||Password strength is insufficient | Passwords are not match
match, _ := gregex.MatchString(`\s*((\w+)\s*@){0,1}\s*([^#]+)\s*(#\s*(.*)){0,1}\s*`, tag)
if len(match) > 5 {
msg = strings.TrimSpace(match[5])
rule = strings.TrimSpace(match[3])
field = strings.TrimSpace(match[2])
} else {
intlog.Errorf(context.TODO(), `invalid validation tag value: %s`, tag)
return field, rule, msg, true
}
return
intlog.Errorf(context.TODO(), `invalid validation tag value: %s`, tag)
return "", "", "", false
}

// parseTagValue parses one validation tag value, using the parsed value cache
// unless the cache is disabled for current Validator.
func (v *Validator) parseTagValue(tag string) (field, rule, msg string) {
if v.cache {
return parseTagValueCached(tag)
}
return ParseTagValue(tag)
}

// parseRuleItem parses one rule item into rule key and rule pattern.
// The rule item is in format of: ruleKey[:rulePattern], for example: "max:6".
// Note that only the first char ':' is treated as the separator, and the rest
// part, which might contain chars like ':' and '|', belongs to the rule pattern.
// Examples:
//
// "required" -> ruleKey "required", rulePattern ""
// "between:1,100" -> ruleKey "between", rulePattern "1,100"
// "regex:^\d+:\w+$" -> ruleKey "regex", rulePattern "^\d+:\w+$"
func parseRuleItem(ruleItem string) (ruleKey, rulePattern string) {
if index := strings.IndexByte(ruleItem, ':'); index != -1 {
return gstr.Trim(ruleItem[:index]), gstr.Trim(ruleItem[index+1:])
}
return gstr.Trim(ruleItem), ""
}

// GetTags returns the validation tags.
Expand Down
20 changes: 16 additions & 4 deletions util/gvalid/gvalid_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,26 @@ type Validator struct {
bail bool // Stop validation after the first validation error.
foreach bool // It tells the next validation using current value as an array and validates each of its element.
caseInsensitive bool // Case-Insensitive configuration for those rules that need value comparison.
cache bool // Enable the parsed rule value cache for the validation, which is enabled in default.
}

// New creates and returns a new Validator.
func New() *Validator {
//
// The optional parameter `cached` specifies whether to enable the parsed rule
// value cache for the validation, which is disabled in default, as the rule
// values might be dynamically generated in user code and the process level
// cache would then grow without bound. Enable it if the rule values are known
// to be static, which is the case for the validation rules from struct tag,
// as the framework does for HTTP request handling (see package `ghttp`).
func New(cached ...bool) *Validator {
var cacheEnabled = false
if len(cached) > 0 {
cacheEnabled = cached[0]
}
return &Validator{
i18nManager: gi18n.Instance(), // Use default i18n manager.
ruleFuncMap: make(map[string]RuleFunc), // Custom rule function storing map.
cache: cacheEnabled,
}
}

Expand Down Expand Up @@ -87,9 +100,8 @@ func (v *Validator) Run(ctx context.Context) Error {

// Clone creates and returns a new Validator, which is a shallow copy of the current one.
func (v *Validator) Clone() *Validator {
newValidator := New()
*newValidator = *v
return newValidator
newValidator := *v
return &newValidator
}

// I18n sets the i18n manager for the validator.
Expand Down
2 changes: 1 addition & 1 deletion util/gvalid/gvalid_validator_check_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func (v *Validator) doCheckMap(ctx context.Context, params any) Error {
// Sequence has order for error results.
case []string:
for _, tag := range assertValue {
name, rule, msg := ParseTagValue(tag)
name, rule, msg := v.parseTagValue(tag)
if len(name) == 0 {
continue
}
Expand Down
6 changes: 3 additions & 3 deletions util/gvalid/gvalid_validator_check_struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (v *Validator) doCheckStruct(ctx context.Context, object any) Error {
// Sequence has order for error results.
case []string:
for _, tag := range assertValue {
name, rule, msg := ParseTagValue(tag)
name, rule, msg := v.parseTagValue(tag)
if len(name) == 0 {
continue
}
Expand Down Expand Up @@ -129,8 +129,8 @@ func (v *Validator) doCheckStruct(ctx context.Context, object any) Error {
for _, field := range tagFields {
var (
isMeta bool
fieldName = field.Name() // Attribute name.
name, rule, msg = ParseTagValue(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only.
fieldName = field.Name() // Attribute name.
name, rule, msg = v.parseTagValue(field.TagValue) // The `name` is different from `attribute alias`, which is used for validation only.
)
if len(name) == 0 {
if value, ok := fieldToAliasNameMap[fieldName]; ok {
Expand Down
28 changes: 20 additions & 8 deletions util/gvalid/gvalid_validator_check_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,16 +45,27 @@ func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Erro
)
// Custom error messages handling.
var (
msgArray = make([]string, 0)
customMsgMap = make(map[string]string)
msgArray []string
customMsgMap map[string]string
)
switch messages := in.Messages.(type) {
case string:
msgArray = strings.Split(messages, "|")

// The custom messages from struct tag parsing and from `Messages` function are
// usually of type map[string]string, which are directly usable without conversion.
// Note that it is only read in the following logic, so it is used directly without a copy.
case map[string]string:
customMsgMap = messages

default:
for k, message := range gconv.Map(in.Messages) {
customMsgMap[k] = gconv.String(message)
if messages != nil {
if msgMap := gconv.Map(messages); len(msgMap) > 0 {
customMsgMap = make(map[string]string, len(msgMap))
for k, message := range msgMap {
customMsgMap[k] = gconv.String(message)
}
}
}
}
// Handle the char '|' in the rule,
Expand Down Expand Up @@ -103,10 +114,8 @@ func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Erro
)
for index := 0; index < len(ruleItems); {
var (
err error
results = ruleRegex.FindStringSubmatch(ruleItems[index]) // split single rule.
ruleKey = gstr.Trim(results[1]) // rule key like "max" in rule "max: 6"
rulePattern = gstr.Trim(results[2]) // rule pattern is like "6" in rule:"max:6"
err error
ruleKey, rulePattern = parseRuleItem(ruleItems[index]) // split single rule, like "max:6" to "max" and "6".
)

if !hasBailRule && ruleKey == ruleNameBail {
Expand All @@ -126,6 +135,9 @@ func (v *Validator) doCheckValue(ctx context.Context, in doCheckValueInput) Erro
}

if len(msgArray) > index {
if customMsgMap == nil {
customMsgMap = make(map[string]string)
}
customMsgMap[ruleKey] = strings.TrimSpace(msgArray[index])
}

Expand Down
Loading
Loading