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

View File

@@ -0,0 +1,211 @@
package lagoon
import (
"encoding/json"
"reflect"
"strings"
"testing"
)
// laravelRuleCases is a truth table for ValidateRequest. Each want is the
// English errors object that WinterCMS's validator (Laravel 9, the
// vendored winter/storm Factory) returned for the same JSON input and rule
// strings, so a case that fails here is a divergence from PHP. Inputs are
// decoded with UseNumber, as the application decodes request bodies.
var laravelRuleCases = []struct {
name string
data string
rules [][2]string
want string
}{
{"accepted-yes", `{"t":"yes"}`, [][2]string{{"t", `accepted`}}, `{}`},
{"accepted-on", `{"t":"on"}`, [][2]string{{"t", `accepted`}}, `{}`},
{"accepted-one-string", `{"t":"1"}`, [][2]string{{"t", `accepted`}}, `{}`},
{"accepted-true-string", `{"t":"true"}`, [][2]string{{"t", `accepted`}}, `{}`},
{"accepted-true", `{"t":true}`, [][2]string{{"t", `accepted`}}, `{}`},
{"accepted-one", `{"t":1}`, [][2]string{{"t", `accepted`}}, `{}`},
{"accepted-no", `{"t":"no"}`, [][2]string{{"t", `accepted`}}, `{"t":["The t must be accepted."]}`},
{"accepted-zero", `{"t":0}`, [][2]string{{"t", `accepted`}}, `{"t":["The t must be accepted."]}`},
{"accepted-false-string", `{"t":"false"}`, [][2]string{{"t", `accepted`}}, `{"t":["The t must be accepted."]}`},
{"accepted-two", `{"t":2}`, [][2]string{{"t", `accepted`}}, `{"t":["The t must be accepted."]}`},
{"accepted-missing", `{}`, [][2]string{{"t", `accepted`}}, `{"t":["The t must be accepted."]}`},
{"array-keys-ok", `{"a":{"x":1,"y":2}}`, [][2]string{{"a", `array:x,y`}}, `{}`},
{"array-keys-extra", `{"a":{"x":1,"z":2}}`, [][2]string{{"a", `array:x,y`}}, `{"a":["The a must be an array."]}`},
{"array-scalar", `{"a":"str"}`, [][2]string{{"a", `array`}}, `{"a":["The a must be an array."]}`},
{"array-list-keys", `{"a":[1,2]}`, [][2]string{{"a", `array:0,1`}}, `{}`},
{"array-list-keys-short", `{"a":[1,2,3]}`, [][2]string{{"a", `array:0,1`}}, `{"a":["The a must be an array."]}`},
{"boolean-true", `{"b":true}`, [][2]string{{"b", `boolean`}}, `{}`},
{"boolean-false", `{"b":false}`, [][2]string{{"b", `boolean`}}, `{}`},
{"boolean-zero", `{"b":0}`, [][2]string{{"b", `boolean`}}, `{}`},
{"boolean-one", `{"b":1}`, [][2]string{{"b", `boolean`}}, `{}`},
{"boolean-zero-string", `{"b":"0"}`, [][2]string{{"b", `boolean`}}, `{}`},
{"boolean-one-string", `{"b":"1"}`, [][2]string{{"b", `boolean`}}, `{}`},
{"boolean-true-string", `{"b":"true"}`, [][2]string{{"b", `boolean`}}, `{"b":["The b field must be true or false."]}`},
{"boolean-yes", `{"b":"yes"}`, [][2]string{{"b", `boolean`}}, `{"b":["The b field must be true or false."]}`},
{"boolean-two", `{"b":2}`, [][2]string{{"b", `boolean`}}, `{"b":["The b field must be true or false."]}`},
{"boolean-two-string", `{"b":"2"}`, [][2]string{{"b", `boolean`}}, `{"b":["The b field must be true or false."]}`},
{"boolean-float-one", `{"b":1.0}`, [][2]string{{"b", `boolean`}}, `{"b":["The b field must be true or false."]}`},
{"numeric-exp", `{"n":"1e3"}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-spaces", `{"n":" 12 "}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-trailing-letters", `{"n":"12abc"}`, [][2]string{{"n", `numeric`}}, `{"n":["The n must be a number."]}`},
{"numeric-neg-frac", `{"n":"-.5"}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-frac-exp", `{"n":".5e1"}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-plus", `{"n":"+1"}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-bare-e", `{"n":"1e"}`, [][2]string{{"n", `numeric`}}, `{"n":["The n must be a number."]}`},
{"numeric-hex", `{"n":"0x1A"}`, [][2]string{{"n", `numeric`}}, `{"n":["The n must be a number."]}`},
{"numeric-float", `{"n":12.5}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-true", `{"n":true}`, [][2]string{{"n", `numeric`}}, `{"n":["The n must be a number."]}`},
{"numeric-dot", `{"n":"."}`, [][2]string{{"n", `numeric`}}, `{"n":["The n must be a number."]}`},
{"numeric-leading-space", `{"n":" 12"}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-trailing-space", `{"n":"12 "}`, [][2]string{{"n", `numeric`}}, `{}`},
{"numeric-array", `{"n":[1]}`, [][2]string{{"n", `numeric`}}, `{"n":["The n must be a number."]}`},
{"integer-leading-zero", `{"i":"007"}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-neg-zero", `{"i":"-0"}`, [][2]string{{"i", `integer`}}, `{}`},
{"integer-plus", `{"i":"+5"}`, [][2]string{{"i", `integer`}}, `{}`},
{"integer-spaces", `{"i":" 5 "}`, [][2]string{{"i", `integer`}}, `{}`},
{"integer-decimal-string", `{"i":"5.0"}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-float-whole", `{"i":5.0}`, [][2]string{{"i", `integer`}}, `{}`},
{"integer-float-exp", `{"i":1000.0}`, [][2]string{{"i", `integer`}}, `{}`},
{"integer-overflow", `{"i":"9223372036854775808"}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-max", `{"i":"9223372036854775807"}`, [][2]string{{"i", `integer`}}, `{}`},
{"integer-true", `{"i":true}`, [][2]string{{"i", `integer`}}, `{}`},
{"integer-false", `{"i":false}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-array", `{"i":[]}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-exp-string", `{"i":"1e3"}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-minus", `{"i":"-"}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-plus-only", `{"i":"+"}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-float-frac", `{"i":5.5}`, [][2]string{{"i", `integer`}}, `{"i":["The i must be an integer."]}`},
{"integer-zero", `{"i":"0"}`, [][2]string{{"i", `integer`}}, `{}`},
{"in-ok", `{"f":"LP"}`, [][2]string{{"f", `in:LP,CD`}}, `{}`},
{"in-case", `{"f":"lp"}`, [][2]string{{"f", `in:LP,CD`}}, `{"f":["The selected f is invalid."]}`},
{"in-loose-numeric", `{"f":"1.0"}`, [][2]string{{"f", `in:1,2`}}, `{}`},
{"in-int-value", `{"f":1}`, [][2]string{{"f", `in:1,2`}}, `{}`},
{"in-bool-value", `{"f":true}`, [][2]string{{"f", `in:1,2`}}, `{}`},
{"in-array-ok", `{"f":["LP","CD"]}`, [][2]string{{"f", `array|in:LP,CD`}}, `{}`},
{"in-array-bad", `{"f":["LP","X"]}`, [][2]string{{"f", `array|in:LP,CD`}}, `{"f":["The selected f is invalid."]}`},
{"in-array-strict-string", `{"f":["1.0"]}`, [][2]string{{"f", `array|in:1,2`}}, `{"f":["The selected f is invalid."]}`},
{"in-array-ints", `{"f":[1,2]}`, [][2]string{{"f", `array|in:1,2`}}, `{}`},
{"in-array-nested", `{"f":[["LP"]]}`, [][2]string{{"f", `array|in:LP`}}, `{"f":["The selected f is invalid."]}`},
{"in-array-without-array-rule", `{"f":["LP"]}`, [][2]string{{"f", `in:LP`}}, `{"f":["The selected f is invalid."]}`},
{"not-in-ok", `{"x":"a"}`, [][2]string{{"x", `not_in:b,c`}}, `{}`},
{"not-in-fail", `{"x":"b"}`, [][2]string{{"x", `not_in:b,c`}}, `{"x":["The selected x is invalid."]}`},
{"not-in-loose", `{"x":"2.0"}`, [][2]string{{"x", `not_in:2`}}, `{"x":["The selected x is invalid."]}`},
{"not-in-array-some", `{"x":["a","b"]}`, [][2]string{{"x", `array|not_in:b`}}, `{}`},
{"not-in-array-all", `{"x":["b"]}`, [][2]string{{"x", `array|not_in:b`}}, `{"x":["The selected x is invalid."]}`},
{"not-in-array-none", `{"x":["a"]}`, [][2]string{{"x", `array|not_in:b`}}, `{}`},
{"not-in-array-without-array-rule", `{"x":["b"]}`, [][2]string{{"x", `not_in:b`}}, `{}`},
{"string-int", `{"s":123}`, [][2]string{{"s", `string`}}, `{"s":["The s must be a string."]}`},
{"string-null", `{"s":null}`, [][2]string{{"s", `string`}}, `{"s":["The s must be a string."]}`},
{"string-null-nullable", `{"s":null}`, [][2]string{{"s", `nullable|string`}}, `{}`},
{"size-string", `{"s":"abc"}`, [][2]string{{"s", `size:4`}}, `{"s":["The s must be 4 characters."]}`},
{"size-string-ok", `{"s":"abc"}`, [][2]string{{"s", `size:3`}}, `{}`},
{"max-string-chars", `{"s":"ąęś"}`, [][2]string{{"s", `max:2`}}, `{"s":["The s may not be greater than 2 characters."]}`},
{"min-array", `{"a":[1]}`, [][2]string{{"a", `array|min:2`}}, `{"a":["The a must have at least 2 items."]}`},
{"max-array", `{"a":[1,2,3]}`, [][2]string{{"a", `array|max:2`}}, `{"a":["The a may not have more than 2 items."]}`},
{"size-array", `{"a":[1,2]}`, [][2]string{{"a", `array|size:3`}}, `{"a":["The a must contain 3 items."]}`},
{"between-array", `{"a":[]}`, [][2]string{{"a", `array|between:1,2`}}, `{"a":["The a must have between 1 and 2 items."]}`},
{"between-numeric", `{"n":"5"}`, [][2]string{{"n", `numeric|between:1,4`}}, `{"n":["The n must be between 1 and 4."]}`},
{"max-integer", `{"n":5}`, [][2]string{{"n", `integer|max:4`}}, `{"n":["The n may not be greater than 4."]}`},
{"min-numeric-decimal", `{"n":"0.5"}`, [][2]string{{"n", `numeric|min:1`}}, `{"n":["The n must be at least 1."]}`},
{"size-numeric", `{"n":"3"}`, [][2]string{{"n", `numeric|size:4`}}, `{"n":["The n must be 4."]}`},
{"size-numeric-string-without-numeric-rule", `{"n":"12345"}`, [][2]string{{"n", `size:5`}}, `{}`},
{"min-null", `{"n":null}`, [][2]string{{"n", `min:1`}}, `{"n":["The n must be at least 1 characters."]}`},
{"between-string", `{"s":"a"}`, [][2]string{{"s", `string|between:2,4`}}, `{"s":["The s must be between 2 and 4 characters."]}`},
{"max-bool", `{"s":true}`, [][2]string{{"s", `max:0`}}, `{"s":["The s may not be greater than 0 characters."]}`},
{"regex-ok", `{"d":"3:45"}`, [][2]string{{"d", `regex:/^\d{1,3}:[0-5]\d$/`}}, `{}`},
{"regex-bad", `{"d":"3:75"}`, [][2]string{{"d", `regex:/^\d{1,3}:[0-5]\d$/`}}, `{"d":["The d format is invalid."]}`},
{"regex-number", `{"d":345}`, [][2]string{{"d", `regex:/^\d+$/`}}, `{}`},
{"regex-array", `{"d":["x"]}`, [][2]string{{"d", `regex:/x/`}}, `{"d":["The d format is invalid."]}`},
{"regex-bool", `{"d":true}`, [][2]string{{"d", `regex:/1/`}}, `{"d":["The d format is invalid."]}`},
{"not-regex", `{"d":"abc"}`, [][2]string{{"d", `not_regex:/a/`}}, `{"d":["The d format is invalid."]}`},
{"not-regex-ok", `{"d":"xyz"}`, [][2]string{{"d", `not_regex:/a/`}}, `{}`},
{"regex-flags", `{"d":"ABC"}`, [][2]string{{"d", `regex:/^abc$/i`}}, `{}`},
{"regex-hash-delimiter", `{"d":"a/b"}`, [][2]string{{"d", `regex:#^a/b$#`}}, `{}`},
{"date-feb30", `{"d":"2020-02-30"}`, [][2]string{{"d", `date`}}, `{"d":["The d is not a valid date."]}`},
{"date-leap", `{"d":"2020-02-29"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-garbage", `{"d":"garbage"}`, [][2]string{{"d", `date`}}, `{"d":["The d is not a valid date."]}`},
{"date-int", `{"d":12345}`, [][2]string{{"d", `date`}}, `{"d":["The d is not a valid date."]}`},
{"date-slash", `{"d":"2020/02/29"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-us", `{"d":"02/29/2020"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-dot", `{"d":"29.02.2020"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-dash-dmy", `{"d":"29-02-2020"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-iso-z", `{"d":"2020-02-29T10:00:00Z"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-iso-offset", `{"d":"2020-02-29T10:00:00+02:00"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-bad-hour", `{"d":"2020-02-29 25:00"}`, [][2]string{{"d", `date`}}, `{"d":["The d is not a valid date."]}`},
{"date-with-time", `{"d":"2020-02-29 10:30"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-with-seconds", `{"d":"2020-02-29 10:30:15"}`, [][2]string{{"d", `date`}}, `{}`},
{"date-month13", `{"d":"2020-13-01"}`, [][2]string{{"d", `date`}}, `{"d":["The d is not a valid date."]}`},
{"date-array", `{"d":["2020-01-01"]}`, [][2]string{{"d", `date`}}, `{"d":["The d is not a valid date."]}`},
{"after-same", `{"d":"2020-01-01"}`, [][2]string{{"d", `after:2020-01-01`}}, `{"d":["The d must be a date after 2020-01-01."]}`},
{"after-or-equal-same", `{"d":"2020-01-01"}`, [][2]string{{"d", `after_or_equal:2020-01-01`}}, `{}`},
{"before-ok", `{"d":"2019-12-31"}`, [][2]string{{"d", `before:2020-01-01`}}, `{}`},
{"before-or-equal-future", `{"d":"2099-01-01"}`, [][2]string{{"d", `before_or_equal:tomorrow`}}, `{"d":["The d must be a date before or equal to tomorrow."]}`},
{"before-or-equal-past", `{"d":"2001-01-01"}`, [][2]string{{"d", `before_or_equal:tomorrow`}}, `{}`},
{"after-or-equal-1900", `{"d":"1899-12-31"}`, [][2]string{{"d", `after_or_equal:1900-01-01`}}, `{"d":["The d must be a date after or equal to 1900-01-01."]}`},
{"after-field-bad", `{"start":"2020-01-01","end":"2019-01-01"}`, [][2]string{{"end", `after:start`}}, `{"end":["The end must be a date after start."]}`},
{"after-field-ok", `{"start":"2020-01-01","end":"2021-01-01"}`, [][2]string{{"end", `after:start`}}, `{}`},
{"after-unparsable", `{"d":"garbage"}`, [][2]string{{"d", `after:2020-01-01`}}, `{"d":["The d must be a date after 2020-01-01."]}`},
{"after-missing-field", `{"d":"2020-01-01"}`, [][2]string{{"d", `after:nofield`}}, `{}`},
{"before-int", `{"d":5}`, [][2]string{{"d", `before:2020-01-01`}}, `{}`},
{"after-yesterday", `{"d":"2001-01-01"}`, [][2]string{{"d", `after:yesterday`}}, `{"d":["The d must be a date after yesterday."]}`},
{"before-today", `{"d":"2001-01-01"}`, [][2]string{{"d", `before:today`}}, `{}`},
{"after-now", `{"d":"2099-01-01 10:00"}`, [][2]string{{"d", `after:now`}}, `{}`},
{"url-https", `{"u":"https://example.com/x?y=1"}`, [][2]string{{"u", `url`}}, `{}`},
{"url-bare", `{"u":"notaurl"}`, [][2]string{{"u", `url`}}, `{"u":["The u format is invalid."]}`},
{"url-localhost-port", `{"u":"http://localhost:8000"}`, [][2]string{{"u", `url`}}, `{}`},
{"url-ftp", `{"u":"ftp://a.b"}`, [][2]string{{"u", `url`}}, `{}`},
{"url-space", `{"u":"https://exa mple.com"}`, [][2]string{{"u", `url`}}, `{"u":["The u format is invalid."]}`},
{"url-int", `{"u":5}`, [][2]string{{"u", `url`}}, `{"u":["The u format is invalid."]}`},
{"filled-null", `{"x":null}`, [][2]string{{"x", `filled`}}, `{"x":["The x field must have a value."]}`},
{"filled-missing", `{}`, [][2]string{{"x", `filled`}}, `{}`},
{"filled-blank", `{"x":""}`, [][2]string{{"x", `filled`}}, `{"x":["The x field must have a value."]}`},
{"nullable-blank", `{"x":""}`, [][2]string{{"x", `nullable|string|max:2`}}, `{}`},
{"sometimes-missing", `{}`, [][2]string{{"x", `sometimes|required`}}, `{}`},
{"sometimes-present-null", `{"x":null}`, [][2]string{{"x", `sometimes|required`}}, `{"x":["The x field is required."]}`},
{"integer-blank", `{"x":""}`, [][2]string{{"x", `integer`}}, `{}`},
{"present-missing", `{}`, [][2]string{{"x", `present`}}, `{"x":["The x field must be present."]}`},
{"present-null", `{"x":null}`, [][2]string{{"x", `present`}}, `{}`},
{"required-whitespace", `{"x":" "}`, [][2]string{{"x", `required`}}, `{"x":["The x field is required."]}`},
{"required-empty-array", `{"x":[]}`, [][2]string{{"x", `required`}}, `{"x":["The x field is required."]}`},
{"required-zero", `{"x":0}`, [][2]string{{"x", `required`}}, `{}`},
{"required-false", `{"x":false}`, [][2]string{{"x", `required`}}, `{}`},
{"nested-wildcard", `{"a":[{"b":[{"c":""}]},{"b":[{"c":"x"},{"c":null}]}]}`, [][2]string{{"a.*.b.*.c", `required`}}, `{"a.0.b.0.c":["The a.0.b.0.c field is required."],"a.1.b.1.c":["The a.1.b.1.c field is required."]}`},
{"map-wildcard", `{"a":{"k2":{"c":"v"},"k1":{"c":""}}}`, [][2]string{{"a.*.c", `required`}}, `{"a.k1.c":["The a.k1.c field is required."]}`},
{"wildcard-missing-mid", `{"a":[{"x":1}]}`, [][2]string{{"a.*.b.*.c", `required`}}, `{}`},
{"wildcard-scalar-parent", `{"a":"str"}`, [][2]string{{"a.*", `required`}}, `{}`},
{"wildcard-in", `{"f":["LP","X","CD","Y"]}`, [][2]string{{"f", `array`}, {"f.*", `in:LP,CD`}}, `{"f.1":["The selected f.1 is invalid."],"f.3":["The selected f.3 is invalid."]}`},
{"wildcard-numeric-keys", `{"t":[{"title":"a","duration":"3:75"},{"title":"","duration":"1:00"}]}`, [][2]string{{"t.*.title", `required|string|max:255`}, {"t.*.duration", `nullable|string|regex:/^\d{1,3}:[0-5]\d$/`}}, `{"t.1.title":["The t.1.title field is required."],"t.0.duration":["The t.0.duration format is invalid."]}`},
{"wildcard-trailing", `{"a":["x",""]}`, [][2]string{{"a.*", `required`}}, `{"a.1":["The a.1 field is required."]}`},
{"bail", `{"x":"ab"}`, [][2]string{{"x", `bail|integer|min:5`}}, `{"x":["The x must be an integer."]}`},
{"no-bail", `{"x":"ab"}`, [][2]string{{"x", `integer|min:5`}}, `{"x":["The x must be an integer.","The x must be at least 5."]}`},
{"implicit-stop", `{}`, [][2]string{{"x", `required|integer|min:5`}}, `{"x":["The x field is required."]}`},
{"order", `{"b":"","a":""}`, [][2]string{{"b", `required`}, {"a", `required`}}, `{"b":["The b field is required."],"a":["The a field is required."]}`},
}
func TestValidateRulesMatchLaravel(t *testing.T) {
for _, tc := range laravelRuleCases {
t.Run(tc.name, func(t *testing.T) {
dec := json.NewDecoder(strings.NewReader(tc.data))
dec.UseNumber()
var input map[string]any
if err := dec.Decode(&input); err != nil {
t.Fatal(err)
}
var rules []RequestRule
for _, r := range tc.rules {
rules = append(rules, RequestRule{Field: r[0], Rules: ParseRules(r[1])})
}
got := mustValidate(t, inLocale("en"), input, rules)
var want map[string][]string
if err := json.Unmarshal([]byte(tc.want), &want); err != nil {
t.Fatal(err)
}
if len(want) == 0 && len(got) == 0 {
return
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("got %v\nwant %v (PHP)", got, want)
}
})
}
}