Skip to content

Commit 636bfaa

Browse files
Feat: Add NoneOf Validation (#1554)
## Fixes Or Enhances Addresses issue #1552 **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
1 parent 0ccc97b commit 636bfaa

5 files changed

Lines changed: 189 additions & 13 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ validate := validator.New(validator.WithRequiredStructEnabled())
255255
| max | Maximum |
256256
| min | Minimum |
257257
| oneof | One Of |
258+
| noneof | None Of |
258259
| required | Required |
259260
| required_if | Required If |
260261
| required_unless | Required Unless |

baked_in.go

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"os"
1818
"reflect"
1919
"runtime"
20+
"slices"
2021
"strconv"
2122
"strings"
2223
"sync"
@@ -217,6 +218,8 @@ var (
217218
"unique": isUnique,
218219
"oneof": isOneOf,
219220
"oneofci": isOneOfCI,
221+
"noneof": isNoneOf,
222+
"noneofci": isNoneOfCI,
220223
"html": isHTML,
221224
"html_encoded": isHTMLEncoded,
222225
"url_encoded": isURLEncoded,
@@ -307,12 +310,8 @@ func isOneOf(fl FieldLevel) bool {
307310
default:
308311
panic(fmt.Sprintf("Bad field type %s", field.Type()))
309312
}
310-
for i := 0; i < len(vals); i++ {
311-
if vals[i] == v {
312-
return true
313-
}
314-
}
315-
return false
313+
314+
return slices.Contains(vals, v)
316315
}
317316

318317
// isOneOfCI is the validation function for validating if the current field's value is one of the provided string values (case insensitive).
@@ -323,13 +322,20 @@ func isOneOfCI(fl FieldLevel) bool {
323322
if field.Kind() != reflect.String {
324323
panic(fmt.Sprintf("Bad field type %s", field.Type()))
325324
}
326-
v := field.String()
327-
for _, val := range vals {
328-
if strings.EqualFold(val, v) {
329-
return true
330-
}
331-
}
332-
return false
325+
326+
return slices.ContainsFunc(vals, func(val string) bool {
327+
return strings.EqualFold(val, field.String())
328+
})
329+
}
330+
331+
// isNoneOf validates that the current field's value is not one of the provided string or integer values
332+
func isNoneOf(fl FieldLevel) bool {
333+
return !isOneOf(fl)
334+
}
335+
336+
// isNoneOfCI validates that the current field's value is not one of the provided string values (case insensitive)
337+
func isNoneOfCI(fl FieldLevel) bool {
338+
return !isOneOfCI(fl)
333339
}
334340

335341
// isUnique is the validation function for validating if each array|slice|map value is unique

benchmarks_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,6 +1099,30 @@ func BenchmarkOneofParallel(b *testing.B) {
10991099
})
11001100
}
11011101

1102+
type TestNoneOf struct {
1103+
Color string `validate:"noneof=red green"`
1104+
}
1105+
1106+
func BenchmarkNoneOf(b *testing.B) {
1107+
w := &TestNoneOf{Color: "blue"}
1108+
val := New()
1109+
for i := 0; i < b.N; i++ {
1110+
_ = val.Struct(w)
1111+
}
1112+
}
1113+
1114+
func BenchmarkNoneOfParallel(b *testing.B) {
1115+
w := &TestNoneOf{Color: "blue"}
1116+
val := New()
1117+
1118+
b.ResetTimer()
1119+
b.RunParallel(func(pb *testing.PB) {
1120+
for pb.Next() {
1121+
_ = val.Struct(w)
1122+
}
1123+
})
1124+
}
1125+
11021126
type T struct{}
11031127

11041128
func (*T) Validate() error { return errors.New("ops") }

doc.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,24 @@ Works the same as oneof but is case insensitive and therefore only accepts strin
546546
Usage: oneofci=red green
547547
oneofci='red green' 'blue yellow'
548548
549+
# None Of
550+
551+
For strings, ints, and uints, noneof will ensure that the value is not one of
552+
the values in the parameter. The parameter should be a list of values separated by whitespace.
553+
Values may be strings or numbers. To inversely match strings with spaces in them, include the target string between single quotes.
554+
Kind of like an 'enum'.
555+
556+
Usage: noneof=red green
557+
noneof='red green' 'blue yellow'
558+
noneof=5 7 9
559+
560+
561+
# None Of Case Insensitive
562+
Works the same as noneof but is case insensitive and therefore only accepts strings.
563+
564+
Usage: noneofci=red green
565+
noneofci='red green' 'blue yellow'
566+
549567
# Greater Than
550568
551569
For numbers, this will ensure that the value is greater than the

validator_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5883,6 +5883,133 @@ func TestOneOfCIValidation(t *testing.T) {
58835883
Equal(t, panicCount, len(panicSpecs))
58845884
}
58855885

