fix(12-05): match Laravel's in and not_in rules on array values

A 162-case truth table recorded from WinterCMS's validator (the vendored
winter/storm Factory, Laravel 9) found three divergences, all on arrays:

- in compared array elements loosely; Laravel uses array_diff, an exact
  string comparison, so ["1.0"] is not in 1,2;
- not_in failed when any element was listed; Laravel's validateNotIn is
  !validateIn, so an array passes unless every element is listed;
- not_in failed an array without the array rule; Laravel passes it.

validate_rules_test.go keeps the whole table (accepted, array keys,
boolean, numeric, integer, in/not_in, sizes by type, regex, dates and
comparisons, url, presence, nested and map wildcards, bail and order).
This commit is contained in:
Jakub Zych
2026-10-02 15:42:47 +02:00
parent 92b12fee4c
commit 36983bc87e
2 changed files with 224 additions and 15 deletions

View File

@@ -11,6 +11,7 @@ import (
"net/http"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"time"
@@ -700,6 +701,11 @@ func inList(s string, list []string) bool {
return false
}
// validateIn is Laravel's validateIn. An array value needs the array rule
// and no nested element, and then, like count(array_diff($value,
// $parameters)) === 0, every element's string form must equal a parameter
// exactly. A scalar compares like in_array((string) $value, $parameters),
// where two numeric strings compare as numbers.
func (v *requestValidator) validateIn(attr string, value any, params []string) bool {
if _, isArr := arrayLen(value); isArr {
if !v.hasRule(attr, "array") {
@@ -709,8 +715,10 @@ func (v *requestValidator) validateIn(attr string, value any, params []string) b
if _, nested := arrayLen(ch.value); nested {
return false
}
}
for _, ch := range children(value) {
s, ok := phpScalarString(ch.value)
if !ok || !inList(s, params) {
if !ok || !slices.Contains(params, s) {
return false
}
}
@@ -720,21 +728,11 @@ func (v *requestValidator) validateIn(attr string, value any, params []string) b
return ok && inList(s, params)
}
// validateNotIn is Laravel's validateNotIn: the negation of validateIn, so
// an array passes unless every element is listed, and an array without the
// array rule always passes.
func (v *requestValidator) validateNotIn(attr string, value any, params []string) bool {
if _, isArr := arrayLen(value); isArr {
if !v.hasRule(attr, "array") {
return false
}
for _, ch := range children(value) {
s, ok := phpScalarString(ch.value)
if ok && inList(s, params) {
return false
}
}
return true
}
s, ok := phpScalarString(value)
return ok && !inList(s, params)
return !v.validateIn(attr, value, params)
}
// exists is Laravel's exists:table,column presence check. It counts rows