Repository: go-playground/validator
Pull request #1252 (2024-04-16 12:29:30)

Add EU countries validator 
## Enhances


**Make sure that you've checked the boxes below before you submit PR:**
- [x] Tests exist or have been written that cover this particular change.

@go-playground/validator-maintainers

coveralls: 
[![Coverage Status](https://coveralls.io/builds/66941698/badge)](https://coveralls.io/builds/66941698)

coverage: 74.238% (+0.03%) from 74.208%
when pulling **66c6ec835fae608c47c4e368c1542b975e82351d on masv3971:master**
into **a0f74b0fb2a7ae1750c0f0b0a49550d8b6e2e708 on go-playground:master**.


Patch:
diff --git a/baked_in.go b/baked_in.go
index 95f56e0..f622c62 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -64,8 +64,9 @@ var (
 	// defines a common or complex set of validation(s) to simplify
 	// adding validation to structs.
 	bakedInAliases = map[string]string{
-		"iscolor":      "hexcolor|rgb|rgba|hsl|hsla",
-		"country_code": "iso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric",
+		"iscolor":         "hexcolor|rgb|rgba|hsl|hsla",
+		"country_code":    "iso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric",
+		"eu_country_code": "iso3166_1_alpha2_eu|iso3166_1_alpha3_eu|iso3166_1_alpha_numeric_eu",
 	}
 
 	// bakedInValidators is the default map of ValidationFunc
@@ -133,6 +134,7 @@ var (
 		"urn_rfc2141":                   isUrnRFC2141, // RFC 2141
 		"file":                          isFile,
 		"filepath":                      isFilePath,
+		"base32":                        isBase32,
 		"base64":                        isBase64,
 		"base64url":                     isBase64URL,
 		"base64rawurl":                  isBase64RawURL,
@@ -216,8 +218,11 @@ var (
 		"datetime":                      isDatetime,
 		"timezone":                      isTimeZone,
 		"iso3166_1_alpha2":              isIso3166Alpha2,
+		"iso3166_1_alpha2_eu":           isIso3166Alpha2EU,
 		"iso3166_1_alpha3":              isIso3166Alpha3,
+		"iso3166_1_alpha3_eu":           isIso3166Alpha3EU,
 		"iso3166_1_alpha_numeric":       isIso3166AlphaNumeric,
+		"iso3166_1_alpha_numeric_eu":    isIso3166AlphaNumericEU,
 		"iso3166_2":                     isIso31662,
 		"iso4217":                       isIso4217,
 		"iso4217_numeric":               isIso4217Numeric,
@@ -1399,6 +1404,11 @@ func isPostcodeByIso3166Alpha2Field(fl FieldLevel) bool {
 	return reg.MatchString(field.String())
 }
 
+// isBase32 is the validation function for validating if the current field's value is a valid base 32.
+func isBase32(fl FieldLevel) bool {
+	return base32Regex.MatchString(fl.Field().String())
+}
+
 // isBase64 is the validation function for validating if the current field's value is a valid base 64.
 func isBase64(fl FieldLevel) bool {
 	return base64Regex.MatchString(fl.Field().String())
@@ -2762,12 +2772,24 @@ func isIso3166Alpha2(fl FieldLevel) bool {
 	return iso3166_1_alpha2[val]
 }
 
+// isIso3166Alpha2EU is the validation function for validating if the current field's value is a valid iso3166-1 alpha-2 European Union country code.
+func isIso3166Alpha2EU(fl FieldLevel) bool {
+	val := fl.Field().String()
+	return iso3166_1_alpha2_eu[val]
+}
+
 // isIso3166Alpha3 is the validation function for validating if the current field's value is a valid iso3166-1 alpha-3 country code.
 func isIso3166Alpha3(fl FieldLevel) bool {
 	val := fl.Field().String()
 	return iso3166_1_alpha3[val]
 }
 
+// isIso3166Alpha3EU is the validation function for validating if the current field's value is a valid iso3166-1 alpha-3 European Union country code.
+func isIso3166Alpha3EU(fl FieldLevel) bool {
+	val := fl.Field().String()
+	return iso3166_1_alpha3_eu[val]
+}
+
 // isIso3166AlphaNumeric is the validation function for validating if the current field's value is a valid iso3166-1 alpha-numeric country code.
 func isIso3166AlphaNumeric(fl FieldLevel) bool {
 	field := fl.Field()
@@ -2790,6 +2812,28 @@ func isIso3166AlphaNumeric(fl FieldLevel) bool {
 	return iso3166_1_alpha_numeric[code]
 }
 
+// isIso3166AlphaNumericEU is the validation function for validating if the current field's value is a valid iso3166-1 alpha-numeric European Union country code.
+func isIso3166AlphaNumericEU(fl FieldLevel) bool {
+	field := fl.Field()
+
+	var code int
+	switch field.Kind() {
+	case reflect.String:
+		i, err := strconv.Atoi(field.String())
+		if err != nil {
+			return false
+		}
+		code = i % 1000
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		code = int(field.Int() % 1000)
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		code = int(field.Uint() % 1000)
+	default:
+		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	}
+	return iso3166_1_alpha_numeric_eu[code]
+}
+
 // isIso31662 is the validation function for validating if the current field's value is a valid iso3166-2 code.
 func isIso31662(fl FieldLevel) bool {
 	val := fl.Field().String()
diff --git a/country_codes.go b/country_codes.go
index 0119f05..e507149 100644
--- a/country_codes.go
+++ b/country_codes.go
@@ -54,6 +54,15 @@ var iso3166_1_alpha2 = map[string]bool{
 	"EH": true, "YE": true, "ZM": true, "ZW": true, "XK": true,
 }
 
+var iso3166_1_alpha2_eu = map[string]bool{
+	"AT": true, "BE": true, "BG": true, "HR": true, "CY": true,
+	"CZ": true, "DK": true, "EE": true, "FI": true, "FR": true,
+	"DE": true, "GR": true, "HU": true, "IE": true, "IT": true,
+	"LV": true, "LT": true, "LU": true, "MT": true, "NL": true,
+	"PL": true, "PT": true, "RO": true, "SK": true, "SI": true,
+	"ES": true, "SE": true,
+}
+
 var iso3166_1_alpha3 = map[string]bool{
 	// see: https://www.iso.org/iso-3166-country-codes.html
 	"AFG": true, "ALB": true, "DZA": true, "ASM": true, "AND": true,
@@ -107,6 +116,15 @@ var iso3166_1_alpha3 = map[string]bool{
 	"VNM": true, "VGB": true, "VIR": true, "WLF": true, "ESH": true,
 	"YEM": true, "ZMB": true, "ZWE": true, "ALA": true, "UNK": true,
 }
+
+var iso3166_1_alpha3_eu = map[string]bool{
+	"AUT": true, "BEL": true, "BGR": true, "HRV": true, "CYP": true,
+	"CZE": true, "DNK": true, "EST": true, "FIN": true, "FRA": true,
+	"DEU": true, "GRC": true, "HUN": true, "IRL": true, "ITA": true,
+	"LVA": true, "LTU": true, "LUX": true, "MLT": true, "NLD": true,
+	"POL": true, "PRT": true, "ROU": true, "SVK": true, "SVN": true,
+	"ESP": true, "SWE": true,
+}
 var iso3166_1_alpha_numeric = map[int]bool{
 	// see: https://www.iso.org/iso-3166-country-codes.html
 	4: true, 8: true, 12: true, 16: true, 20: true,
@@ -161,6 +179,15 @@ var iso3166_1_alpha_numeric = map[int]bool{
 	887: true, 894: true, 716: true, 248: true, 153: true,
 }
 
+var iso3166_1_alpha_numeric_eu = map[int]bool{
+	40: true, 56: true, 100: true, 191: true, 196: true,
+	200: true, 208: true, 233: true, 246: true, 250: true,
+	276: true, 300: true, 348: true, 372: true, 380: true,
+	428: true, 440: true, 442: true, 470: true, 528: true,
+	616: true, 620: true, 642: true, 703: true, 705: true,
+	724: true, 752: true,
+}
+
 var iso3166_2 = map[string]bool{
 	"AD-02": true, "AD-03": true, "AD-04": true, "AD-05": true, "AD-06": true,
 	"AD-07": true, "AD-08": true, "AE-AJ": true, "AE-AZ": true, "AE-DU": true,
diff --git a/doc.go b/doc.go
index b474091..2e8092a 100644
--- a/doc.go
+++ b/doc.go
@@ -916,6 +916,15 @@ according to the RFC 2141 spec.
 
 	Usage: urn_rfc2141
 
+# Base32 String
+
+This validates that a string value contains a valid bas324 value.
+Although an empty string is valid base32 this will report an empty string
+as an error, if you wish to accept an empty string as valid you can use
+this with the omitempty tag.
+
+	Usage: base32
+
 # Base64 String
 
 This validates that a string value contains a valid base64 value.
diff --git a/regexes.go b/regexes.go
index af98d8d..f39a466 100644
--- a/regexes.go
+++ b/regexes.go
@@ -17,6 +17,7 @@ const (
 	hslaRegexString                  = "^hsla\\(\\s*(?:0|[1-9]\\d?|[12]\\d\\d|3[0-5]\\d|360)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0|[1-9]\\d?|100)%)\\s*,\\s*(?:(?:0.[1-9]*)|[01])\\s*\\)$"
 	emailRegexString                 = "^(?:(?:(?:(?:[a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(?:\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|(?:(?:\\x22)(?:(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(?:\\x20|\\x09)+)?(?:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(?:(?:(?:\\x20|\\x09)*(?:\\x0d\\x0a))?(\\x20|\\x09)+)?(?:\\x22))))@(?:(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(?:(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])(?:[a-zA-Z]|\\d|-|\\.|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*(?:[a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
 	e164RegexString                  = "^\\+[1-9]?[0-9]{7,14}$"
+	base32RegexString                = "^(?:[A-Z2-7]{8})*(?:[A-Z2-7]{2}={6}|[A-Z2-7]{4}={4}|[A-Z2-7]{5}={3}|[A-Z2-7]{7}=|[A-Z2-7]{8})$"
 	base64RegexString                = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
 	base64URLRegexString             = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2}==|[A-Za-z0-9-_]{3}=|[A-Za-z0-9-_]{4})$"
 	base64RawURLRegexString          = "^(?:[A-Za-z0-9-_]{4})*(?:[A-Za-z0-9-_]{2,4})$"
@@ -31,7 +32,7 @@ const (
 	uUID4RFC4122RegexString          = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
 	uUID5RFC4122RegexString          = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-5[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$"
 	uUIDRFC4122RegexString           = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
-	uLIDRegexString                  = "^[A-HJKMNP-TV-Z0-9]{26}$"
+	uLIDRegexString                  = "^(?i)[A-HJKMNP-TV-Z0-9]{26}$"
 	md4RegexString                   = "^[0-9a-f]{32}$"
 	md5RegexString                   = "^[0-9a-f]{32}$"
 	sha256RegexString                = "^[0-9a-f]{64}$"
@@ -89,6 +90,7 @@ var (
 	hslaRegex                  = regexp.MustCompile(hslaRegexString)
 	e164Regex                  = regexp.MustCompile(e164RegexString)
 	emailRegex                 = regexp.MustCompile(emailRegexString)
+	base32Regex                = regexp.MustCompile(base32RegexString)
 	base64Regex                = regexp.MustCompile(base64RegexString)
 	base64URLRegex             = regexp.MustCompile(base64URLRegexString)
 	base64RawURLRegex          = regexp.MustCompile(base64RawURLRegexString)


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 3b6d263..ffff4b0 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -4338,7 +4338,7 @@ func TestULIDValidation(t *testing.T) {
 	}{
 		{"", false},
 		{"01BX5ZZKBKACT-V9WEVGEMMVRZ", false},
-		{"01bx5zzkbkactav9wevgemmvrz", false},
+		{"01bx5zzkbkactav9wevgemmvrz", true},
 		{"a987Fbc9-4bed-3078-cf07-9141ba07c9f3xxx", false},
 		{"01BX5ZZKBKACTAV9WEVGEMMVRZABC", false},
 		{"01BX5ZZKBKACTAV9WEVGEMMVRZABC", false},
@@ -5680,6 +5680,40 @@ func TestOneOfValidation(t *testing.T) {
 	}, "Bad field type float64")
 }
 
+func TestBase32Validation(t *testing.T) {
+	validate := New()
+
+	s := "ABCD2345"
+	errs := validate.Var(s, "base32")
+	Equal(t, errs, nil)
+
+	s = "AB======"
+	errs = validate.Var(s, "base32")
+	Equal(t, errs, nil)
+
+	s = "ABCD2==="
+	errs = validate.Var(s, "base32")
+	Equal(t, errs, nil)
+
+	s = "ABCD===="
+	errs = validate.Var(s, "base32")
+	Equal(t, errs, nil)
+
+	s = "ABCD234="
+	errs = validate.Var(s, "base32")
+	Equal(t, errs, nil)
+
+	s = ""
+	errs = validate.Var(s, "base32")
+	NotEqual(t, errs, nil)
+	AssertError(t, errs, "", "", "", "", "base32")
+
+	s = "ABCabc1890== foo bar"
+	errs = validate.Var(s, "base32")
+	NotEqual(t, errs, nil)
+	AssertError(t, errs, "", "", "", "", "base32")
+}
+
 func TestBase64Validation(t *testing.T) {
 	validate := New()
 
@@ -12482,6 +12516,33 @@ func TestIsIso3166Alpha2Validation(t *testing.T) {
 	}
 }
 
+func TestIsIso3166Alpha2EUValidation(t *testing.T) {
+	tests := []struct {
+		value    string `validate:"iso3166_1_alpha2_eu"`
+		expected bool
+	}{
+		{"SE", true},
+		{"UK", false},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.value, "iso3166_1_alpha2_eu")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d iso3166_1_alpha2_eu failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d iso3166_1_alpha2_eu failed Error: %s", i, errs)
+			}
+		}
+	}
+}
+
 func TestIsIso31662Validation(t *testing.T) {
 	tests := []struct {
 		value    string `validate:"iso3166_2"`
@@ -12538,6 +12599,34 @@ func TestIsIso3166Alpha3Validation(t *testing.T) {
 	}
 }
 
+func TestIsIso3166Alpha3EUValidation(t *testing.T) {
+	tests := []struct {
+		value    string `validate:"iso3166_1_alpha3_eu"`
+		expected bool
+	}{
+		{"POL", true},
+		{"SWE", true},
+		{"UNK", false},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.value, "iso3166_1_alpha3_eu")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d iso3166_1_alpha3_eu failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d iso3166_1_alpha3_eu failed Error: %s", i, errs)
+			}
+		}
+	}
+}
+
 func TestIsIso3166AlphaNumericValidation(t *testing.T) {
 	tests := []struct {
 		value    interface{}
@@ -12573,6 +12662,39 @@ func TestIsIso3166AlphaNumericValidation(t *testing.T) {
 	}, "Bad field type []string")
 }
 
+func TestIsIso3166AlphaNumericEUValidation(t *testing.T) {
+	tests := []struct {
+		value    interface{}
+		expected bool
+	}{
+		{752, true}, //Sweden
+		{"752", true},
+		{826, false}, // UK
+		{"826", false},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.value, "iso3166_1_alpha_numeric_eu")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d iso3166_1_alpha_numeric_eu failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d iso3166_1_alpha_numeric_eu failed Error: %s", i, errs)
+			}
+		}
+	}
+
+	PanicMatches(t, func() {
+		_ = validate.Var([]string{"1"}, "iso3166_1_alpha_numeric_eu")
+	}, "Bad field type []string")
+}
+
 func TestCountryCodeValidation(t *testing.T) {
 	tests := []struct {
 		value    interface{}
@@ -12606,6 +12728,39 @@ func TestCountryCodeValidation(t *testing.T) {
 	}
 }
 
+func TestEUCountryCodeValidation(t *testing.T) {
+	tests := []struct {
+		value    interface{}
+		expected bool
+	}{
+		{724, true},
+		{0, false},
+		{1, false},
+		{"POL", true},
+		{"NO", false},
+		{"724", true},
+		{"1", false},
+		{"0", false},
+	}
+
+	validate := New()
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.value, "eu_country_code")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d eu_country_code failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d eu_country_code failed Error: %s", i, errs)
+			}
+		}
+	}
+}
+
 func TestIsIso4217Validation(t *testing.T) {
 	tests := []struct {
 		value    string `validate:"iso4217"`