1025 lines
26 KiB
Go
1025 lines
26 KiB
Go
|
// Package govalidator is package of validators and sanitizers for strings, structs and collections.
|
||
|
package govalidator
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"net"
|
||
|
"net/url"
|
||
|
"reflect"
|
||
|
"regexp"
|
||
|
"sort"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"time"
|
||
|
"unicode"
|
||
|
"unicode/utf8"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
fieldsRequiredByDefault bool
|
||
|
notNumberRegexp = regexp.MustCompile("[^0-9]+")
|
||
|
whiteSpacesAndMinus = regexp.MustCompile("[\\s-]+")
|
||
|
)
|
||
|
|
||
|
const maxURLRuneCount = 2083
|
||
|
const minURLRuneCount = 3
|
||
|
|
||
|
// SetFieldsRequiredByDefault causes validation to fail when struct fields
|
||
|
// do not include validations or are not explicitly marked as exempt (using `valid:"-"` or `valid:"email,optional"`).
|
||
|
// This struct definition will fail govalidator.ValidateStruct() (and the field values do not matter):
|
||
|
// type exampleStruct struct {
|
||
|
// Name string ``
|
||
|
// Email string `valid:"email"`
|
||
|
// This, however, will only fail when Email is empty or an invalid email address:
|
||
|
// type exampleStruct2 struct {
|
||
|
// Name string `valid:"-"`
|
||
|
// Email string `valid:"email"`
|
||
|
// Lastly, this will only fail when Email is an invalid email address but not when it's empty:
|
||
|
// type exampleStruct2 struct {
|
||
|
// Name string `valid:"-"`
|
||
|
// Email string `valid:"email,optional"`
|
||
|
func SetFieldsRequiredByDefault(value bool) {
|
||
|
fieldsRequiredByDefault = value
|
||
|
}
|
||
|
|
||
|
// IsEmail check if the string is an email.
|
||
|
func IsEmail(str string) bool {
|
||
|
// TODO uppercase letters are not supported
|
||
|
return rxEmail.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsURL check if the string is an URL.
|
||
|
func IsURL(str string) bool {
|
||
|
if str == "" || utf8.RuneCountInString(str) >= maxURLRuneCount || len(str) <= minURLRuneCount || strings.HasPrefix(str, ".") {
|
||
|
return false
|
||
|
}
|
||
|
u, err := url.Parse(str)
|
||
|
if err != nil {
|
||
|
return false
|
||
|
}
|
||
|
if strings.HasPrefix(u.Host, ".") {
|
||
|
return false
|
||
|
}
|
||
|
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
|
||
|
return false
|
||
|
}
|
||
|
return rxURL.MatchString(str)
|
||
|
|
||
|
}
|
||
|
|
||
|
// IsRequestURL check if the string rawurl, assuming
|
||
|
// it was received in an HTTP request, is a valid
|
||
|
// URL confirm to RFC 3986
|
||
|
func IsRequestURL(rawurl string) bool {
|
||
|
url, err := url.ParseRequestURI(rawurl)
|
||
|
if err != nil {
|
||
|
return false //Couldn't even parse the rawurl
|
||
|
}
|
||
|
if len(url.Scheme) == 0 {
|
||
|
return false //No Scheme found
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// IsRequestURI check if the string rawurl, assuming
|
||
|
// it was received in an HTTP request, is an
|
||
|
// absolute URI or an absolute path.
|
||
|
func IsRequestURI(rawurl string) bool {
|
||
|
_, err := url.ParseRequestURI(rawurl)
|
||
|
return err == nil
|
||
|
}
|
||
|
|
||
|
// IsAlpha check if the string contains only letters (a-zA-Z). Empty string is valid.
|
||
|
func IsAlpha(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxAlpha.MatchString(str)
|
||
|
}
|
||
|
|
||
|
//IsUTFLetter check if the string contains only unicode letter characters.
|
||
|
//Similar to IsAlpha but for all languages. Empty string is valid.
|
||
|
func IsUTFLetter(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
for _, c := range str {
|
||
|
if !unicode.IsLetter(c) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
|
||
|
}
|
||
|
|
||
|
// IsAlphanumeric check if the string contains only letters and numbers. Empty string is valid.
|
||
|
func IsAlphanumeric(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxAlphanumeric.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsUTFLetterNumeric check if the string contains only unicode letters and numbers. Empty string is valid.
|
||
|
func IsUTFLetterNumeric(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
for _, c := range str {
|
||
|
if !unicode.IsLetter(c) && !unicode.IsNumber(c) { //letters && numbers are ok
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
|
||
|
}
|
||
|
|
||
|
// IsNumeric check if the string contains only numbers. Empty string is valid.
|
||
|
func IsNumeric(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxNumeric.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsUTFNumeric check if the string contains only unicode numbers of any kind.
|
||
|
// Numbers can be 0-9 but also Fractions ¾,Roman Ⅸ and Hangzhou 〩. Empty string is valid.
|
||
|
func IsUTFNumeric(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
if strings.IndexAny(str, "+-") > 0 {
|
||
|
return false
|
||
|
}
|
||
|
if len(str) > 1 {
|
||
|
str = strings.TrimPrefix(str, "-")
|
||
|
str = strings.TrimPrefix(str, "+")
|
||
|
}
|
||
|
for _, c := range str {
|
||
|
if unicode.IsNumber(c) == false { //numbers && minus sign are ok
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
|
||
|
}
|
||
|
|
||
|
// IsUTFDigit check if the string contains only unicode radix-10 decimal digits. Empty string is valid.
|
||
|
func IsUTFDigit(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
if strings.IndexAny(str, "+-") > 0 {
|
||
|
return false
|
||
|
}
|
||
|
if len(str) > 1 {
|
||
|
str = strings.TrimPrefix(str, "-")
|
||
|
str = strings.TrimPrefix(str, "+")
|
||
|
}
|
||
|
for _, c := range str {
|
||
|
if !unicode.IsDigit(c) { //digits && minus sign are ok
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
|
||
|
}
|
||
|
|
||
|
// IsHexadecimal check if the string is a hexadecimal number.
|
||
|
func IsHexadecimal(str string) bool {
|
||
|
return rxHexadecimal.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsHexcolor check if the string is a hexadecimal color.
|
||
|
func IsHexcolor(str string) bool {
|
||
|
return rxHexcolor.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsRGBcolor check if the string is a valid RGB color in form rgb(RRR, GGG, BBB).
|
||
|
func IsRGBcolor(str string) bool {
|
||
|
return rxRGBcolor.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsLowerCase check if the string is lowercase. Empty string is valid.
|
||
|
func IsLowerCase(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return str == strings.ToLower(str)
|
||
|
}
|
||
|
|
||
|
// IsUpperCase check if the string is uppercase. Empty string is valid.
|
||
|
func IsUpperCase(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return str == strings.ToUpper(str)
|
||
|
}
|
||
|
|
||
|
// IsInt check if the string is an integer. Empty string is valid.
|
||
|
func IsInt(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxInt.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsFloat check if the string is a float.
|
||
|
func IsFloat(str string) bool {
|
||
|
return str != "" && rxFloat.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsDivisibleBy check if the string is a number that's divisible by another.
|
||
|
// If second argument is not valid integer or zero, it's return false.
|
||
|
// Otherwise, if first argument is not valid integer or zero, it's return true (Invalid string converts to zero).
|
||
|
func IsDivisibleBy(str, num string) bool {
|
||
|
f, _ := ToFloat(str)
|
||
|
p := int64(f)
|
||
|
q, _ := ToInt(num)
|
||
|
if q == 0 {
|
||
|
return false
|
||
|
}
|
||
|
return (p == 0) || (p%q == 0)
|
||
|
}
|
||
|
|
||
|
// IsNull check if the string is null.
|
||
|
func IsNull(str string) bool {
|
||
|
return len(str) == 0
|
||
|
}
|
||
|
|
||
|
// IsByteLength check if the string's length (in bytes) falls in a range.
|
||
|
func IsByteLength(str string, min, max int) bool {
|
||
|
return len(str) >= min && len(str) <= max
|
||
|
}
|
||
|
|
||
|
// IsUUIDv3 check if the string is a UUID version 3.
|
||
|
func IsUUIDv3(str string) bool {
|
||
|
return rxUUID3.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsUUIDv4 check if the string is a UUID version 4.
|
||
|
func IsUUIDv4(str string) bool {
|
||
|
return rxUUID4.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsUUIDv5 check if the string is a UUID version 5.
|
||
|
func IsUUIDv5(str string) bool {
|
||
|
return rxUUID5.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsUUID check if the string is a UUID (version 3, 4 or 5).
|
||
|
func IsUUID(str string) bool {
|
||
|
return rxUUID.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsCreditCard check if the string is a credit card.
|
||
|
func IsCreditCard(str string) bool {
|
||
|
sanitized := notNumberRegexp.ReplaceAllString(str, "")
|
||
|
if !rxCreditCard.MatchString(sanitized) {
|
||
|
return false
|
||
|
}
|
||
|
var sum int64
|
||
|
var digit string
|
||
|
var tmpNum int64
|
||
|
var shouldDouble bool
|
||
|
for i := len(sanitized) - 1; i >= 0; i-- {
|
||
|
digit = sanitized[i:(i + 1)]
|
||
|
tmpNum, _ = ToInt(digit)
|
||
|
if shouldDouble {
|
||
|
tmpNum *= 2
|
||
|
if tmpNum >= 10 {
|
||
|
sum += ((tmpNum % 10) + 1)
|
||
|
} else {
|
||
|
sum += tmpNum
|
||
|
}
|
||
|
} else {
|
||
|
sum += tmpNum
|
||
|
}
|
||
|
shouldDouble = !shouldDouble
|
||
|
}
|
||
|
|
||
|
if sum%10 == 0 {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// IsISBN10 check if the string is an ISBN version 10.
|
||
|
func IsISBN10(str string) bool {
|
||
|
return IsISBN(str, 10)
|
||
|
}
|
||
|
|
||
|
// IsISBN13 check if the string is an ISBN version 13.
|
||
|
func IsISBN13(str string) bool {
|
||
|
return IsISBN(str, 13)
|
||
|
}
|
||
|
|
||
|
// IsISBN check if the string is an ISBN (version 10 or 13).
|
||
|
// If version value is not equal to 10 or 13, it will be check both variants.
|
||
|
func IsISBN(str string, version int) bool {
|
||
|
sanitized := whiteSpacesAndMinus.ReplaceAllString(str, "")
|
||
|
var checksum int32
|
||
|
var i int32
|
||
|
if version == 10 {
|
||
|
if !rxISBN10.MatchString(sanitized) {
|
||
|
return false
|
||
|
}
|
||
|
for i = 0; i < 9; i++ {
|
||
|
checksum += (i + 1) * int32(sanitized[i]-'0')
|
||
|
}
|
||
|
if sanitized[9] == 'X' {
|
||
|
checksum += 10 * 10
|
||
|
} else {
|
||
|
checksum += 10 * int32(sanitized[9]-'0')
|
||
|
}
|
||
|
if checksum%11 == 0 {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
} else if version == 13 {
|
||
|
if !rxISBN13.MatchString(sanitized) {
|
||
|
return false
|
||
|
}
|
||
|
factor := []int32{1, 3}
|
||
|
for i = 0; i < 12; i++ {
|
||
|
checksum += factor[i%2] * int32(sanitized[i]-'0')
|
||
|
}
|
||
|
if (int32(sanitized[12]-'0'))-((10-(checksum%10))%10) == 0 {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
return IsISBN(str, 10) || IsISBN(str, 13)
|
||
|
}
|
||
|
|
||
|
// IsJSON check if the string is valid JSON (note: uses json.Unmarshal).
|
||
|
func IsJSON(str string) bool {
|
||
|
var js json.RawMessage
|
||
|
return json.Unmarshal([]byte(str), &js) == nil
|
||
|
}
|
||
|
|
||
|
// IsMultibyte check if the string contains one or more multibyte chars. Empty string is valid.
|
||
|
func IsMultibyte(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxMultibyte.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsASCII check if the string contains ASCII chars only. Empty string is valid.
|
||
|
func IsASCII(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxASCII.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsPrintableASCII check if the string contains printable ASCII chars only. Empty string is valid.
|
||
|
func IsPrintableASCII(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxPrintableASCII.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsFullWidth check if the string contains any full-width chars. Empty string is valid.
|
||
|
func IsFullWidth(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxFullWidth.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsHalfWidth check if the string contains any half-width chars. Empty string is valid.
|
||
|
func IsHalfWidth(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxHalfWidth.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsVariableWidth check if the string contains a mixture of full and half-width chars. Empty string is valid.
|
||
|
func IsVariableWidth(str string) bool {
|
||
|
if IsNull(str) {
|
||
|
return true
|
||
|
}
|
||
|
return rxHalfWidth.MatchString(str) && rxFullWidth.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsBase64 check if a string is base64 encoded.
|
||
|
func IsBase64(str string) bool {
|
||
|
return rxBase64.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsFilePath check is a string is Win or Unix file path and returns it's type.
|
||
|
func IsFilePath(str string) (bool, int) {
|
||
|
if rxWinPath.MatchString(str) {
|
||
|
//check windows path limit see:
|
||
|
// http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx#maxpath
|
||
|
if len(str[3:]) > 32767 {
|
||
|
return false, Win
|
||
|
}
|
||
|
return true, Win
|
||
|
} else if rxUnixPath.MatchString(str) {
|
||
|
return true, Unix
|
||
|
}
|
||
|
return false, Unknown
|
||
|
}
|
||
|
|
||
|
// IsDataURI checks if a string is base64 encoded data URI such as an image
|
||
|
func IsDataURI(str string) bool {
|
||
|
dataURI := strings.Split(str, ",")
|
||
|
if !rxDataURI.MatchString(dataURI[0]) {
|
||
|
return false
|
||
|
}
|
||
|
return IsBase64(dataURI[1])
|
||
|
}
|
||
|
|
||
|
// IsISO3166Alpha2 checks if a string is valid two-letter country code
|
||
|
func IsISO3166Alpha2(str string) bool {
|
||
|
for _, entry := range ISO3166List {
|
||
|
if str == entry.Alpha2Code {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// IsISO3166Alpha3 checks if a string is valid three-letter country code
|
||
|
func IsISO3166Alpha3(str string) bool {
|
||
|
for _, entry := range ISO3166List {
|
||
|
if str == entry.Alpha3Code {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// IsDNSName will validate the given string as a DNS name
|
||
|
func IsDNSName(str string) bool {
|
||
|
if str == "" || len(strings.Replace(str, ".", "", -1)) > 255 {
|
||
|
// constraints already violated
|
||
|
return false
|
||
|
}
|
||
|
return !IsIP(str) && rxDNSName.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsDialString validates the given string for usage with the various Dial() functions
|
||
|
func IsDialString(str string) bool {
|
||
|
|
||
|
if h, p, err := net.SplitHostPort(str); err == nil && h != "" && p != "" && (IsDNSName(h) || IsIP(h)) && IsPort(p) {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// IsIP checks if a string is either IP version 4 or 6.
|
||
|
func IsIP(str string) bool {
|
||
|
return net.ParseIP(str) != nil
|
||
|
}
|
||
|
|
||
|
// IsPort checks if a string represents a valid port
|
||
|
func IsPort(str string) bool {
|
||
|
if i, err := strconv.Atoi(str); err == nil && i > 0 && i < 65536 {
|
||
|
return true
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// IsIPv4 check if the string is an IP version 4.
|
||
|
func IsIPv4(str string) bool {
|
||
|
ip := net.ParseIP(str)
|
||
|
return ip != nil && strings.Contains(str, ".")
|
||
|
}
|
||
|
|
||
|
// IsIPv6 check if the string is an IP version 6.
|
||
|
func IsIPv6(str string) bool {
|
||
|
ip := net.ParseIP(str)
|
||
|
return ip != nil && strings.Contains(str, ":")
|
||
|
}
|
||
|
|
||
|
// IsCIDR check if the string is an valid CIDR notiation (IPV4 & IPV6)
|
||
|
func IsCIDR(str string) bool {
|
||
|
_, _, err := net.ParseCIDR(str)
|
||
|
return err == nil
|
||
|
}
|
||
|
|
||
|
// IsMAC check if a string is valid MAC address.
|
||
|
// Possible MAC formats:
|
||
|
// 01:23:45:67:89:ab
|
||
|
// 01:23:45:67:89:ab:cd:ef
|
||
|
// 01-23-45-67-89-ab
|
||
|
// 01-23-45-67-89-ab-cd-ef
|
||
|
// 0123.4567.89ab
|
||
|
// 0123.4567.89ab.cdef
|
||
|
func IsMAC(str string) bool {
|
||
|
_, err := net.ParseMAC(str)
|
||
|
return err == nil
|
||
|
}
|
||
|
|
||
|
// IsHost checks if the string is a valid IP (both v4 and v6) or a valid DNS name
|
||
|
func IsHost(str string) bool {
|
||
|
return IsIP(str) || IsDNSName(str)
|
||
|
}
|
||
|
|
||
|
// IsMongoID check if the string is a valid hex-encoded representation of a MongoDB ObjectId.
|
||
|
func IsMongoID(str string) bool {
|
||
|
return rxHexadecimal.MatchString(str) && (len(str) == 24)
|
||
|
}
|
||
|
|
||
|
// IsLatitude check if a string is valid latitude.
|
||
|
func IsLatitude(str string) bool {
|
||
|
return rxLatitude.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsLongitude check if a string is valid longitude.
|
||
|
func IsLongitude(str string) bool {
|
||
|
return rxLongitude.MatchString(str)
|
||
|
}
|
||
|
|
||
|
func toJSONName(tag string) string {
|
||
|
if tag == "" {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// JSON name always comes first. If there's no options then split[0] is
|
||
|
// JSON name, if JSON name is not set, then split[0] is an empty string.
|
||
|
split := strings.SplitN(tag, ",", 2)
|
||
|
return split[0]
|
||
|
}
|
||
|
|
||
|
// ValidateStruct use tags for fields.
|
||
|
// result will be equal to `false` if there are any errors.
|
||
|
func ValidateStruct(s interface{}) (bool, error) {
|
||
|
if s == nil {
|
||
|
return true, nil
|
||
|
}
|
||
|
result := true
|
||
|
var err error
|
||
|
val := reflect.ValueOf(s)
|
||
|
if val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr {
|
||
|
val = val.Elem()
|
||
|
}
|
||
|
// we only accept structs
|
||
|
if val.Kind() != reflect.Struct {
|
||
|
return false, fmt.Errorf("function only accepts structs; got %s", val.Kind())
|
||
|
}
|
||
|
var errs Errors
|
||
|
for i := 0; i < val.NumField(); i++ {
|
||
|
valueField := val.Field(i)
|
||
|
typeField := val.Type().Field(i)
|
||
|
if typeField.PkgPath != "" {
|
||
|
continue // Private field
|
||
|
}
|
||
|
structResult := true
|
||
|
if valueField.Kind() == reflect.Struct {
|
||
|
var err error
|
||
|
structResult, err = ValidateStruct(valueField.Interface())
|
||
|
if err != nil {
|
||
|
errs = append(errs, err)
|
||
|
}
|
||
|
}
|
||
|
resultField, err2 := typeCheck(valueField, typeField, val, nil)
|
||
|
if err2 != nil {
|
||
|
|
||
|
// Replace structure name with JSON name if there is a tag on the variable
|
||
|
jsonTag := toJSONName(typeField.Tag.Get("json"))
|
||
|
if jsonTag != "" {
|
||
|
switch jsonError := err2.(type) {
|
||
|
case Error:
|
||
|
jsonError.Name = jsonTag
|
||
|
err2 = jsonError
|
||
|
case Errors:
|
||
|
err2 = jsonError
|
||
|
}
|
||
|
}
|
||
|
|
||
|
errs = append(errs, err2)
|
||
|
}
|
||
|
result = result && resultField && structResult
|
||
|
}
|
||
|
if len(errs) > 0 {
|
||
|
err = errs
|
||
|
}
|
||
|
return result, err
|
||
|
}
|
||
|
|
||
|
// parseTagIntoMap parses a struct tag `valid:required~Some error message,length(2|3)` into map[string]string{"required": "Some error message", "length(2|3)": ""}
|
||
|
func parseTagIntoMap(tag string) tagOptionsMap {
|
||
|
optionsMap := make(tagOptionsMap)
|
||
|
options := strings.SplitN(tag, ",", -1)
|
||
|
for _, option := range options {
|
||
|
validationOptions := strings.Split(option, "~")
|
||
|
if !isValidTag(validationOptions[0]) {
|
||
|
continue
|
||
|
}
|
||
|
if len(validationOptions) == 2 {
|
||
|
optionsMap[validationOptions[0]] = validationOptions[1]
|
||
|
} else {
|
||
|
optionsMap[validationOptions[0]] = ""
|
||
|
}
|
||
|
}
|
||
|
return optionsMap
|
||
|
}
|
||
|
|
||
|
func isValidTag(s string) bool {
|
||
|
if s == "" {
|
||
|
return false
|
||
|
}
|
||
|
for _, c := range s {
|
||
|
switch {
|
||
|
case strings.ContainsRune("\\'\"!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
|
||
|
// Backslash and quote chars are reserved, but
|
||
|
// otherwise any punctuation chars are allowed
|
||
|
// in a tag name.
|
||
|
default:
|
||
|
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// IsSSN will validate the given string as a U.S. Social Security Number
|
||
|
func IsSSN(str string) bool {
|
||
|
if str == "" || len(str) != 11 {
|
||
|
return false
|
||
|
}
|
||
|
return rxSSN.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsSemver check if string is valid semantic version
|
||
|
func IsSemver(str string) bool {
|
||
|
return rxSemver.MatchString(str)
|
||
|
}
|
||
|
|
||
|
// IsTime check if string is valid according to given format
|
||
|
func IsTime(str string, format string) bool {
|
||
|
_, err := time.Parse(format, str)
|
||
|
return err == nil
|
||
|
}
|
||
|
|
||
|
// IsRFC3339 check if string is valid timestamp value according to RFC3339
|
||
|
func IsRFC3339(str string) bool {
|
||
|
return IsTime(str, time.RFC3339)
|
||
|
}
|
||
|
|
||
|
// IsISO4217 check if string is valid ISO currency code
|
||
|
func IsISO4217(str string) bool {
|
||
|
for _, currency := range ISO4217List {
|
||
|
if str == currency {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// ByteLength check string's length
|
||
|
func ByteLength(str string, params ...string) bool {
|
||
|
if len(params) == 2 {
|
||
|
min, _ := ToInt(params[0])
|
||
|
max, _ := ToInt(params[1])
|
||
|
return len(str) >= int(min) && len(str) <= int(max)
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// RuneLength check string's length
|
||
|
// Alias for StringLength
|
||
|
func RuneLength(str string, params ...string) bool {
|
||
|
return StringLength(str, params...)
|
||
|
}
|
||
|
|
||
|
// StringMatches checks if a string matches a given pattern.
|
||
|
func StringMatches(s string, params ...string) bool {
|
||
|
if len(params) == 1 {
|
||
|
pattern := params[0]
|
||
|
return Matches(s, pattern)
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// StringLength check string's length (including multi byte strings)
|
||
|
func StringLength(str string, params ...string) bool {
|
||
|
|
||
|
if len(params) == 2 {
|
||
|
strLength := utf8.RuneCountInString(str)
|
||
|
min, _ := ToInt(params[0])
|
||
|
max, _ := ToInt(params[1])
|
||
|
return strLength >= int(min) && strLength <= int(max)
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Range check string's length
|
||
|
func Range(str string, params ...string) bool {
|
||
|
if len(params) == 2 {
|
||
|
value, _ := ToFloat(str)
|
||
|
min, _ := ToFloat(params[0])
|
||
|
max, _ := ToFloat(params[1])
|
||
|
return InRange(value, min, max)
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func isInRaw(str string, params ...string) bool {
|
||
|
if len(params) == 1 {
|
||
|
rawParams := params[0]
|
||
|
|
||
|
parsedParams := strings.Split(rawParams, "|")
|
||
|
|
||
|
return IsIn(str, parsedParams...)
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// IsIn check if string str is a member of the set of strings params
|
||
|
func IsIn(str string, params ...string) bool {
|
||
|
for _, param := range params {
|
||
|
if str == param {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func checkRequired(v reflect.Value, t reflect.StructField, options tagOptionsMap) (bool, error) {
|
||
|
if requiredOption, isRequired := options["required"]; isRequired {
|
||
|
if len(requiredOption) > 0 {
|
||
|
return false, Error{t.Name, fmt.Errorf(requiredOption), true}
|
||
|
}
|
||
|
return false, Error{t.Name, fmt.Errorf("non zero value required"), false}
|
||
|
} else if _, isOptional := options["optional"]; fieldsRequiredByDefault && !isOptional {
|
||
|
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false}
|
||
|
}
|
||
|
// not required and empty is valid
|
||
|
return true, nil
|
||
|
}
|
||
|
|
||
|
func typeCheck(v reflect.Value, t reflect.StructField, o reflect.Value, options tagOptionsMap) (isValid bool, resultErr error) {
|
||
|
if !v.IsValid() {
|
||
|
return false, nil
|
||
|
}
|
||
|
|
||
|
tag := t.Tag.Get(tagName)
|
||
|
|
||
|
// Check if the field should be ignored
|
||
|
switch tag {
|
||
|
case "":
|
||
|
if !fieldsRequiredByDefault {
|
||
|
return true, nil
|
||
|
}
|
||
|
return false, Error{t.Name, fmt.Errorf("All fields are required to at least have one validation defined"), false}
|
||
|
case "-":
|
||
|
return true, nil
|
||
|
}
|
||
|
|
||
|
isRootType := false
|
||
|
if options == nil {
|
||
|
isRootType = true
|
||
|
options = parseTagIntoMap(tag)
|
||
|
}
|
||
|
|
||
|
if isEmptyValue(v) {
|
||
|
// an empty value is not validated, check only required
|
||
|
return checkRequired(v, t, options)
|
||
|
}
|
||
|
|
||
|
var customTypeErrors Errors
|
||
|
for validatorName, customErrorMessage := range options {
|
||
|
if validatefunc, ok := CustomTypeTagMap.Get(validatorName); ok {
|
||
|
delete(options, validatorName)
|
||
|
|
||
|
if result := validatefunc(v.Interface(), o.Interface()); !result {
|
||
|
if len(customErrorMessage) > 0 {
|
||
|
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf(customErrorMessage), CustomErrorMessageExists: true})
|
||
|
continue
|
||
|
}
|
||
|
customTypeErrors = append(customTypeErrors, Error{Name: t.Name, Err: fmt.Errorf("%s does not validate as %s", fmt.Sprint(v), validatorName), CustomErrorMessageExists: false})
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(customTypeErrors.Errors()) > 0 {
|
||
|
return false, customTypeErrors
|
||
|
}
|
||
|
|
||
|
if isRootType {
|
||
|
// Ensure that we've checked the value by all specified validators before report that the value is valid
|
||
|
defer func() {
|
||
|
delete(options, "optional")
|
||
|
delete(options, "required")
|
||
|
|
||
|
if isValid && resultErr == nil && len(options) != 0 {
|
||
|
for validator := range options {
|
||
|
isValid = false
|
||
|
resultErr = Error{t.Name, fmt.Errorf(
|
||
|
"The following validator is invalid or can't be applied to the field: %q", validator), false}
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
switch v.Kind() {
|
||
|
case reflect.Bool,
|
||
|
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||
|
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
|
||
|
reflect.Float32, reflect.Float64,
|
||
|
reflect.String:
|
||
|
// for each tag option check the map of validator functions
|
||
|
for validatorSpec, customErrorMessage := range options {
|
||
|
var negate bool
|
||
|
validator := validatorSpec
|
||
|
customMsgExists := len(customErrorMessage) > 0
|
||
|
|
||
|
// Check whether the tag looks like '!something' or 'something'
|
||
|
if validator[0] == '!' {
|
||
|
validator = validator[1:]
|
||
|
negate = true
|
||
|
}
|
||
|
|
||
|
// Check for param validators
|
||
|
for key, value := range ParamTagRegexMap {
|
||
|
ps := value.FindStringSubmatch(validator)
|
||
|
if len(ps) == 0 {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
validatefunc, ok := ParamTagMap[key]
|
||
|
if !ok {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
delete(options, validatorSpec)
|
||
|
|
||
|
switch v.Kind() {
|
||
|
case reflect.String:
|
||
|
field := fmt.Sprint(v) // make value into string, then validate with regex
|
||
|
if result := validatefunc(field, ps[1:]...); (!result && !negate) || (result && negate) {
|
||
|
if customMsgExists {
|
||
|
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists}
|
||
|
}
|
||
|
if negate {
|
||
|
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists}
|
||
|
}
|
||
|
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists}
|
||
|
}
|
||
|
default:
|
||
|
// type not yet supported, fail
|
||
|
return false, Error{t.Name, fmt.Errorf("Validator %s doesn't support kind %s", validator, v.Kind()), false}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if validatefunc, ok := TagMap[validator]; ok {
|
||
|
delete(options, validatorSpec)
|
||
|
|
||
|
switch v.Kind() {
|
||
|
case reflect.String:
|
||
|
field := fmt.Sprint(v) // make value into string, then validate with regex
|
||
|
if result := validatefunc(field); !result && !negate || result && negate {
|
||
|
if customMsgExists {
|
||
|
return false, Error{t.Name, fmt.Errorf(customErrorMessage), customMsgExists}
|
||
|
}
|
||
|
if negate {
|
||
|
return false, Error{t.Name, fmt.Errorf("%s does validate as %s", field, validator), customMsgExists}
|
||
|
}
|
||
|
return false, Error{t.Name, fmt.Errorf("%s does not validate as %s", field, validator), customMsgExists}
|
||
|
}
|
||
|
default:
|
||
|
//Not Yet Supported Types (Fail here!)
|
||
|
err := fmt.Errorf("Validator %s doesn't support kind %s for value %v", validator, v.Kind(), v)
|
||
|
return false, Error{t.Name, err, false}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return true, nil
|
||
|
case reflect.Map:
|
||
|
if v.Type().Key().Kind() != reflect.String {
|
||
|
return false, &UnsupportedTypeError{v.Type()}
|
||
|
}
|
||
|
var sv stringValues
|
||
|
sv = v.MapKeys()
|
||
|
sort.Sort(sv)
|
||
|
result := true
|
||
|
for _, k := range sv {
|
||
|
resultItem, err := ValidateStruct(v.MapIndex(k).Interface())
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
result = result && resultItem
|
||
|
}
|
||
|
return result, nil
|
||
|
case reflect.Slice, reflect.Array:
|
||
|
result := true
|
||
|
for i := 0; i < v.Len(); i++ {
|
||
|
var resultItem bool
|
||
|
var err error
|
||
|
if v.Index(i).Kind() != reflect.Struct {
|
||
|
resultItem, err = typeCheck(v.Index(i), t, o, options)
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
} else {
|
||
|
resultItem, err = ValidateStruct(v.Index(i).Interface())
|
||
|
if err != nil {
|
||
|
return false, err
|
||
|
}
|
||
|
}
|
||
|
result = result && resultItem
|
||
|
}
|
||
|
return result, nil
|
||
|
case reflect.Interface:
|
||
|
// If the value is an interface then encode its element
|
||
|
if v.IsNil() {
|
||
|
return true, nil
|
||
|
}
|
||
|
return ValidateStruct(v.Interface())
|
||
|
case reflect.Ptr:
|
||
|
// If the value is a pointer then check its element
|
||
|
if v.IsNil() {
|
||
|
return true, nil
|
||
|
}
|
||
|
return typeCheck(v.Elem(), t, o, options)
|
||
|
case reflect.Struct:
|
||
|
return ValidateStruct(v.Interface())
|
||
|
default:
|
||
|
return false, &UnsupportedTypeError{v.Type()}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func isEmptyValue(v reflect.Value) bool {
|
||
|
switch v.Kind() {
|
||
|
case reflect.String, reflect.Array:
|
||
|
return v.Len() == 0
|
||
|
case reflect.Map, reflect.Slice:
|
||
|
return v.Len() == 0 || v.IsNil()
|
||
|
case reflect.Bool:
|
||
|
return !v.Bool()
|
||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||
|
return v.Int() == 0
|
||
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||
|
return v.Uint() == 0
|
||
|
case reflect.Float32, reflect.Float64:
|
||
|
return v.Float() == 0
|
||
|
case reflect.Interface, reflect.Ptr:
|
||
|
return v.IsNil()
|
||
|
}
|
||
|
|
||
|
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
|
||
|
}
|
||
|
|
||
|
// ErrorByField returns error for specified field of the struct
|
||
|
// validated by ValidateStruct or empty string if there are no errors
|
||
|
// or this field doesn't exists or doesn't have any errors.
|
||
|
func ErrorByField(e error, field string) string {
|
||
|
if e == nil {
|
||
|
return ""
|
||
|
}
|
||
|
return ErrorsByField(e)[field]
|
||
|
}
|
||
|
|
||
|
// ErrorsByField returns map of errors of the struct validated
|
||
|
// by ValidateStruct or empty map if there are no errors.
|
||
|
func ErrorsByField(e error) map[string]string {
|
||
|
m := make(map[string]string)
|
||
|
if e == nil {
|
||
|
return m
|
||
|
}
|
||
|
// prototype for ValidateStruct
|
||
|
|
||
|
switch e.(type) {
|
||
|
case Error:
|
||
|
m[e.(Error).Name] = e.(Error).Err.Error()
|
||
|
case Errors:
|
||
|
for _, item := range e.(Errors).Errors() {
|
||
|
n := ErrorsByField(item)
|
||
|
for k, v := range n {
|
||
|
m[k] = v
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// Error returns string equivalent for reflect.Type
|
||
|
func (e *UnsupportedTypeError) Error() string {
|
||
|
return "validator: unsupported type: " + e.Type.String()
|
||
|
}
|
||
|
|
||
|
func (sv stringValues) Len() int { return len(sv) }
|
||
|
func (sv stringValues) Swap(i, j int) { sv[i], sv[j] = sv[j], sv[i] }
|
||
|
func (sv stringValues) Less(i, j int) bool { return sv.get(i) < sv.get(j) }
|
||
|
func (sv stringValues) get(i int) string { return sv[i].String() }
|