Repository: go-playground/validator
Pull request #1076 (2023-03-03 15:19:44)

added IsEqIgnoreCase and IsNeIgnoreCase
## Fixes Or Enhances
I added the functions isEqIgnoreCase and isNeIgnoreCase to make case-insensitive validation.

**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/57512816/badge)](https://coveralls.io/builds/57512816)

Coverage: 74.214% (+0.02%) from 74.191% when pulling **9354dc0f7d1a6d6a18b2fb5cfe916d087b0650f0 on sgallizia:dev_eqignorecase** into **8f07b0368280d9fc70b4ffd3708b31427bbc286d on go-playground:master**.


Patch:
diff --git a/README.md b/README.md
index 5f8878d..2e5fdcd 100644
--- a/README.md
+++ b/README.md
@@ -207,11 +207,13 @@ Baked-in Validations
 | Tag | Description |
 | - | - |
 | eq | Equals |
+| eqIgnoreCase | Equals ignoring case |
 | gt | Greater than|
 | gte | Greater than or equal |
 | lt | Less Than |
 | lte | Less Than or Equal |
 | ne | Not Equal |
+| neIgnoreCase | Not Equal ignoring case |
 
 ### Other:
 | Tag | Description |
diff --git a/baked_in.go b/baked_in.go
index c9b1db4..b9c3fdd 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -86,7 +86,9 @@ var (
 		"min":                           hasMinOf,
 		"max":                           hasMaxOf,
 		"eq":                            isEq,
+		"eqIgnoreCase":                  isEqIgnoreCase,
 		"ne":                            isNe,
+		"neIgnoreCase":                  isNeIgnoreCase,
 		"lt":                            isLt,
 		"lte":                           isLte,
 		"gt":                            isGt,
@@ -140,6 +142,7 @@ var (
 		"isbn10":                        isISBN10,
 		"isbn13":                        isISBN13,
 		"eth_addr":                      isEthereumAddress,
+		"eth_addr_checksum":             isEthereumAddressChecksum,
 		"btc_addr":                      isBitcoinAddress,
 		"btc_addr_bech32":               isBitcoinBech32Address,
 		"uuid":                          isUUID,
@@ -613,14 +616,16 @@ func isISBN10(fl FieldLevel) bool {
 func isEthereumAddress(fl FieldLevel) bool {
 	address := fl.Field().String()
 
+	return ethAddressRegex.MatchString(address)
+}
+
+// isEthereumAddressChecksum is the validation function for validating if the field's value is a valid checksumed Ethereum address.
+func isEthereumAddressChecksum(fl FieldLevel) bool {
+	address := fl.Field().String()
+
 	if !ethAddressRegex.MatchString(address) {
 		return false
 	}
-
-	if ethAddressRegexUpper.MatchString(address) || ethAddressRegexLower.MatchString(address) {
-		return true
-	}
-
 	// Checksum validation. Reference: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md
 	address = address[2:] // Skip "0x" prefix.
 	h := sha3.NewLegacyKeccak256()
@@ -889,6 +894,12 @@ func isNe(fl FieldLevel) bool {
 	return !isEq(fl)
 }
 
+// isNe is the validation function for validating that the field's string value does not equal the
+// provided param value. The comparison is case-insensitive
+func isNeIgnoreCase(fl FieldLevel) bool {
+	return !isEqIgnoreCase(fl)
+}
+
 // isLteCrossStructField is the validation function for validating if the current field's value is less than or equal to the field, within a separate struct, specified by the param's value.
 func isLteCrossStructField(fl FieldLevel) bool {
 	field := fl.Field()
@@ -1260,6 +1271,22 @@ func isEq(fl FieldLevel) bool {
 	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
 }
 
+// isEqIgnoreCase is the validation function for validating if the current field's string value is
+//equal to the param's value.
+// The comparison is case-insensitive.
+func isEqIgnoreCase(fl FieldLevel) bool {
+	field := fl.Field()
+	param := fl.Param()
+
+	switch field.Kind() {
+
+	case reflect.String:
+		return strings.EqualFold(field.String(), param)
+	}
+
+	panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+}
+
 // isPostcodeByIso3166Alpha2 validates by value which is country code in iso 3166 alpha 2
 // example: `postcode_iso3166_alpha2=US`
 func isPostcodeByIso3166Alpha2(fl FieldLevel) bool {
@@ -1539,7 +1566,9 @@ func requireCheckFieldKind(fl FieldLevel, param string, defaultNotFoundValue boo
 }
 
 // requireCheckFieldValue is a func for check field value
-func requireCheckFieldValue(fl FieldLevel, param string, value string, defaultNotFoundValue bool) bool {
+func requireCheckFieldValue(
+	fl FieldLevel, param string, value string, defaultNotFoundValue bool,
+) bool {
 	field, kind, _, found := fl.GetStructFieldOKAdvanced2(fl.Parent(), param)
 	if !found {
 		return defaultNotFoundValue
@@ -2316,7 +2345,9 @@ func isHostnamePort(fl FieldLevel) bool {
 		return false
 	}
 	// Port must be a iny <= 65535.
-	if portNum, err := strconv.ParseInt(port, 10, 32); err != nil || portNum > 65535 || portNum < 1 {
+	if portNum, err := strconv.ParseInt(
+		port, 10, 32,
+	); err != nil || portNum > 65535 || portNum < 1 {
 		return false
 	}
 
diff --git a/doc.go b/doc.go
index 7341c67..7f0a853 100644
--- a/doc.go
+++ b/doc.go
@@ -232,16 +232,16 @@ require another 'keys' and 'endkeys' tag. These tags are only valid for maps.
 
 Example #1
 
-	map[string]string with validation tag "gt=0,dive,keys,eg=1|eq=2,endkeys,required"
+	map[string]string with validation tag "gt=0,dive,keys,eq=1|eq=2,endkeys,required"
 	// gt=0 will be applied to the map itself
-	// eg=1|eq=2 will be applied to the map keys
+	// eq=1|eq=2 will be applied to the map keys
 	// required will be applied to map values
 
 Example #2
 
 	map[[2]string]string with validation tag "gt=0,dive,keys,dive,eq=1|eq=2,endkeys,required"
 	// gt=0 will be applied to the map itself
-	// eg=1|eq=2 will be applied to each array element in the the map keys
+	// eq=1|eq=2 will be applied to each array element in the the map keys
 	// required will be applied to map values
 
 Required
diff --git a/regexes.go b/regexes.go
index 9c1c634..fb2ea18 100644
--- a/regexes.go
+++ b/regexes.go
@@ -118,8 +118,6 @@ var (
 	btcUpperAddressRegexBech32 = regexp.MustCompile(btcAddressUpperRegexStringBech32)
 	btcLowerAddressRegexBech32 = regexp.MustCompile(btcAddressLowerRegexStringBech32)
 	ethAddressRegex            = regexp.MustCompile(ethAddressRegexString)
-	ethAddressRegexUpper       = regexp.MustCompile(ethAddressUpperRegexString)
-	ethAddressRegexLower       = regexp.MustCompile(ethAddressLowerRegexString)
 	uRLEncodedRegex            = regexp.MustCompile(uRLEncodedRegexString)
 	hTMLEncodedRegex           = regexp.MustCompile(hTMLEncodedRegexString)
 	hTMLRegex                  = regexp.MustCompile(hTMLRegexString)


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 62be26a..8c9a4dd 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -5207,6 +5207,24 @@ func TestIsNeValidation(t *testing.T) {
 	Equal(t, errs, nil)
 }
 
+func TestIsNeIgnoreCaseValidation(t *testing.T) {
+	var errs error
+	validate := New()
+	s := "abcd"
+	now := time.Now()
+
+	errs = validate.Var(s, "neIgnoreCase=efgh")
+	Equal(t, errs, nil)
+
+	errs = validate.Var(s, "neIgnoreCase=AbCd")
+	NotEqual(t, errs, nil)
+	AssertError(t, errs, "", "", "", "", "neIgnoreCase")
+
+	PanicMatches(
+		t, func() { _ = validate.Var(now, "eqIgnoreCase=abcd") }, "Bad field type time.Time",
+	)
+}
+
 func TestIsEqFieldValidation(t *testing.T) {
 	var errs error
 	validate := New()
@@ -5484,6 +5502,23 @@ func TestIsEqValidation(t *testing.T) {
 	Equal(t, errs, nil)
 }
 
+func TestIsEqIgnoreCaseValidation(t *testing.T) {
+	var errs error
+	validate := New()
+	s := "abcd"
+	now := time.Now()
+
+	errs = validate.Var(s, "eqIgnoreCase=abcd")
+	Equal(t, errs, nil)
+
+	errs = validate.Var(s, "eqIgnoreCase=AbCd")
+	Equal(t, errs, nil)
+
+	PanicMatches(
+		t, func() { _ = validate.Var(now, "eqIgnoreCase=abcd") }, "Bad field type time.Time",
+	)
+}
+
 func TestOneOfValidation(t *testing.T) {
 	validate := New()
 
@@ -5672,7 +5707,7 @@ func TestEthereumAddressValidation(t *testing.T) {
 		{"0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359", true},
 		{"0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB", true},
 		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", true},
-		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDB", false}, // Invalid checksum.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDB", true}, // Invalid checksum, but valid address.
 
 		// Other.
 		{"", false},
@@ -5703,6 +5738,56 @@ func TestEthereumAddressValidation(t *testing.T) {
 	}
 }
 
+func TestEthereumAddressChecksumValidation(t *testing.T) {
+	validate := New()
+
+	tests := []struct {
+		param    string
+		expected bool
+	}{
+		// All caps.
+		{"0x52908400098527886E0F7030069857D2E4169EE7", true},
+		{"0x8617E340B3D01FA5F11F306F4090FD50E238070D", true},
+
+		// All lower.
+		{"0x27b1fdb04752bbc536007a920d24acb045561c26", true},
+		{"0x123f681646d4a755815f9cb19e1acc8565a0c2ac", false},
+
+		// Mixed case: runs checksum validation.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", true},
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDB", false}, // Invalid checksum.
+		{"0x000000000000000000000000000000000000dead", false}, // Invalid checksum.
+		{"0x000000000000000000000000000000000000dEaD", true},  // Valid checksum.
+
+		// Other.
+		{"", false},
+		{"D1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb", false},    // Missing "0x" prefix.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDbc", false}, // More than 40 hex digits.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aD", false},   // Less than 40 hex digits.
+		{"0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDw", false},  // Invalid hex digit "w".
+	}
+
+	for i, test := range tests {
+
+		errs := validate.Var(test.param, "eth_addr_checksum")
+
+		if test.expected {
+			if !IsEqual(errs, nil) {
+				t.Fatalf("Index: %d eth_addr_checksum failed Error: %s", i, errs)
+			}
+		} else {
+			if IsEqual(errs, nil) {
+				t.Fatalf("Index: %d eth_addr_checksum failed Error: %s", i, errs)
+			} else {
+				val := getError(errs, "", "")
+				if val.Tag() != "eth_addr_checksum" {
+					t.Fatalf("Index: %d Latitude failed Error: %s", i, errs)
+				}
+			}
+		}
+	}
+}
+
 func TestBitcoinAddressValidation(t *testing.T) {
 	validate := New()