// Copyright GoFrame Author(https://goframe.org). 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 gvalid implements powerful and useful data/form validation functionality. package gvalid import ( "context" "regexp" "strings" "github.com/gogf/gf/text/gregex" ) // Refer to Laravel validation: // https://laravel.com/docs/5.5/validation#available-validation-rules // https://learnku.com/docs/laravel/5.4/validation // // All supported rules: // required format: required brief: Required. // required-if format: required-if:field,value,... brief: Required unless all given field and its value are equal. // required-unless format: required-unless:field,value,... brief: Required unless all given field and its value are not equal. // required-with format: required-with:field1,field2,... brief: Required if any of given fields are not empty. // required-with-all format: required-with-all:field1,field2,... brief: Required if all given fields are not empty. // required-without format: required-without:field1,field2,... brief: Required if any of given fields are empty. // required-without-all format: required-without-all:field1,field2,...brief: Required if all given fields are empty. // bail format: bail brief: Stop validating when this field's validation failed. // date format: date brief: Standard date, like: 2006-01-02, 20060102, 2006.01.02 // date-format format: date-format:format brief: Custom date format. // email format: email brief: Email address. // phone format: phone brief: Phone number. // telephone format: telephone brief: Telephone number, like: "XXXX-XXXXXXX"、"XXXX-XXXXXXXX"、"XXX-XXXXXXX"、"XXX-XXXXXXXX"、"XXXXXXX"、"XXXXXXXX" // passport format: passport brief: Universal passport format rule: Starting with letter, containing only numbers or underscores, length between 6 and 18 // password format: password brief: Universal password format rule1: Containing any visible chars, length between 6 and 18. // password2 format: password2 brief: Universal password format rule2: Must meet password rule1, must contain lower and upper letters and numbers. // password3 format: password3 brief: Universal password format rule3: Must meet password rule1, must contain lower and upper letters, numbers and special chars. // postcode format: postcode brief: Postcode number. // resident-id format: resident-id brief: Resident id number. // bank-card format: bank-card brief: Bank card nunber. // qq format: qq brief: Tencent QQ number. // ip format: ip brief: IPv4/IPv6. // ipv4 format: ipv4 brief: IPv4. // ipv6 format: ipv6 brief: IPv6. // mac format: mac brief: MAC. // url format: url brief: URL. // domain format: domain brief: Domain. // length format: length:min,max brief: Length between :min and :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. // min-length format: min-length:min brief: Length is equal or greater than :min. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. // max-length format: max-length:max brief: Length is equal or lesser than :max. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. // size format: size:size brief: Length must be :size. The length is calculated using unicode string, which means one chinese character or letter both has the length of 1. // between format: between:min,max brief: Range between :min and :max. It supports both integer and float. // min format: min:min brief: Equal or greater than :min. It supports both integer and float. // max format: max:max brief: Equal or lesser than :max. It supports both integer and float. // json format: json brief: JSON. // integer format: integer brief: Integer. // float format: float brief: Float. Note that an integer is actually a float number. // boolean format: boolean brief: Boolean(1,true,on,yes:true | 0,false,off,no,"":false) // same format: same:field brief: Value should be the same as value of field. // different format: different:field brief: Value should be different from value of field. // in format: in:value1,value2,... brief: Value should be in: value1,value2,... // not-in format: not-in:value1,value2,... brief: Value should not be in: value1,value2,... // regex format: regex:pattern brief: Value should match custom regular expression pattern. // CustomMsg is the custom error message type, // like: map[field] => string|map[rule]string type CustomMsg = map[string]interface{} // fieldRule defined the alias name and rule string for specified field. type fieldRule struct { Name string // Alias name for the field. Rule string // Rule string like: "max:6" } // apiNoValidation is an interface that marks current struct not validated by package `gvalid`. type apiNoValidation interface { NoValidation() } 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 = "nv" // no validation tag name for struct attribute. bailRuleName = "bail" // the name for rule "bail" ) var ( defaultValidator = New() // defaultValidator is the default validator for package functions. structTagPriority = []string{"gvalid", "valid", "v"} // structTagPriority specifies the validation tag priority array. aliasNameTagPriority = []string{"param", "params", "p"} // aliasNameTagPriority specifies the alias tag priority array. // all internal error keys. internalErrKeyMap = map[string]string{ internalRulesErrRuleName: internalRulesErrRuleName, internalParamsErrRuleName: internalParamsErrRuleName, internalObjectErrRuleName: internalObjectErrRuleName, } // regular expression object for single rule // which is compiled just once and of repeatable usage. ruleRegex, _ = regexp.Compile(singleRulePattern) // mustCheckRulesEvenValueEmpty specifies some rules that must be validated // even the value is empty (nil or empty). mustCheckRulesEvenValueEmpty = map[string]struct{}{ "required": {}, "required-if": {}, "required-unless": {}, "required-with": {}, "required-with-all": {}, "required-without": {}, "required-without-all": {}, //"same": {}, //"different": {}, //"in": {}, //"not-in": {}, //"regex": {}, } // allSupportedRules defines all supported rules that is used for quick checks. allSupportedRules = map[string]struct{}{ "required": {}, "required-if": {}, "required-unless": {}, "required-with": {}, "required-with-all": {}, "required-without": {}, "required-without-all": {}, "bail": {}, "date": {}, "date-format": {}, "email": {}, "phone": {}, "phone-loose": {}, "telephone": {}, "passport": {}, "password": {}, "password2": {}, "password3": {}, "postcode": {}, "resident-id": {}, "bank-card": {}, "qq": {}, "ip": {}, "ipv4": {}, "ipv6": {}, "mac": {}, "url": {}, "domain": {}, "length": {}, "min-length": {}, "max-length": {}, "size": {}, "between": {}, "min": {}, "max": {}, "json": {}, "integer": {}, "float": {}, "boolean": {}, "same": {}, "different": {}, "in": {}, "not-in": {}, "regex": {}, } // boolMap defines the boolean values. boolMap = map[string]struct{}{ "1": {}, "true": {}, "on": {}, "yes": {}, "": {}, "0": {}, "false": {}, "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", "size": "The :attribute value length must be :size", "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", } // markedRuleMap defines all rules that are just marked rules which have neither functional meaning // nor error messages. markedRuleMap = map[string]bool{ bailRuleName: true, //"nullable": true, } ) // CheckValue checks single value with specified rules. // It returns nil if successful validation. // // The parameter `value` can be any type of variable, which will be converted to string // for validation. // The parameter `rules` can be one or more rules, multiple rules joined using char '|'. // The parameter `messages` specifies the custom error messages, which can be type of: // string/map/struct/*struct. // The optional parameter `params` specifies the extra validation parameters for some rules // like: required-*、same、different, etc. func CheckValue(ctx context.Context, value interface{}, rules string, messages interface{}, params ...interface{}) Error { var data interface{} if len(params) > 0 { data = params[0] } return defaultValidator.Ctx(ctx).Rules(rules).Data(data).Messages(messages).CheckValue(value) } // CheckMap validates map and returns the error result. It returns nil if with successful validation. // // The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result // if `rules` is type of []string. // The optional parameter `messages` specifies the custom error messages for specified keys and rules. func CheckMap(ctx context.Context, params interface{}, rules interface{}, messages ...CustomMsg) Error { var customErrorMessages CustomMsg if len(messages) > 0 { customErrorMessages = messages[0] } return defaultValidator.Ctx(ctx).Rules(rules).Messages(customErrorMessages).CheckMap(params) } // CheckStruct validates struct and returns the error result. // // The parameter `object` should be type of struct/*struct. // The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result // if `rules` is type of []string. // The optional parameter `messages` specifies the custom error messages for specified keys and rules. func CheckStruct(ctx context.Context, object interface{}, rules interface{}, messages ...CustomMsg) Error { var customErrorMessages CustomMsg if len(messages) > 0 { customErrorMessages = messages[0] } return defaultValidator.Ctx(ctx).Rules(rules).Messages(customErrorMessages).CheckStruct(object) } // CheckStructWithData validates struct with given parameter map and returns the error result. // // The parameter `object` should be type of struct/*struct. // The parameter `rules` can be type of []string/map[string]string. It supports sequence in error result // if `rules` is type of []string. // The optional parameter `messages` specifies the custom error messages for specified keys and rules. func CheckStructWithData(ctx context.Context, object interface{}, data interface{}, rules interface{}, messages ...CustomMsg) Error { var customErrorMessages CustomMsg if len(messages) > 0 { customErrorMessages = messages[0] } return defaultValidator.Ctx(ctx).Data(data).Rules(rules).Messages(customErrorMessages).CheckStruct(object) } // parseSequenceTag parses one sequence tag to field, rule and error message. // The sequence tag is like: [alias@]rule[...#msg...] func parseSequenceTag(tag string) (field, rule, msg string) { // Complete sequence tag. // Example: name@required|length:2,20|password3|same:password1#||密码强度不足|两次密码不一致 match, _ := gregex.MatchString(`\s*((\w+)\s*@){0,1}\s*([^#]+)\s*(#\s*(.*)){0,1}\s*`, tag) return strings.TrimSpace(match[2]), strings.TrimSpace(match[3]), strings.TrimSpace(match[5]) }