Repository: go-playground/validator
Pull request #1294 (2024-07-20 00:27:47)

feat: add validator for numeric ports
## Fixes Or Enhances
This PR adds a new validation for numeric ports.
Connected to: #1216 

```
	type Host struct {
		Port uint32 `validate:"port"`
	}

```
**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/68755705/badge)](https://coveralls.io/builds/68755705)

coverage: 74.298% (+0.007%) from 74.291%
when pulling **70d0cd06f245743d69f699a8d35c777d79e67c86 on nodivbyzero:feat-numeric-port-validation**
into **a947377040f8ebaee09f20d09a745ec369396793 on go-playground:master**.


Patch:
diff --git a/README.md b/README.md
index 9ab0705..ddd65b0 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 Package validator
 =================
 <img align="right" src="logo.png">[![Join the chat at https://gitter.im/go-playground/validator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/go-playground/validator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
-![Project status](https://img.shields.io/badge/version-10.22.0-green.svg)
+![Project status](https://img.shields.io/badge/version-10.22.1-green.svg)
 [![Build Status](https://travis-ci.org/go-playground/validator.svg?branch=master)](https://travis-ci.org/go-playground/validator)
 [![Coverage Status](https://coveralls.io/repos/go-playground/validator/badge.svg?branch=master&service=github)](https://coveralls.io/github/go-playground/validator?branch=master)
 [![Go Report Card](https://goreportcard.com/badge/github.com/go-playground/validator)](https://goreportcard.com/report/github.com/go-playground/validator)
diff --git a/baked_in.go b/baked_in.go
index b6fbaaf..2f66c18 100644
--- a/baked_in.go
+++ b/baked_in.go
@@ -205,6 +205,7 @@ var (
 		"fqdn":                          isFQDN,
 		"unique":                        isUnique,
 		"oneof":                         isOneOf,
+		"oneofci":                       isOneOfCI,
 		"html":                          isHTML,
 		"html_encoded":                  isHTMLEncoded,
 		"url_encoded":                   isURLEncoded,
@@ -213,6 +214,7 @@ var (
 		"json":                          isJSON,
 		"jwt":                           isJWT,
 		"hostname_port":                 isHostnamePort,
+		"port":                          isPort,
 		"lowercase":                     isLowercase,
 		"uppercase":                     isUppercase,
 		"datetime":                      isDatetime,
@@ -299,6 +301,23 @@ func isOneOf(fl FieldLevel) bool {
 	return false
 }
 
+// isOneOfCI is the validation function for validating if the current field's value is one of the provided string values (case insensitive).
+func isOneOfCI(fl FieldLevel) bool {
+	vals := parseOneOfParam2(fl.Param())
+	field := fl.Field()
+
+	if field.Kind() != reflect.String {
+		panic(fmt.Sprintf("Bad field type %T", field.Interface()))
+	}
+	v := field.String()
+	for _, val := range vals {
+		if strings.EqualFold(val, v) {
+			return true
+		}
+	}
+	return false
+}
+
 // isUnique is the validation function for validating if each array|slice|map value is unique
 func isUnique(fl FieldLevel) bool {
 	field := fl.Field()
@@ -1828,7 +1847,14 @@ func requireCheckFieldValue(
 		return int64(field.Len()) == asInt(value)
 
 	case reflect.Bool:
-		return field.Bool() == asBool(value)
+		return field.Bool() == (value == "true")
+
+	case reflect.Ptr:
+		if field.IsNil() {
+			return value == "nil"
+		}
+		// Handle non-nil pointers
+		return requireCheckFieldValue(fl, param, value, defaultNotFoundValue)
 	}
 
 	// default reflect.String:
@@ -2704,6 +2730,13 @@ func isHostnamePort(fl FieldLevel) bool {
 	return true
 }
 
+// IsPort validates if the current field's value represents a valid port
+func isPort(fl FieldLevel) bool {
+	val := fl.Field().Uint()
+
+	return val >= 1 && val <= 65535
+}
+
 // isLowercase is the validation function for validating if the current field's value is a lowercase string.
 func isLowercase(fl FieldLevel) bool {
 	field := fl.Field()
diff --git a/doc.go b/doc.go
index 90a8ade..c9b1616 100644
--- a/doc.go
+++ b/doc.go
@@ -489,12 +489,19 @@ For strings, ints, and uints, oneof will ensure that the value
 is one of the values in the parameter.  The parameter should be
 a list of values separated by whitespace. Values may be
 strings or numbers. To match strings with spaces in them, include
-the target string between single quotes.
+the target string between single quotes. Kind of like an 'enum'.
 
 	Usage: oneof=red green
 	       oneof='red green' 'blue yellow'
 	       oneof=5 7 9
 
+# One Of Case Insensitive
+
+Works the same as oneof but is case insensitive and therefore only accepts strings.
+
+	Usage: oneofci=red green
+	       oneofci='red green' 'blue yellow'
+
 # Greater Than
 
 For numbers, this will ensure that the value is greater than the
diff --git a/regexes.go b/regexes.go
index 7e1dd5a..871cf7d 100644
--- a/regexes.go
+++ b/regexes.go
@@ -73,7 +73,7 @@ const (
 	cveRegexString                   = `^CVE-(1999|2\d{3})-(0[^0]\d{2}|0\d[^0]\d{1}|0\d{2}[^0]|[1-9]{1}\d{3,})$` // CVE Format Id https://cve.mitre.org/cve/identifiers/syntaxchange.html
 	mongodbIdRegexString             = "^[a-f\\d]{24}$"
 	mongodbConnStringRegexString     = "^mongodb(\\+srv)?:\\/\\/(([a-zA-Z\\d]+):([a-zA-Z\\d$:\\/?#\\[\\]@]+)@)?(([a-z\\d.-]+)(:[\\d]+)?)((,(([a-z\\d.-]+)(:(\\d+))?))*)?(\\/[a-zA-Z-_]{1,64})?(\\?(([a-zA-Z]+)=([a-zA-Z\\d]+))(&(([a-zA-Z\\d]+)=([a-zA-Z\\d]+))?)*)?$"
-	cronRegexString                  = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7})`
+	cronRegexString                  = `(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)|((((\d+,)+\d+|((\*|\d+)(\/|-)\d+)|\d+|\*) ?){5,7})`
 	spicedbIDRegexString             = `^(([a-zA-Z0-9/_|\-=+]{1,})|\*)$`
 	spicedbPermissionRegexString     = "^([a-z][a-z0-9_]{1,62}[a-z0-9])?$"
 	spicedbTypeRegexString           = "^([a-z][a-z0-9_]{1,61}[a-z0-9]/)?[a-z][a-z0-9_]{1,62}[a-z0-9]$"


Test patch:
diff --git a/validator_test.go b/validator_test.go
index 0f96b77..5eadb25 100644
--- a/validator_test.go
+++ b/validator_test.go
@@ -5680,6 +5680,82 @@ func TestOneOfValidation(t *testing.T) {
 	}, "Bad field type float64")
 }
 
+func TestOneOfCIValidation(t *testing.T) {
+	validate := New()
+
+	passSpecs := []struct {
+		f interface{}
+		t string
+	}{
+		{f: "red", t: "oneofci=RED GREEN"},
+		{f: "RED", t: "oneofci=red green"},
+		{f: "red", t: "oneofci=red green"},
+		{f: "RED", t: "oneofci=RED GREEN"},
+		{f: "green", t: "oneofci=red green"},
+		{f: "red green", t: "oneofci='red green' blue"},
+		{f: "blue", t: "oneofci='red green' blue"},
+		{f: "GREEN", t: "oneofci=Red Green"},
+		{f: "ReD", t: "oneofci=RED GREEN"},
+		{f: "gReEn", t: "oneofci=rEd GrEeN"},
+		{f: "RED GREEN", t: "oneofci='red green' blue"},
+		{f: "red Green", t: "oneofci='RED GREEN' Blue"},
+		{f: "Red green", t: "oneofci='Red Green' BLUE"},
+		{f: "rEd GrEeN", t: "oneofci='ReD gReEn' BlUe"},
+		{f: "BLUE", t: "oneofci='Red Green' BLUE"},
+		{f: "BlUe", t: "oneofci='RED GREEN' Blue"},
+		{f: "bLuE", t: "oneofci='red green' BLUE"},
+	}
+
+	for _, spec := range passSpecs {
+		t.Logf("%#v", spec)
+		errs := validate.Var(spec.f, spec.t)
+		Equal(t, errs, nil)
+	}
+
+	failSpecs := []struct {
+		f interface{}
+		t string
+	}{
+		{f: "", t: "oneofci=red green"},
+		{f: "yellow", t: "oneofci=red green"},
+		{f: "green", t: "oneofci='red green' blue"},
+	}
+
+	for _, spec := range failSpecs {
+		t.Logf("%#v", spec)
+		errs := validate.Var(spec.f, spec.t)
+		AssertError(t, errs, "", "", "", "", "oneofci")
+	}
+
+	panicSpecs := []struct {
+		f interface{}
+		t string
+	}{
+		{f: 3.14, t: "oneofci=red green"},
+		{f: 5, t: "oneofci=red green"},
+		{f: uint(6), t: "oneofci=7"},
+		{f: int8(5), t: "oneofci=red green"},
+		{f: int16(5), t: "oneofci=red green"},
+		{f: int32(5), t: "oneofci=red green"},
+		{f: int64(5), t: "oneofci=red green"},
+		{f: uint(5), t: "oneofci=red green"},
+		{f: uint8(5), t: "oneofci=red green"},
+		{f: uint16(5), t: "oneofci=red green"},
+		{f: uint32(5), t: "oneofci=red green"},
+		{f: uint64(5), t: "oneofci=red green"},
+	}
+
+	panicCount := 0
+	for _, spec := range panicSpecs {
+		t.Logf("%#v", spec)
+		PanicMatches(t, func() {
+			_ = validate.Var(spec.f, spec.t)
+		}, fmt.Sprintf("Bad field type %T", spec.f))
+		panicCount++
+	}
+	Equal(t, panicCount, len(panicSpecs))
+}
+
 func TestBase32Validation(t *testing.T) {
 	validate := New()
 
@@ -12002,6 +12078,25 @@ func TestExcludedIf(t *testing.T) {
 	errs = validate.Struct(test10)
 	Equal(t, errs, nil)
 
+	test11 := struct {
+		Field1 bool
+  		Field2 *string `validate:"excluded_if=Field1 false"`
+	}{
+		Field1: false,
+		Field2: nil,
+	}
+	errs = validate.Struct(test11)
+	Equal(t, errs, nil)
+
+	test12 := struct {
+		Field1 bool
+		Field2 *string `validate:"excluded_if=Field1 !Field1"`
+	}{
+		Field1: true,
+		Field2: nil,
+	}
+	errs = validate.Struct(test12)
+	Equal(t, errs, nil)
 	// Checks number of params in struct tag is correct
 	defer func() {
 		if r := recover(); r == nil {
@@ -12377,6 +12472,32 @@ func Test_hostnameport_validator(t *testing.T) {
 	}
 }
 
+func Test_port_validator(t *testing.T) {
+	type Host struct {
+		Port uint32 `validate:"port"`
+	}
+
+	type testInput struct {
+		data     uint32
+		expected bool
+	}
+	testData := []testInput{
+		{0, false},
+		{1, true},
+		{65535, true},
+		{65536, false},
+		{65538, false},
+	}
+	for _, td := range testData {
+		h := Host{Port: td.data}
+		v := New()
+		err := v.Struct(h)
+		if td.expected != (err == nil) {
+			t.Fatalf("Test failed for data: %v Error: %v", td.data, err)
+		}
+	}
+}
+
 func TestLowercaseValidation(t *testing.T) {
 	tests := []struct {
 		param    string
@@ -13637,6 +13758,7 @@ func TestCronExpressionValidation(t *testing.T) {
 		{"*/20 * * * *", "cron", true},
 		{"0 15 10 ? * MON-FRI", "cron", true},
 		{"0 15 10 ? * 6#3", "cron", true},
+		{"0 */15 * * *", "cron", true},
 		{"wrong", "cron", false},
 	}