5886+
func TestNoneOfValidation(t *testing.T) {
5887+
validate := New()
5888+
5889+
passSpecs := []struct {
5890+
f any
5891+
t string
5892+
}{
5893+
{f: "", t: "noneof=red green"},
5894+
{f: "yellow", t: "noneof=red green"},
5895+
{f: "green", t: "noneof='red green' blue"},
5896+
{f: 5, t: "noneof=red green"},
5897+
{f: 6, t: "noneof=red green"},
5898+
{f: 6, t: "noneof=7"},
5899+
{f: int8(5), t: "noneof=red green"},
5900+
{f: int16(5), t: "noneof=red green"},
5901+
{f: int32(5), t: "noneof=red green"},
5902+
{f: int64(5), t: "noneof=red green"},
5903+
{f: uint(6), t: "noneof=7"},
5904+
{f: uint8(6), t: "noneof=7"},
5905+
{f: uint16(6), t: "noneof=7"},
5906+
{f: uint32(6), t: "noneof=7"},
5907+
{f: uint64(6), t: "noneof=7"},
5908+
}
5909+
for _, spec := range passSpecs {
5910+
t.Logf("%#v", spec)
5911+
errs := validate.Var(spec.f, spec.t)
5912+
Equal(t, errs, nil)
5913+
}
5914+
5915+
failSpecs := []struct {
5916+
f any
5917+
t string
5918+
}{
5919+
{f: "red", t: "noneof=red green"},
5920+
{f: "green", t: "noneof=red green"},
5921+
{f: "red green", t: "noneof='red green' blue'"},
5922+
{f: "blue", t: "noneof='red green' blue'"},
5923+
{f: 5, t: "noneof=5 6"},
5924+
{f: 6, t: "noneof=5 6"},
5925+
{f: int8(6), t: "noneof=5 6"},
5926+
{f: int16(6), t: "noneof=5 6"},
5927+
{f: int32(6), t: "noneof=5 6"},
5928+
{f: int64(6), t: "noneof=5 6"},
5929+
{f: uint(6), t: "noneof=5 6"},
5930+
{f: uint8(6), t: "noneof=5 6"},
5931+
{f: uint16(6), t: "noneof=5 6"},
5932+
{f: uint32(6), t: "noneof=5 6"},
5933+
{f: uint64(6), t: "noneof=5 6"},
5934+
}
5935+
for _, spec := range failSpecs {
5936+
t.Logf("%#v", spec)
5937+
errs := validate.Var(spec.f, spec.t)
5938+
AssertError(t, errs, "", "", "", "", "noneof")
5939+
}
5940+
5941+
PanicMatches(t, func() {
5942+
_ = validate.Var(3.14, "noneof=red green")
5943+
}, "Bad field type float64")
5944+
}
5945+
5946+
func TestNoneOfCIValidation(t *testing.T) {
5947+
validate := New()
5948+
5949+
passSpecs := []struct {
5950+
f any
5951+
t string
5952+
}{
5953+
{f: "", t: "noneofci=red green"},
5954+
{f: "yellow", t: "noneofci=red green"},
5955+
{f: "green", t: "noneofci='red yellow' blue"},
5956+
{f: "RED", t: "noneofci=blue green"},
5957+
{f: "RED", t: "noneofci=BLUE GREEN"},
5958+
{f: "ReD", t: "noneofci=BLUE GREEN"},
5959+
{f: "gReEn", t: "noneofci=rEd BlUe"},
5960+
{f: "red Green", t: "noneofci='BLUE YELLOW' Orange"},
5961+
{f: "Red green", t: "noneofci='Blue Yellow' ORANGE"},
5962+
{f: "rEd GrEeN", t: "noneofci='bLuE YeLlOw' OrAnGe"},
5963+
{f: "BlUe", t: "noneofci='RED GREEN' Yellow"},
5964+
{f: "bLuE", t: "noneofci='red green' YELLOW"},
5965+
}
5966+
for _, spec := range passSpecs {
5967+
t.Logf("%#v", spec)
5968+
errs := validate.Var(spec.f, spec.t)
5969+
Equal(t, errs, nil)
5970+
}
5971+
5972+
failSpecs := []struct {
5973+
f any
5974+
t string
5975+
}{
5976+
{f: "red", t: "noneofci=red green"},
5977+
{f: "green", t: "noneofci=red green"},
5978+
{f: "red green", t: "noneofci='red green' blue'"},
5979+
}
5980+
for _, spec := range failSpecs {
5981+
t.Logf("%#v", spec)
5982+
errs := validate.Var(spec.f, spec.t)
5983+
AssertError(t, errs, "", "", "", "", "noneofci")
5984+
}
5985+
5986+
panicSpecs := []struct {
5987+
f any
5988+
t string
5989+
}{
5990+
{f: 3.14, t: "noneofci=red green"},
5991+
{f: 5, t: "noneofci=red green"},
5992+
{f: int8(5), t: "noneofci=red green"},
5993+
{f: int16(5), t: "noneofci=red green"},
5994+
{f: int32(5), t: "noneofci=red green"},
5995+
{f: int64(5), t: "noneofci=red green"},
5996+
{f: uint(5), t: "noneofci=red green"},
5997+
{f: uint8(5), t: "noneofci=red green"},
5998+
{f: uint16(5), t: "noneofci=red green"},
5999+
{f: uint32(5), t: "noneofci=red green"},
6000+
{f: uint64(5), t: "noneofci=red green"},
6001+
}
6002+
panicCount := 0
6003+
for _, spec := range panicSpecs {
6004+
t.Logf("%#v", spec)
6005+
PanicMatches(t, func() {
6006+
_ = validate.Var(spec.f, spec.t)
6007+
}, fmt.Sprintf("Bad field type %T", spec.f))
6008+
panicCount++
6009+
}
6010+
Equal(t, panicCount, len(panicSpecs))
6011+
}
6012+
58866013
func TestBase32Validation(t *testing.T) {
58876014
validate := New()
58886015

0 commit comments

Comments
 (0)