@orieljs/validation 0.1.0-alpha.5 → 0.1.0-alpha.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -71,15 +71,17 @@ var FormRequest = class {
71
71
  /**
72
72
  * `const data = await StorePostRequest.validate()` (spec §4.3). Resolves the current request and the
73
73
  * ValidationFactory from the active container, runs authorize() first (throwing AuthorizationErrorImpl if it
74
- * returns false), then validates request.all() against rules()/messages()/attributes() and returns the typed
75
- * validated data (or throws the 422 ValidationError — see ValidatorImpl.validated()).
74
+ * returns false), then validates request.all() against rules()/messages()/attributes() and returns the validated
75
+ * data (or throws the 422 ValidationError — see ValidatorImpl.validated()). The data is typed from rules(): write
76
+ * it without a return type and with `as const` (`return { title: 'required|string' } as const`), so its rule
77
+ * strings keep their text; a rules() typed `Rules` gives `Record<string, unknown>`.
76
78
  */
77
79
  static async validate() {
78
80
  const instance = new this();
79
81
  if (!await instance.authorize()) throw new AuthorizationErrorImpl();
80
82
  const request = inject(HttpRequest);
81
83
  const factory = inject(ValidationFactory);
82
- const validator = factory.make(request.all(), instance.rules(), instance.messages(), instance.attributes());
84
+ const validator = factory.make(request.all(), await instance.rules(), instance.messages(), instance.attributes());
83
85
  return validator.validated();
84
86
  }
85
87
  };
@@ -87,7 +89,7 @@ var FormRequest = class {
87
89
  // messages.ts
88
90
  import { Str } from "@orieljs/support";
89
91
  function defaultAttributeName(key) {
90
- return key.split(".").map((segment) => Str.snake(segment).replace(/_/g, " ")).join(".");
92
+ return key.split(".").map((segment) => segment === "*" ? segment : Str.snake(segment).replace(/_/g, " ")).join(".");
91
93
  }
92
94
  function applyReplacements(template, replacements) {
93
95
  return Object.entries(replacements).reduce((message, [key, value]) => message.replaceAll(`:${key}`, value), template);
@@ -100,18 +102,59 @@ var DEFAULT_MESSAGES = {
100
102
  required_with_all: "The :attribute field is required when :values are present.",
101
103
  required_without: "The :attribute field is required when :values is not present.",
102
104
  required_without_all: "The :attribute field is required when none of :values are present.",
105
+ required_if_accepted: "The :attribute field is required when :other is accepted.",
106
+ required_if_declined: "The :attribute field is required when :other is declined.",
107
+ present: "The :attribute field must be present.",
108
+ present_if: "The :attribute field must be present when :other is :value.",
109
+ present_unless: "The :attribute field must be present unless :other is :value.",
110
+ present_with: "The :attribute field must be present when :values is present.",
111
+ present_with_all: "The :attribute field must be present when :values are present.",
112
+ filled: "The :attribute field must have a value.",
113
+ missing: "The :attribute field must be missing.",
114
+ missing_if: "The :attribute field must be missing when :other is :value.",
115
+ missing_unless: "The :attribute field must be missing unless :other is :value.",
116
+ missing_with: "The :attribute field must be missing when :values is present.",
117
+ missing_with_all: "The :attribute field must be missing when :values are present.",
118
+ prohibited: "The :attribute field is prohibited.",
119
+ prohibited_if: "The :attribute field is prohibited when :other is :value.",
120
+ prohibited_if_accepted: "The :attribute field is prohibited when :other is accepted.",
121
+ prohibited_if_declined: "The :attribute field is prohibited when :other is declined.",
122
+ prohibited_unless: "The :attribute field is prohibited unless :other is in :values.",
123
+ prohibits: "The :attribute field prohibits :other from being present.",
103
124
  string: "The :attribute field must be a string.",
104
125
  integer: "The :attribute field must be an integer.",
105
126
  numeric: "The :attribute field must be a number.",
106
127
  boolean: "The :attribute field must be true or false.",
107
128
  array: "The :attribute field must be an array.",
129
+ list: "The :attribute field must be a list.",
130
+ required_array_keys: "The :attribute field must contain entries for: :values.",
131
+ in_array_keys: "The :attribute field must contain at least one of the following keys: :values.",
132
+ contains: "The :attribute field is missing a required value.",
133
+ doesnt_contain: "The :attribute field must not contain any of the following: :values.",
134
+ distinct: "The :attribute field has a duplicate value.",
135
+ in_array: "The :attribute field must exist in :other.",
108
136
  email: "The :attribute field must be a valid email address.",
109
137
  url: "The :attribute field must be a valid URL.",
110
138
  uuid: "The :attribute field must be a valid UUID.",
139
+ ulid: "The :attribute field must be a valid ULID.",
140
+ ascii: "The :attribute field must only contain single-byte alphanumeric characters and symbols.",
141
+ hex_color: "The :attribute field must be a valid hexadecimal color.",
142
+ json: "The :attribute field must be a valid JSON string.",
143
+ ip: "The :attribute field must be a valid IP address.",
144
+ ipv4: "The :attribute field must be a valid IPv4 address.",
145
+ ipv6: "The :attribute field must be a valid IPv6 address.",
146
+ mac_address: "The :attribute field must be a valid MAC address.",
147
+ active_url: "The :attribute field must be a valid URL.",
148
+ timezone: "The :attribute field must be a valid timezone.",
149
+ starts_with: "The :attribute field must start with one of the following: :values.",
150
+ ends_with: "The :attribute field must end with one of the following: :values.",
151
+ doesnt_start_with: "The :attribute field must not start with one of the following: :values.",
152
+ doesnt_end_with: "The :attribute field must not end with one of the following: :values.",
111
153
  alpha: "The :attribute field must only contain letters.",
112
154
  alpha_num: "The :attribute field must only contain letters and numbers.",
113
- alpha_dash: "The :attribute field must only contain letters, numbers, dashes and underscores.",
155
+ alpha_dash: "The :attribute field must only contain letters, numbers, dashes, and underscores.",
114
156
  regex: "The :attribute field format is invalid.",
157
+ not_regex: "The :attribute field format is invalid.",
115
158
  lowercase: "The :attribute field must be lowercase.",
116
159
  uppercase: "The :attribute field must be uppercase.",
117
160
  current_password: "The password is incorrect.",
@@ -121,14 +164,27 @@ var DEFAULT_MESSAGES = {
121
164
  "password.symbols": "The :attribute field must contain at least one symbol.",
122
165
  confirmed: "The :attribute field confirmation does not match.",
123
166
  same: "The :attribute field must match :other.",
124
- different: "The :attribute field must be different from :other.",
167
+ different: "The :attribute field and :other must be different.",
125
168
  accepted: "The :attribute field must be accepted.",
169
+ accepted_if: "The :attribute field must be accepted when :other is :value.",
170
+ declined: "The :attribute field must be declined.",
171
+ declined_if: "The :attribute field must be declined when :other is :value.",
126
172
  digits: "The :attribute field must be :digits digits.",
173
+ digits_between: "The :attribute field must be between :min and :max digits.",
174
+ max_digits: "The :attribute field must not have more than :max digits.",
175
+ min_digits: "The :attribute field must have at least :min digits.",
176
+ decimal: "The :attribute field must have :decimal decimal places.",
177
+ multiple_of: "The :attribute field must be a multiple of :value.",
127
178
  date: "The :attribute field must be a valid date.",
128
179
  before: "The :attribute field must be a date before :date.",
180
+ before_or_equal: "The :attribute field must be a date before or equal to :date.",
129
181
  after: "The :attribute field must be a date after :date.",
182
+ after_or_equal: "The :attribute field must be a date after or equal to :date.",
183
+ date_equals: "The :attribute field must be a date equal to :date.",
184
+ date_format: "The :attribute field must match the format :format.",
130
185
  in: "The selected :attribute is invalid.",
131
186
  not_in: "The selected :attribute is invalid.",
187
+ enum: "The selected :attribute is invalid.",
132
188
  unique: "The :attribute has already been taken.",
133
189
  exists: "The selected :attribute is invalid.",
134
190
  file: "The :attribute field must be a file.",
@@ -152,13 +208,29 @@ var DEFAULT_MESSAGES = {
152
208
  "size.string": "The :attribute field must be :size characters.",
153
209
  "size.numeric": "The :attribute field must be :size.",
154
210
  "size.array": "The :attribute field must contain :size items.",
155
- "size.file": "The :attribute field must be :size kilobytes."
211
+ "size.file": "The :attribute field must be :size kilobytes.",
212
+ "gt.array": "The :attribute field must have more than :value items.",
213
+ "gt.file": "The :attribute field must be greater than :value kilobytes.",
214
+ "gt.numeric": "The :attribute field must be greater than :value.",
215
+ "gt.string": "The :attribute field must be greater than :value characters.",
216
+ "gte.array": "The :attribute field must have :value items or more.",
217
+ "gte.file": "The :attribute field must be greater than or equal to :value kilobytes.",
218
+ "gte.numeric": "The :attribute field must be greater than or equal to :value.",
219
+ "gte.string": "The :attribute field must be greater than or equal to :value characters.",
220
+ "lt.array": "The :attribute field must have less than :value items.",
221
+ "lt.file": "The :attribute field must be less than :value kilobytes.",
222
+ "lt.numeric": "The :attribute field must be less than :value.",
223
+ "lt.string": "The :attribute field must be less than :value characters.",
224
+ "lte.array": "The :attribute field must not have more than :value items.",
225
+ "lte.file": "The :attribute field must be less than or equal to :value kilobytes.",
226
+ "lte.numeric": "The :attribute field must be less than or equal to :value.",
227
+ "lte.string": "The :attribute field must be less than or equal to :value characters."
156
228
  };
157
229
 
158
230
  // MessageBag.ts
159
231
  var MessageBagImpl = class {
160
- items = {};
161
- /** Not part of the MessageBag contract — used internally by ValidatorImpl and by after() hooks. */
232
+ // No prototype: a field named `constructor` or `toString` is a key like any other.
233
+ items = /* @__PURE__ */ Object.create(null);
162
234
  add(key, message) {
163
235
  ;
164
236
  (this.items[key] ??= []).push(message);
@@ -190,6 +262,294 @@ var MessageBagImpl = class {
190
262
  }
191
263
  };
192
264
 
265
+ // rules/acceptance.ts
266
+ import { Arr as Arr3 } from "@orieljs/support";
267
+
268
+ // parse.ts
269
+ import { Arr } from "@orieljs/support";
270
+ function splitParameters(name, raw) {
271
+ return name === "regex" || name === "not_regex" ? [raw] : raw.split(",");
272
+ }
273
+ function splitOnce(value, separator) {
274
+ const index = value.indexOf(separator);
275
+ return index === -1 ? [value, void 0] : [value.slice(0, index), value.slice(index + 1)];
276
+ }
277
+ function isExpandingRule(item) {
278
+ return typeof item === "object" && item !== null && typeof item.expandRules === "function";
279
+ }
280
+ function parseRuleSpec(spec, data = {}) {
281
+ const items = typeof spec === "string" ? spec.split("|") : spec;
282
+ return items.filter((item) => item !== "").flatMap((item) => {
283
+ if (isExpandingRule(item)) return parseRuleSpec(item.expandRules(data), data);
284
+ if (typeof item !== "string") return [{ kind: "object", rule: item }];
285
+ const [name, raw] = splitOnce(item, ":");
286
+ return [{ kind: "named", name, parameters: raw === void 0 ? [] : splitParameters(name, raw) }];
287
+ });
288
+ }
289
+ function isEmptyValue(value) {
290
+ if (value === null || value === void 0) return true;
291
+ if (typeof value === "string") return value.trim() === "";
292
+ if (Array.isArray(value)) return value.length === 0;
293
+ if (typeof value === "object") {
294
+ const proto = Object.getPrototypeOf(value);
295
+ return (proto === Object.prototype || proto === null) && Object.keys(value).length === 0;
296
+ }
297
+ return false;
298
+ }
299
+ function isUploadedFileLike(value) {
300
+ return typeof value === "object" && value !== null && typeof value.clientName === "string" && typeof value.arrayBuffer === "function";
301
+ }
302
+ var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
303
+ function expandWildcardKeys(pattern, data) {
304
+ const segments = pattern.split(".");
305
+ let paths = [""];
306
+ for (const segment of segments) {
307
+ const next = [];
308
+ for (const base of paths) {
309
+ const prefix = base === "" ? "" : `${base}.`;
310
+ if (segment !== "*") {
311
+ next.push(`${prefix}${segment}`);
312
+ continue;
313
+ }
314
+ const container = Arr.get(data, base === "" ? void 0 : base);
315
+ if (Array.isArray(container)) {
316
+ container.forEach((_, index) => next.push(`${prefix}${index}`));
317
+ } else if (container !== null && typeof container === "object") {
318
+ Object.keys(container).filter((key) => !UNSAFE_KEYS.has(key)).forEach((key) => next.push(`${prefix}${key}`));
319
+ }
320
+ }
321
+ paths = next;
322
+ }
323
+ return paths;
324
+ }
325
+
326
+ // rules/dependent.ts
327
+ import { Arr as Arr2 } from "@orieljs/support";
328
+
329
+ // rules/type.ts
330
+ var BOOLEAN_VALUES = [true, false, 0, 1, "0", "1"];
331
+ var NUMERIC_STRING_RE = /^\s*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\s*$/;
332
+ function isNumeric(value) {
333
+ if (typeof value === "number") return Number.isFinite(value);
334
+ return typeof value === "string" && NUMERIC_STRING_RE.test(value);
335
+ }
336
+ function isPlainObject(value) {
337
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
338
+ const proto = Object.getPrototypeOf(value);
339
+ return proto === Object.prototype || proto === null;
340
+ }
341
+ var typeRules = {
342
+ string: { validate: (ctx) => typeof ctx.value === "string", messageKey: () => "string" },
343
+ /** `integer:strict` refuses the text of a number (Laravel's is_int()). */
344
+ integer: {
345
+ validate: (ctx) => ctx.parameters[0] === "strict" ? Number.isInteger(ctx.value) : typeof ctx.value !== "object" && /^-?\d+$/.test(String(ctx.value).trim()),
346
+ messageKey: () => "integer"
347
+ },
348
+ /** `numeric:strict` refuses the text of a number. */
349
+ numeric: {
350
+ validate: (ctx) => ctx.parameters[0] === "strict" ? typeof ctx.value === "number" && Number.isFinite(ctx.value) : isNumeric(ctx.value),
351
+ messageKey: () => "numeric"
352
+ },
353
+ /** `boolean:strict` lets only true and false through. */
354
+ boolean: {
355
+ validate: (ctx) => ctx.parameters[0] === "strict" ? typeof ctx.value === "boolean" : BOOLEAN_VALUES.includes(ctx.value),
356
+ messageKey: () => "boolean"
357
+ },
358
+ /**
359
+ * `array`: a list. `array:name,email`: an object (PHP's associative array) whose keys are all among those named, as
360
+ * Laravel's validateArray() with keys.
361
+ */
362
+ array: {
363
+ validate: (ctx) => ctx.parameters.length === 0 ? Array.isArray(ctx.value) : isPlainObject(ctx.value) && Object.keys(ctx.value).every((key) => ctx.parameters.includes(key)),
364
+ messageKey: () => "array"
365
+ }
366
+ };
367
+
368
+ // rules/dependent.ts
369
+ function truthy(value) {
370
+ if (typeof value === "string") return value !== "" && value !== "0";
371
+ if (Array.isArray(value)) return value.length > 0;
372
+ return Boolean(value);
373
+ }
374
+ function looseEquals(value, parameter) {
375
+ if (typeof parameter === "boolean") return truthy(value) === parameter;
376
+ if (typeof value === "number") return isNumeric(parameter) ? Number(parameter) === value : String(value) === parameter;
377
+ if (typeof value === "string") return isNumeric(value) && isNumeric(parameter) ? Number(value) === Number(parameter) : value === parameter;
378
+ return false;
379
+ }
380
+ function otherFieldMatches(ctx) {
381
+ const [field = "", ...listed4] = ctx.parameters;
382
+ const other = Arr2.get(ctx.data, field);
383
+ let values = listed4;
384
+ if (typeof other === "boolean" || ctx.getRule?.(field, "boolean") !== void 0) {
385
+ values = values.map((value) => value === "true" ? true : value === "false" ? false : value);
386
+ }
387
+ if (other === null || other === void 0) {
388
+ values = values.map((value) => typeof value === "string" && value.toLowerCase() === "null" ? null : value);
389
+ return values.includes(null);
390
+ }
391
+ if (typeof other === "boolean") return values.includes(other);
392
+ return values.some((value) => value !== null && looseEquals(other, value));
393
+ }
394
+ function displayableValue(value) {
395
+ if (typeof value === "boolean") return value ? "true" : "false";
396
+ if (value === null || value === void 0) return "empty";
397
+ if (typeof value === "object") return "array";
398
+ return String(value);
399
+ }
400
+ function displayName(ctx, field) {
401
+ return ctx.displayName?.(field) ?? defaultAttributeName(field);
402
+ }
403
+ function otherAndValue(ctx) {
404
+ const field = ctx.parameters[0] ?? "";
405
+ return { other: displayName(ctx, field), value: displayableValue(Arr2.get(ctx.data, field)) };
406
+ }
407
+ function otherAndValues(ctx) {
408
+ return { other: displayName(ctx, ctx.parameters[0] ?? ""), values: ctx.parameters.slice(1).join(", ") };
409
+ }
410
+ function fieldList(ctx) {
411
+ return { values: ctx.parameters.map((field) => displayName(ctx, field)).join(" / ") };
412
+ }
413
+
414
+ // rules/acceptance.ts
415
+ var ACCEPTED_VALUES = ["yes", "on", 1, "1", true, "true"];
416
+ var DECLINED_VALUES = ["no", "off", 0, "0", false, "false"];
417
+ var isAccepted = (value) => !isEmptyValue(value) && ACCEPTED_VALUES.includes(value);
418
+ var isDeclined = (value) => !isEmptyValue(value) && DECLINED_VALUES.includes(value);
419
+ var otherAccepted = (data, field = "") => isAccepted(Arr3.get(data, field));
420
+ var otherDeclined = (data, field = "") => isDeclined(Arr3.get(data, field));
421
+ var acceptanceRules = {
422
+ accepted: { implicit: true, validate: (ctx) => isAccepted(ctx.value), messageKey: () => "accepted" },
423
+ accepted_if: {
424
+ implicit: true,
425
+ validate: (ctx) => !otherFieldMatches(ctx) || isAccepted(ctx.value),
426
+ messageKey: () => "accepted_if",
427
+ replacements: otherAndValue
428
+ },
429
+ declined: { implicit: true, validate: (ctx) => isDeclined(ctx.value), messageKey: () => "declined" },
430
+ declined_if: {
431
+ implicit: true,
432
+ validate: (ctx) => !otherFieldMatches(ctx) || isDeclined(ctx.value),
433
+ messageKey: () => "declined_if",
434
+ replacements: otherAndValue
435
+ }
436
+ };
437
+
438
+ // rules/array.ts
439
+ import { Arr as Arr4 } from "@orieljs/support";
440
+ function alike(a, b, strict = false) {
441
+ if (strict || a === b) return a === b;
442
+ if (typeof b === "string" || typeof b === "boolean") return looseEquals(a, b);
443
+ if (typeof a === "string" || typeof a === "boolean") return looseEquals(b, a);
444
+ return false;
445
+ }
446
+ var listed = (values) => ({ values: values.join(", ") });
447
+ function matches(value, other, comparison3) {
448
+ if (comparison3 === "strict") return value === other;
449
+ if (comparison3 === "loose") return alike(value, other);
450
+ return (typeof other === "string" || typeof other === "number") && String(other).toLowerCase() === String(value).toLowerCase();
451
+ }
452
+ function looseClass(value) {
453
+ if (typeof value === "number") return Number.isFinite(value) ? `n:${value}` : void 0;
454
+ if (typeof value === "string") return isNumeric(value) ? `n:${Number(value)}` : `s:${value}`;
455
+ return void 0;
456
+ }
457
+ var increment = (counts, key) => counts.set(key, (counts.get(key) ?? 0) + 1);
458
+ var PatternValues = class _PatternValues {
459
+ constructor(values) {
460
+ this.values = values;
461
+ for (const value of values) {
462
+ increment(this.identical, value);
463
+ if (typeof value === "string" || typeof value === "number") increment(this.text, String(value).toLowerCase());
464
+ const loose = looseClass(value);
465
+ if (loose !== void 0) increment(this.classes, loose);
466
+ else if (typeof value === "number") this.unclassed.push(value);
467
+ if (typeof value === "boolean") increment(this.booleans, value);
468
+ if (typeof value !== "string") increment(this.truthiness, truthy(value));
469
+ }
470
+ }
471
+ values;
472
+ /** By identity (SameValueZero): strict comparison, and null, undefined and objects under loose comparison. */
473
+ identical = /* @__PURE__ */ new Map();
474
+ /** Strings and numbers by their lower-cased text. */
475
+ text = /* @__PURE__ */ new Map();
476
+ /** Strings and finite numbers by looseClass(). */
477
+ classes = /* @__PURE__ */ new Map();
478
+ /** Booleans by value: what a string, number, null or object equals loosely besides its own class. */
479
+ booleans = /* @__PURE__ */ new Map();
480
+ /** Every value but a string, by PHP truthiness: what a boolean equals loosely. */
481
+ truthiness = /* @__PURE__ */ new Map();
482
+ /** NaN and ±Infinity, compared one by one. */
483
+ unclassed = [];
484
+ /** How many of the values `value` matches under the comparison. */
485
+ count(value, comparison3) {
486
+ if (comparison3 === "strict") return Number.isNaN(value) ? 0 : this.identical.get(value) ?? 0;
487
+ if (comparison3 === "ignore_case") return this.text.get(String(value).toLowerCase()) ?? 0;
488
+ if (typeof value === "boolean") return this.truthiness.get(value) ?? 0;
489
+ const loose = looseClass(value);
490
+ if (loose === void 0 && typeof value === "number") return this.values.filter((other) => alike(value, other)).length;
491
+ const own = loose === void 0 ? this.identical.get(value) ?? 0 : (this.classes.get(loose) ?? 0) + this.unclassed.filter((other) => alike(value, other)).length;
492
+ return own + (this.booleans.get(truthy(value)) ?? 0);
493
+ }
494
+ /** The values of the fields `pattern` names in the context's data, gathered once per validation pass. */
495
+ static of(ctx, pattern) {
496
+ const key = `array:values:${pattern}`;
497
+ const cached = ctx.cache?.get(key);
498
+ if (cached instanceof _PatternValues) return cached;
499
+ const fields = expandWildcardKeys(pattern, ctx.data).filter((field) => Arr4.has(ctx.data, field));
500
+ const values = new _PatternValues(fields.map((field) => Arr4.get(ctx.data, field)));
501
+ ctx.cache?.set(key, values);
502
+ return values;
503
+ }
504
+ };
505
+ var arrayRules = {
506
+ list: { validate: (ctx) => Array.isArray(ctx.value), messageKey: () => "list" },
507
+ required_array_keys: {
508
+ validate: (ctx) => isPlainObject(ctx.value) && ctx.parameters.every((key) => Object.hasOwn(ctx.value, key)),
509
+ messageKey: () => "required_array_keys",
510
+ replacements: (ctx) => listed(ctx.parameters)
511
+ },
512
+ in_array_keys: {
513
+ validate: (ctx) => isPlainObject(ctx.value) && ctx.parameters.some((key) => Object.hasOwn(ctx.value, key)),
514
+ messageKey: () => "in_array_keys",
515
+ replacements: (ctx) => listed(ctx.parameters)
516
+ },
517
+ /** Every value named is an item of the array (compared as PHP's in_array() does). */
518
+ contains: {
519
+ validate: (ctx) => {
520
+ const items = ctx.value;
521
+ return Array.isArray(items) && ctx.parameters.every((needle) => items.some((item) => alike(item, needle)));
522
+ },
523
+ messageKey: () => "contains"
524
+ },
525
+ doesnt_contain: {
526
+ validate: (ctx) => {
527
+ const items = ctx.value;
528
+ return Array.isArray(items) && !ctx.parameters.some((needle) => items.some((item) => alike(item, needle)));
529
+ },
530
+ messageKey: () => "doesnt_contain",
531
+ replacements: (ctx) => listed(ctx.parameters)
532
+ },
533
+ /**
534
+ * Laravel's validateDistinct(): no other field under the same wildcard holds this value. `distinct:strict` compares
535
+ * with `===`; `distinct:ignore_case` compares text case-insensitively.
536
+ */
537
+ distinct: {
538
+ validate: (ctx) => {
539
+ const comparison3 = ctx.parameters.includes("ignore_case") ? "ignore_case" : ctx.parameters.includes("strict") ? "strict" : "loose";
540
+ const own = Arr4.has(ctx.data, ctx.attribute) && matches(ctx.value, Arr4.get(ctx.data, ctx.attribute), comparison3) ? 1 : 0;
541
+ return PatternValues.of(ctx, ctx.pattern ?? ctx.attribute).count(ctx.value, comparison3) - own === 0;
542
+ },
543
+ messageKey: () => "distinct"
544
+ },
545
+ /** The value is one of the values of the fields the parameter names (`in_array:colors.*`). */
546
+ in_array: {
547
+ validate: (ctx) => PatternValues.of(ctx, ctx.parameters[0] ?? "").count(ctx.value, "loose") > 0,
548
+ messageKey: () => "in_array",
549
+ replacements: (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") })
550
+ }
551
+ };
552
+
193
553
  // rules/auth.ts
194
554
  import { AuthFactory, Hasher } from "@orieljs/contracts";
195
555
  import { inject as inject2 } from "@orieljs/container";
@@ -238,6 +598,457 @@ var databaseRules = {
238
598
  }
239
599
  };
240
600
 
601
+ // rules/date.ts
602
+ import { Arr as Arr5 } from "@orieljs/support";
603
+
604
+ // dates/phpFormat.ts
605
+ var DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
606
+ var MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
607
+ var UNSUPPORTED = /* @__PURE__ */ new Set(["z", "W", "t", "L", "o", "X", "x", "B", "e", "I", "T", "Z", "c", "r", "N", "w", "#", "?", "*", "+"]);
608
+ var pad = (value, length2) => String(Math.abs(value)).padStart(length2, "0");
609
+ function suffix(day) {
610
+ if (day % 100 >= 11 && day % 100 <= 13) return "th";
611
+ return ["th", "st", "nd", "rd"][day % 10] ?? "th";
612
+ }
613
+ function formatOffset(offset, colon) {
614
+ const sign = offset < 0 ? "-" : "+";
615
+ return `${sign}${pad(Math.trunc(Math.abs(offset) / 60), 2)}${colon ? ":" : ""}${pad(Math.abs(offset) % 60, 2)}`;
616
+ }
617
+ function parse(format2, value) {
618
+ const fields = { year: 1970, month: 1, day: 1, hour: 0, minute: 0, second: 0, micro: 0, offset: void 0, unix: void 0 };
619
+ let meridiem;
620
+ let twelveHour = false;
621
+ let position = 0;
622
+ const digits = (min, max) => {
623
+ const match = new RegExp(`^\\d{${min},${max}}`).exec(value.slice(position));
624
+ if (!match) return void 0;
625
+ position += match[0].length;
626
+ return Number(match[0]);
627
+ };
628
+ const word = (names, short) => {
629
+ const match = /^[A-Za-z]+/.exec(value.slice(position));
630
+ if (!match) return void 0;
631
+ const text = match[0].toLowerCase();
632
+ const index = names.findIndex((name) => name.toLowerCase() === text || short && name.slice(0, 3).toLowerCase() === text);
633
+ if (index === -1) return void 0;
634
+ position += match[0].length;
635
+ return index;
636
+ };
637
+ const offset = (colon) => {
638
+ const rest = value.slice(position);
639
+ if (colon === "z" && rest.startsWith("Z")) {
640
+ position += 1;
641
+ return 0;
642
+ }
643
+ const match = (colon === "none" ? /^([+-])(\d{2})(\d{2})/ : /^([+-])(\d{2}):(\d{2})/).exec(rest);
644
+ if (!match) return void 0;
645
+ position += match[0].length;
646
+ return (match[1] === "-" ? -1 : 1) * (Number(match[2]) * 60 + Number(match[3]));
647
+ };
648
+ for (let i = 0; i < format2.length; i++) {
649
+ const char = format2[i];
650
+ let read = 0;
651
+ switch (char) {
652
+ case "!":
653
+ case "|":
654
+ continue;
655
+ case "\\":
656
+ i++;
657
+ if (value[position] !== format2[i]) return void 0;
658
+ position++;
659
+ continue;
660
+ case "d":
661
+ case "j":
662
+ read = digits(1, 2);
663
+ if (read !== void 0) fields.day = read;
664
+ break;
665
+ case "D":
666
+ case "l":
667
+ read = word(DAYS, true);
668
+ break;
669
+ case "S":
670
+ if (!/^(st|nd|rd|th)/i.test(value.slice(position))) return void 0;
671
+ position += 2;
672
+ break;
673
+ case "F":
674
+ case "M":
675
+ read = word(MONTHS, true);
676
+ if (read !== void 0) fields.month = read + 1;
677
+ break;
678
+ case "m":
679
+ case "n":
680
+ read = digits(1, 2);
681
+ if (read !== void 0) fields.month = read;
682
+ break;
683
+ case "Y":
684
+ read = digits(1, 4);
685
+ if (read !== void 0) fields.year = read;
686
+ break;
687
+ case "y":
688
+ read = digits(2, 2);
689
+ if (read !== void 0) fields.year = read + (read < 70 ? 2e3 : 1900);
690
+ break;
691
+ case "a":
692
+ case "A": {
693
+ const match = /^(am|pm)/i.exec(value.slice(position));
694
+ if (!match) return void 0;
695
+ position += 2;
696
+ meridiem = match[1].toLowerCase();
697
+ break;
698
+ }
699
+ case "g":
700
+ case "h":
701
+ read = digits(1, 2);
702
+ if (read !== void 0 && (read < 1 || read > 12)) return void 0;
703
+ if (read !== void 0) fields.hour = read;
704
+ twelveHour = true;
705
+ break;
706
+ case "G":
707
+ case "H":
708
+ read = digits(1, 2);
709
+ if (read !== void 0) fields.hour = read;
710
+ break;
711
+ case "i":
712
+ read = digits(2, 2);
713
+ if (read !== void 0) fields.minute = read;
714
+ break;
715
+ case "s":
716
+ read = digits(2, 2);
717
+ if (read !== void 0) fields.second = read;
718
+ break;
719
+ case "v":
720
+ read = digits(3, 3);
721
+ if (read !== void 0) fields.micro = read * 1e3;
722
+ break;
723
+ case "u": {
724
+ const match = /^\d{1,6}/.exec(value.slice(position));
725
+ read = match ? Number(match[0].padEnd(6, "0")) : void 0;
726
+ if (match) position += match[0].length;
727
+ if (read !== void 0) fields.micro = read;
728
+ break;
729
+ }
730
+ case "O":
731
+ read = fields.offset = offset("none");
732
+ break;
733
+ case "P":
734
+ read = fields.offset = offset("required");
735
+ break;
736
+ case "p":
737
+ read = fields.offset = offset("z");
738
+ break;
739
+ case "U": {
740
+ const match = /^-?\d+/.exec(value.slice(position));
741
+ read = match ? Number(match[0]) : void 0;
742
+ if (match) position += match[0].length;
743
+ fields.unix = read;
744
+ break;
745
+ }
746
+ default:
747
+ if (value[position] !== char) return void 0;
748
+ position++;
749
+ continue;
750
+ }
751
+ if (read === void 0) return void 0;
752
+ }
753
+ if (position !== value.length) return void 0;
754
+ if (twelveHour && meridiem !== void 0) fields.hour = fields.hour % 12 + (meridiem === "pm" ? 12 : 0);
755
+ return fields;
756
+ }
757
+ function wallClock(fields) {
758
+ if (fields.unix !== void 0) return new Date(fields.unix * 1e3);
759
+ const date = /* @__PURE__ */ new Date(0);
760
+ date.setUTCFullYear(fields.year, fields.month - 1, fields.day);
761
+ date.setUTCHours(fields.hour, fields.minute, fields.second, Math.floor(fields.micro / 1e3));
762
+ return date;
763
+ }
764
+ function format(format2, date, micro, offset) {
765
+ let out = "";
766
+ for (let i = 0; i < format2.length; i++) {
767
+ const char = format2[i];
768
+ const day = date.getUTCDate();
769
+ const hour = date.getUTCHours();
770
+ switch (char) {
771
+ case "!":
772
+ case "|":
773
+ break;
774
+ case "\\":
775
+ i++;
776
+ out += format2[i] ?? "";
777
+ break;
778
+ case "d":
779
+ out += pad(day, 2);
780
+ break;
781
+ case "j":
782
+ out += String(day);
783
+ break;
784
+ case "D":
785
+ out += DAYS[date.getUTCDay()].slice(0, 3);
786
+ break;
787
+ case "l":
788
+ out += DAYS[date.getUTCDay()];
789
+ break;
790
+ case "S":
791
+ out += suffix(day);
792
+ break;
793
+ case "F":
794
+ out += MONTHS[date.getUTCMonth()];
795
+ break;
796
+ case "M":
797
+ out += MONTHS[date.getUTCMonth()].slice(0, 3);
798
+ break;
799
+ case "m":
800
+ out += pad(date.getUTCMonth() + 1, 2);
801
+ break;
802
+ case "n":
803
+ out += String(date.getUTCMonth() + 1);
804
+ break;
805
+ case "Y":
806
+ out += pad(date.getUTCFullYear(), 4);
807
+ break;
808
+ case "y":
809
+ out += pad(date.getUTCFullYear() % 100, 2);
810
+ break;
811
+ case "a":
812
+ out += hour < 12 ? "am" : "pm";
813
+ break;
814
+ case "A":
815
+ out += hour < 12 ? "AM" : "PM";
816
+ break;
817
+ case "g":
818
+ out += String(hour % 12 || 12);
819
+ break;
820
+ case "h":
821
+ out += pad(hour % 12 || 12, 2);
822
+ break;
823
+ case "G":
824
+ out += String(hour);
825
+ break;
826
+ case "H":
827
+ out += pad(hour, 2);
828
+ break;
829
+ case "i":
830
+ out += pad(date.getUTCMinutes(), 2);
831
+ break;
832
+ case "s":
833
+ out += pad(date.getUTCSeconds(), 2);
834
+ break;
835
+ case "v":
836
+ out += pad(Math.floor(micro / 1e3), 3);
837
+ break;
838
+ case "u":
839
+ out += pad(micro, 6);
840
+ break;
841
+ case "O":
842
+ out += formatOffset(offset, false);
843
+ break;
844
+ case "P":
845
+ out += formatOffset(offset, true);
846
+ break;
847
+ case "p":
848
+ out += offset === 0 ? "Z" : formatOffset(offset, true);
849
+ break;
850
+ case "U":
851
+ out += String(Math.floor(date.getTime() / 1e3));
852
+ break;
853
+ default:
854
+ out += char;
855
+ }
856
+ }
857
+ return out;
858
+ }
859
+ function assertSupportedFormat(rule, dateFormat) {
860
+ for (let i = 0; i < dateFormat.length; i++) {
861
+ if (dateFormat[i] === "\\") i++;
862
+ else if (UNSUPPORTED.has(dateFormat[i])) {
863
+ throw new InvalidRuleParameterError(rule, dateFormat, `the format character "${dateFormat[i]}" is not supported`);
864
+ }
865
+ }
866
+ }
867
+ function matchesFormat(dateFormat, value) {
868
+ const fields = parse(dateFormat, value);
869
+ if (!fields) return false;
870
+ const offset = fields.offset ?? 0;
871
+ return format(dateFormat, wallClock(fields), fields.micro, offset) === value;
872
+ }
873
+ function timestampWithFormat(dateFormat, value) {
874
+ const fields = parse(dateFormat, value);
875
+ if (!fields) return Number.NaN;
876
+ if (fields.unix !== void 0) return fields.unix * 1e3;
877
+ const utc = wallClock(fields).getTime();
878
+ if (fields.offset !== void 0) return utc - fields.offset * 6e4;
879
+ const local = /* @__PURE__ */ new Date(0);
880
+ local.setFullYear(fields.year, fields.month - 1, fields.day);
881
+ local.setHours(fields.hour, fields.minute, fields.second, Math.floor(fields.micro / 1e3));
882
+ return local.getTime();
883
+ }
884
+
885
+ // rules/date.ts
886
+ var DATE_SHAPES = [
887
+ /^\d{4}-\d{1,2}-\d{1,2}(?:[T\s].*)?$/,
888
+ /^\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}(?:\s.*)?$/,
889
+ /^(?=.*\d).*\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/i
890
+ ];
891
+ var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
892
+ function parseDate(value) {
893
+ const trimmed = value.trim();
894
+ if (!DATE_SHAPES.some((shape) => shape.test(trimmed))) return Number.NaN;
895
+ const dateOnly = DATE_ONLY_RE.exec(trimmed);
896
+ if (dateOnly) {
897
+ const [, y, m, d] = dateOnly.map(Number);
898
+ const local = new Date(y, m - 1, d);
899
+ return local.getMonth() === m - 1 && local.getDate() === d ? local.getTime() : Number.NaN;
900
+ }
901
+ return Date.parse(trimmed);
902
+ }
903
+ function startOfDay(offsetDays) {
904
+ const date = /* @__PURE__ */ new Date();
905
+ date.setHours(0, 0, 0, 0);
906
+ date.setDate(date.getDate() + offsetDays);
907
+ return date.getTime();
908
+ }
909
+ var RELATIVE_RE = /^([+-]\d+)\s*(second|minute|hour|day|week|month|year)s?$/i;
910
+ function relativeDate(param) {
911
+ switch (param.trim().toLowerCase()) {
912
+ case "now":
913
+ return Date.now();
914
+ case "today":
915
+ return startOfDay(0);
916
+ case "tomorrow":
917
+ return startOfDay(1);
918
+ case "yesterday":
919
+ return startOfDay(-1);
920
+ }
921
+ const match = RELATIVE_RE.exec(param.trim());
922
+ if (!match) return void 0;
923
+ const amount = Number(match[1]);
924
+ const date = /* @__PURE__ */ new Date();
925
+ switch (match[2].toLowerCase()) {
926
+ case "second":
927
+ date.setSeconds(date.getSeconds() + amount);
928
+ break;
929
+ case "minute":
930
+ date.setMinutes(date.getMinutes() + amount);
931
+ break;
932
+ case "hour":
933
+ date.setHours(date.getHours() + amount);
934
+ break;
935
+ case "day":
936
+ date.setDate(date.getDate() + amount);
937
+ break;
938
+ case "week":
939
+ date.setDate(date.getDate() + amount * 7);
940
+ break;
941
+ case "month":
942
+ date.setMonth(date.getMonth() + amount);
943
+ break;
944
+ case "year":
945
+ date.setFullYear(date.getFullYear() + amount);
946
+ break;
947
+ }
948
+ return date.getTime();
949
+ }
950
+ var FIELD_REFERENCE_RE = /^[A-Za-z_][\w-]*(?:\.[\w-]+)*$/;
951
+ var isDateInput = (value) => typeof value === "string" || typeof value === "number" && Number.isFinite(value);
952
+ function resolveDateParam(ctx, rule, param, format2) {
953
+ const fieldValue = Arr5.get(ctx.data, param);
954
+ if (isDateInput(fieldValue)) {
955
+ const parsed = readDate(fieldValue, ctx.getRule?.(param, "date_format")?.[0] ?? format2);
956
+ if (!Number.isNaN(parsed)) return parsed;
957
+ }
958
+ const relative = relativeDate(param);
959
+ if (relative !== void 0) return relative;
960
+ const literal = readDate(param, format2);
961
+ if (!Number.isNaN(literal)) return literal;
962
+ if (FIELD_REFERENCE_RE.test(param)) return null;
963
+ throw new InvalidRuleParameterError(rule, param, 'expected a date, today/tomorrow/yesterday/now, "+N unit", or a field name');
964
+ }
965
+ function readDate(value, format2) {
966
+ if (format2 !== void 0) {
967
+ const formatted = timestampWithFormat(format2, String(value));
968
+ if (!Number.isNaN(formatted)) return formatted;
969
+ }
970
+ return typeof value === "number" ? value * 1e3 : parseDate(value);
971
+ }
972
+ var COMPARE = {
973
+ before: (value, target) => value < target,
974
+ before_or_equal: (value, target) => value <= target,
975
+ after: (value, target) => value > target,
976
+ after_or_equal: (value, target) => value >= target,
977
+ date_equals: (value, target) => value === target
978
+ };
979
+ function compareDates(ctx, rule) {
980
+ const format2 = ctx.getRule?.(ctx.attribute, "date_format")?.[0];
981
+ const param = ctx.parameters[0] ?? "";
982
+ const target = resolveDateParam(ctx, rule, param, format2);
983
+ if (!isDateInput(ctx.value)) return false;
984
+ if (target === null && format2 !== void 0) {
985
+ return Arr5.get(ctx.data, param) == null;
986
+ }
987
+ const value = readDate(ctx.value, format2);
988
+ if (Number.isNaN(value)) return false;
989
+ if (target === null) {
990
+ return COMPARE[rule](Math.trunc(value / 1e3) === 0 ? 0 : 1, 0);
991
+ }
992
+ return COMPARE[rule](value, target);
993
+ }
994
+ function dateReplacement(ctx) {
995
+ const param = ctx.parameters[0] ?? "";
996
+ const format2 = ctx.getRule?.(ctx.attribute, "date_format")?.[0];
997
+ const isDate = relativeDate(param) !== void 0 || !Number.isNaN(readDate(param, format2));
998
+ return { date: isDate && !Arr5.has(ctx.data, param) ? param : displayName(ctx, param) };
999
+ }
1000
+ var comparison = (rule) => ({
1001
+ validate: (ctx) => compareDates(ctx, rule),
1002
+ messageKey: () => rule,
1003
+ replacements: dateReplacement
1004
+ });
1005
+ var dateRules = {
1006
+ date: {
1007
+ validate: (ctx) => typeof ctx.value === "string" && !Number.isNaN(parseDate(ctx.value)),
1008
+ messageKey: () => "date"
1009
+ },
1010
+ before: comparison("before"),
1011
+ before_or_equal: comparison("before_or_equal"),
1012
+ after: comparison("after"),
1013
+ after_or_equal: comparison("after_or_equal"),
1014
+ date_equals: comparison("date_equals"),
1015
+ /**
1016
+ * Laravel's validateDateFormat(): the value (text, or a number) reads with one of the formats (PHP's characters,
1017
+ * `date_format:Y-m-d,d/m/Y`) and formats back to the same text, so `2026-02-30` and `2026-2-28` fail `Y-m-d`.
1018
+ */
1019
+ date_format: {
1020
+ validate: (ctx) => {
1021
+ for (const format2 of ctx.parameters) assertSupportedFormat("date_format", format2);
1022
+ if (typeof ctx.value !== "string" && !(typeof ctx.value === "number" && Number.isFinite(ctx.value))) return false;
1023
+ const text = String(ctx.value);
1024
+ return ctx.parameters.some((format2) => matchesFormat(format2, text));
1025
+ },
1026
+ messageKey: () => "date_format",
1027
+ replacements: (ctx) => ({ format: ctx.parameters[0] ?? "" })
1028
+ }
1029
+ };
1030
+
1031
+ // rules/exclude.ts
1032
+ import { Arr as Arr6 } from "@orieljs/support";
1033
+ var excludeRules = {
1034
+ exclude: { excludes: true, validate: () => false, messageKey: () => "exclude" },
1035
+ /** Excluded when the other field holds one of the values; nothing happens while the other field is missing. */
1036
+ exclude_if: {
1037
+ excludes: true,
1038
+ validate: (ctx) => !Arr6.has(ctx.data, ctx.parameters[0] ?? "") || !otherFieldMatches(ctx),
1039
+ messageKey: () => "exclude_if"
1040
+ },
1041
+ exclude_unless: { excludes: true, validate: (ctx) => otherFieldMatches(ctx), messageKey: () => "exclude_unless" },
1042
+ /** Excluded while the other field's key is there. */
1043
+ exclude_with: { excludes: true, validate: (ctx) => !Arr6.has(ctx.data, ctx.parameters[0] ?? ""), messageKey: () => "exclude_with" },
1044
+ /** Excluded while any field named is missing or empty. */
1045
+ exclude_without: {
1046
+ excludes: true,
1047
+ validate: (ctx) => !ctx.parameters.some((field) => isEmptyValue(Arr6.get(ctx.data, field))),
1048
+ messageKey: () => "exclude_without"
1049
+ }
1050
+ };
1051
+
241
1052
  // rules/file.ts
242
1053
  import { FileType } from "@orieljs/support";
243
1054
 
@@ -318,63 +1129,13 @@ function avif(view) {
318
1129
  const iprp = meta && findBox(view, meta.start + 4, meta.end, "iprp");
319
1130
  const ipco = iprp && findBox(view, iprp.start, iprp.end, "ipco");
320
1131
  if (!ipco) return void 0;
321
- let largest;
322
- for (const box of boxes(view, ipco.start, ipco.end)) {
323
- if (box.type !== "ispe") continue;
324
- const size = { width: view.getUint32(box.start + 4), height: view.getUint32(box.start + 8) };
325
- if (!largest || size.width * size.height > largest.width * largest.height) largest = size;
326
- }
327
- return largest;
328
- }
329
-
330
- // parse.ts
331
- import { Arr } from "@orieljs/support";
332
- function splitParameters(name, raw) {
333
- return name === "regex" || name === "not_regex" ? [raw] : raw.split(",");
334
- }
335
- function splitOnce(value, separator) {
336
- const index = value.indexOf(separator);
337
- return index === -1 ? [value, void 0] : [value.slice(0, index), value.slice(index + 1)];
338
- }
339
- function parseRuleSpec(spec) {
340
- const items = typeof spec === "string" ? spec.split("|") : spec;
341
- return items.filter((item) => item !== "").map((item) => {
342
- if (typeof item !== "string") return { kind: "object", rule: item };
343
- const [name, raw] = splitOnce(item, ":");
344
- return { kind: "named", name, parameters: raw === void 0 ? [] : splitParameters(name, raw) };
345
- });
346
- }
347
- function isEmptyValue(value) {
348
- if (value === null || value === void 0) return true;
349
- if (typeof value === "string") return value === "";
350
- if (Array.isArray(value)) return value.length === 0;
351
- return false;
352
- }
353
- function isUploadedFileLike(value) {
354
- return typeof value === "object" && value !== null && typeof value.clientName === "string" && typeof value.arrayBuffer === "function";
355
- }
356
- var UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
357
- function expandWildcardKeys(pattern, data) {
358
- const segments = pattern.split(".");
359
- let paths = [""];
360
- for (const segment of segments) {
361
- const next = [];
362
- for (const base of paths) {
363
- const prefix = base === "" ? "" : `${base}.`;
364
- if (segment !== "*") {
365
- next.push(`${prefix}${segment}`);
366
- continue;
367
- }
368
- const container = Arr.get(data, base === "" ? void 0 : base);
369
- if (Array.isArray(container)) {
370
- container.forEach((_, index) => next.push(`${prefix}${index}`));
371
- } else if (container !== null && typeof container === "object") {
372
- Object.keys(container).filter((key) => !UNSAFE_KEYS.has(key)).forEach((key) => next.push(`${prefix}${key}`));
373
- }
374
- }
375
- paths = next;
1132
+ let largest;
1133
+ for (const box of boxes(view, ipco.start, ipco.end)) {
1134
+ if (box.type !== "ispe") continue;
1135
+ const size = { width: view.getUint32(box.start + 4), height: view.getUint32(box.start + 8) };
1136
+ if (!largest || size.width * size.height > largest.width * largest.height) largest = size;
376
1137
  }
377
- return paths;
1138
+ return largest;
378
1139
  }
379
1140
 
380
1141
  // rules/file.ts
@@ -414,7 +1175,7 @@ function allowedExtensions(ctx) {
414
1175
  if (allowed.has("jpg") || allowed.has("jpeg")) allowed.add("jpg").add("jpeg");
415
1176
  return allowed;
416
1177
  }
417
- var listed = (ctx) => ({ values: ctx.parameters.join(", ") });
1178
+ var listed2 = (ctx) => ({ values: ctx.parameters.join(", ") });
418
1179
  var DIMENSION_KEYS = ["width", "height", "min_width", "min_height", "max_width", "max_height", "ratio"];
419
1180
  function namedParameter(parameter) {
420
1181
  const at = parameter.indexOf("=");
@@ -471,7 +1232,7 @@ var fileRules = {
471
1232
  return meetsMimes((await inspect(ctx.value)).type, clientExtension(ctx.value), allowed);
472
1233
  },
473
1234
  messageKey: () => "mimes",
474
- replacements: listed
1235
+ replacements: listed2
475
1236
  },
476
1237
  /**
477
1238
  * The detected MIME type is listed, or its `type/*` wildcard is. `image/*` includes SVG (`image/svg+xml`), as in
@@ -486,7 +1247,7 @@ var fileRules = {
486
1247
  return allowed.includes(mime) || allowed.includes(`${mime.split("/")[0]}/*`);
487
1248
  },
488
1249
  messageKey: () => "mimetypes",
489
- replacements: listed
1250
+ replacements: listed2
490
1251
  },
491
1252
  /**
492
1253
  * The pixel size read from the image's header meets every constraint: width, height, min_width, max_width,
@@ -510,18 +1271,81 @@ var fileRules = {
510
1271
  return !blocksPhp(ctx.value, allowed) && allowed.includes(clientExtension(ctx.value));
511
1272
  },
512
1273
  messageKey: () => "extensions",
513
- replacements: listed
1274
+ replacements: listed2
514
1275
  }
515
1276
  };
516
1277
 
517
1278
  // rules/format.ts
518
- import { Arr as Arr2 } from "@orieljs/support";
519
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
520
- var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1279
+ import { Arr as Arr7, Str as Str2 } from "@orieljs/support";
1280
+ var ASCII_RE = /^[\x09\x10\x13\x0A\x0D\x20-\x7E]*$/;
1281
+ var HEX_COLOR_RE = /^#(?:(?:[0-9a-f]{3}){1,2}|(?:[0-9a-f]{4}){1,2})$/i;
521
1282
  var ALPHA_RE = /^[A-Za-z]+$/;
522
1283
  var ALPHA_NUM_RE = /^[A-Za-z0-9]+$/;
523
1284
  var ALPHA_DASH_RE = /^[A-Za-z0-9_-]+$/;
524
- var ACCEPTED_VALUES = ["yes", "on", 1, "1", true, "true"];
1285
+ function isEmail(value) {
1286
+ const at = value.indexOf("@");
1287
+ if (at < 1 || value.includes("@", at + 1) || /\s/.test(value)) return false;
1288
+ return value.slice(at + 2, -1).includes(".");
1289
+ }
1290
+ function isUuid(value, version) {
1291
+ if (!Str2.isUuid(value)) return false;
1292
+ if (version === void 0) return true;
1293
+ const hex = value.replaceAll("-", "").toLowerCase();
1294
+ if (version === "nil" || version === "0") return /^0+$/.test(hex);
1295
+ if (version === "max") return /^f+$/.test(hex);
1296
+ return !/^0+$|^f+$/.test(hex) && hex[12] === version && "89ab".includes(hex[16]);
1297
+ }
1298
+ var URL_PROTOCOLS = "aaa|aaas|about|acap|acct|acd|acr|adiumxtra|adt|afp|afs|aim|amss|android|appdata|apt|ark|attachment|aw|barion|beshare|bitcoin|bitcoincash|blob|bolo|browserext|calculator|callto|cap|cast|casts|chrome|chrome-extension|cid|coap|coap\\+tcp|coap\\+ws|coaps|coaps\\+tcp|coaps\\+ws|com-eventbrite-attendee|content|conti|crid|cvs|dab|data|dav|diaspora|dict|did|dis|dlna-playcontainer|dlna-playsingle|dns|dntp|dpp|drm|drop|dtn|dvb|ed2k|elsi|example|facetime|fax|feed|feedready|file|filesystem|finger|first-run-pen-experience|fish|fm|ftp|fuchsia-pkg|geo|gg|git|gizmoproject|go|gopher|graph|gtalk|h323|ham|hcap|hcp|http|https|hxxp|hxxps|hydrazone|iax|icap|icon|im|imap|info|iotdisco|ipn|ipp|ipps|irc|irc6|ircs|iris|iris\\.beep|iris\\.lwz|iris\\.xpc|iris\\.xpcs|isostore|itms|jabber|jar|jms|keyparc|lastfm|ldap|ldaps|leaptofrogans|lorawan|lvlt|magnet|mailserver|mailto|maps|market|message|mid|mms|modem|mongodb|moz|ms-access|ms-browser-extension|ms-calculator|ms-drive-to|ms-enrollment|ms-excel|ms-eyecontrolspeech|ms-gamebarservices|ms-gamingoverlay|ms-getoffice|ms-help|ms-infopath|ms-inputapp|ms-lockscreencomponent-config|ms-media-stream-id|ms-mixedrealitycapture|ms-mobileplans|ms-officeapp|ms-people|ms-project|ms-powerpoint|ms-publisher|ms-restoretabcompanion|ms-screenclip|ms-screensketch|ms-search|ms-search-repair|ms-secondary-screen-controller|ms-secondary-screen-setup|ms-settings|ms-settings-airplanemode|ms-settings-bluetooth|ms-settings-camera|ms-settings-cellular|ms-settings-cloudstorage|ms-settings-connectabledevices|ms-settings-displays-topology|ms-settings-emailandaccounts|ms-settings-language|ms-settings-location|ms-settings-lock|ms-settings-nfctransactions|ms-settings-notifications|ms-settings-power|ms-settings-privacy|ms-settings-proximity|ms-settings-screenrotation|ms-settings-wifi|ms-settings-workplace|ms-spd|ms-sttoverlay|ms-transit-to|ms-useractivityset|ms-virtualtouchpad|ms-visio|ms-walk-to|ms-whiteboard|ms-whiteboard-cmd|ms-word|msnim|msrp|msrps|mss|mtqp|mumble|mupdate|mvn|news|nfs|ni|nih|nntp|notes|ocf|oid|onenote|onenote-cmd|opaquelocktoken|openpgp4fpr|pack|palm|paparazzi|payto|pkcs11|platform|pop|pres|prospero|proxy|pwid|psyc|pttp|qb|query|redis|rediss|reload|res|resource|rmi|rsync|rtmfp|rtmp|rtsp|rtsps|rtspu|s3|secondlife|service|session|sftp|sgn|shttp|sieve|simpleledger|sip|sips|skype|smb|sms|smtp|snews|snmp|soap\\.beep|soap\\.beeps|soldat|spiffe|spotify|ssh|steam|stun|stuns|submit|svn|tag|teamspeak|tel|teliaeid|telnet|tftp|tg|things|thismessage|tip|tn3270|tool|ts3server|turn|turns|tv|udp|unreal|urn|ut2004|v-event|vemmi|ventrilo|videotex|vnc|view-source|wais|webcal|wpid|ws|wss|wtai|wyciwyg|xcon|xcon-userid|xfire|xmlrpc\\.beep|xmlrpc\\.beeps|xmpp|xri|ymsgr|z39\\.50|z39\\.50r|z39\\.50s";
1299
+ var H16 = "[0-9a-f]{1,4}";
1300
+ var OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
1301
+ var LS32 = `(?:${H16}:${H16}|(?:${OCTET}\\.){3}${OCTET})`;
1302
+ var IPV6 = [
1303
+ `(?:${H16}:){6}${LS32}`,
1304
+ `::(?:${H16}:){5}${LS32}`,
1305
+ `(?:${H16})?::(?:${H16}:){4}${LS32}`,
1306
+ `(?:(?:${H16}:){0,1}${H16})?::(?:${H16}:){3}${LS32}`,
1307
+ `(?:(?:${H16}:){0,2}${H16})?::(?:${H16}:){2}${LS32}`,
1308
+ `(?:(?:${H16}:){0,3}${H16})?::${H16}:${LS32}`,
1309
+ `(?:(?:${H16}:){0,4}${H16})?::${LS32}`,
1310
+ `(?:(?:${H16}:){0,5}${H16})?::${H16}`,
1311
+ `(?:(?:${H16}:){0,6}${H16})?::`
1312
+ ].join("|");
1313
+ var URL_USER = "(?:[_.\\p{L}\\p{N}-]|%[0-9A-Fa-f]{2})+";
1314
+ var URL_DOMAIN = "(?=(?<domain>[\\p{L}\\p{N}\\p{S}\\-_.]+))\\k<domain>(?<=[\\p{L}\\p{N}\\p{S}\\-_.](?:[\\p{L}\\p{N}]|xn--[\\p{L}\\p{N}-]+)\\.?)";
1315
+ function urlPattern(protocols) {
1316
+ return new RegExp(
1317
+ `^(?:${protocols})://(?:(?:${URL_USER}:)?${URL_USER}@)?(?:${URL_DOMAIN}|\\p{Nd}{1,3}\\.\\p{Nd}{1,3}\\.\\p{Nd}{1,3}\\.\\p{Nd}{1,3}|\\[(?:${IPV6})\\])(?::[0-9]+)?(?:/(?:[\\p{L}\\p{N}\\-._~!$&'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*(?:\\?(?:[\\p{L}\\p{N}\\-._~!$&'\\[\\]()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?(?:#(?:[\\p{L}\\p{N}\\-._~!$&'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*)?\\n?$`,
1318
+ "iu"
1319
+ );
1320
+ }
1321
+ var URL_RE = urlPattern(URL_PROTOCOLS);
1322
+ var urlPatterns = /* @__PURE__ */ new Map();
1323
+ function isUrl(value, protocols) {
1324
+ if (typeof value !== "string") return false;
1325
+ if (protocols.length === 0) return URL_RE.test(value);
1326
+ const list = protocols.join("|");
1327
+ let pattern = urlPatterns.get(list);
1328
+ if (pattern === void 0) {
1329
+ try {
1330
+ pattern = urlPattern(list);
1331
+ } catch (error) {
1332
+ throw new InvalidRuleParameterError(
1333
+ "url",
1334
+ protocols.join(","),
1335
+ error instanceof Error ? error.message : String(error)
1336
+ );
1337
+ }
1338
+ urlPatterns.set(list, pattern);
1339
+ }
1340
+ return pattern.test(value);
1341
+ }
1342
+ function edges(value, parameters, end) {
1343
+ if (typeof value !== "string" && typeof value !== "number") return false;
1344
+ const text = String(value);
1345
+ return parameters.some((needle) => needle !== "" && (end ? text.endsWith(needle) : text.startsWith(needle)));
1346
+ }
1347
+ var isText = (value) => typeof value === "string" || typeof value === "number";
1348
+ var listed3 = (ctx) => ({ values: ctx.parameters.join(", ") });
525
1349
  function compileRegex(raw) {
526
1350
  const match = /^\/(.*)\/([a-z]*)$/.exec(raw);
527
1351
  try {
@@ -530,106 +1354,38 @@ function compileRegex(raw) {
530
1354
  throw new InvalidRuleParameterError("regex", raw, error instanceof Error ? error.message : String(error));
531
1355
  }
532
1356
  }
533
- var DATE_SHAPES = [
534
- /^\d{4}-\d{1,2}-\d{1,2}(?:[T\s].*)?$/,
535
- /^\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}(?:\s.*)?$/,
536
- /^(?=.*\d).*\b(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)[a-z]*\b/i
537
- ];
538
- var DATE_ONLY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
539
- function parseDate(value) {
540
- const trimmed = value.trim();
541
- if (!DATE_SHAPES.some((shape) => shape.test(trimmed))) return Number.NaN;
542
- const dateOnly = DATE_ONLY_RE.exec(trimmed);
543
- if (dateOnly) {
544
- const [, y, m, d] = dateOnly.map(Number);
545
- const local = new Date(y, m - 1, d);
546
- return local.getMonth() === m - 1 && local.getDate() === d ? local.getTime() : Number.NaN;
547
- }
548
- return Date.parse(trimmed);
549
- }
550
- function startOfDay(offsetDays) {
551
- const date = /* @__PURE__ */ new Date();
552
- date.setHours(0, 0, 0, 0);
553
- date.setDate(date.getDate() + offsetDays);
554
- return date.getTime();
555
- }
556
- var RELATIVE_RE = /^([+-]\d+)\s*(second|minute|hour|day|week|month|year)s?$/i;
557
- function relativeDate(param) {
558
- switch (param.trim().toLowerCase()) {
559
- case "now":
560
- return Date.now();
561
- case "today":
562
- return startOfDay(0);
563
- case "tomorrow":
564
- return startOfDay(1);
565
- case "yesterday":
566
- return startOfDay(-1);
567
- }
568
- const match = RELATIVE_RE.exec(param.trim());
569
- if (!match) return void 0;
570
- const amount = Number(match[1]);
571
- const date = /* @__PURE__ */ new Date();
572
- switch (match[2].toLowerCase()) {
573
- case "second":
574
- date.setSeconds(date.getSeconds() + amount);
575
- break;
576
- case "minute":
577
- date.setMinutes(date.getMinutes() + amount);
578
- break;
579
- case "hour":
580
- date.setHours(date.getHours() + amount);
581
- break;
582
- case "day":
583
- date.setDate(date.getDate() + amount);
584
- break;
585
- case "week":
586
- date.setDate(date.getDate() + amount * 7);
587
- break;
588
- case "month":
589
- date.setMonth(date.getMonth() + amount);
590
- break;
591
- case "year":
592
- date.setFullYear(date.getFullYear() + amount);
593
- break;
594
- }
595
- return date.getTime();
596
- }
597
- var FIELD_REFERENCE_RE = /^[A-Za-z_][\w-]*(?:\.[\w-]+)*$/;
598
- function resolveDateParam(rule, param, data) {
599
- const fieldValue = Arr2.get(data, param);
600
- if (typeof fieldValue === "string") {
601
- const parsed = parseDate(fieldValue);
602
- if (!Number.isNaN(parsed)) return parsed;
603
- }
604
- const relative = relativeDate(param);
605
- if (relative !== void 0) return relative;
606
- const literal = parseDate(param);
607
- if (!Number.isNaN(literal)) return literal;
608
- if (FIELD_REFERENCE_RE.test(param)) return Number.NaN;
609
- throw new InvalidRuleParameterError(rule, param, 'expected a date, today/tomorrow/yesterday/now, "+N unit", or a field name');
610
- }
611
- function compareDate(ctx, rule) {
612
- const target = resolveDateParam(rule, ctx.parameters[0] ?? "", ctx.data);
613
- if (typeof ctx.value !== "string") return false;
614
- const value = parseDate(ctx.value);
615
- if (Number.isNaN(value) || Number.isNaN(target)) return false;
616
- return rule === "before" ? value < target : value > target;
617
- }
618
1357
  var formatRules = {
619
- email: { validate: (ctx) => typeof ctx.value === "string" && EMAIL_RE.test(ctx.value), messageKey: () => "email" },
620
- url: {
1358
+ email: { validate: (ctx) => typeof ctx.value === "string" && isEmail(ctx.value), messageKey: () => "email" },
1359
+ url: { validate: (ctx) => isUrl(ctx.value, ctx.parameters), messageKey: () => "url" },
1360
+ uuid: { validate: (ctx) => isUuid(ctx.value, ctx.parameters[0]), messageKey: () => "uuid" },
1361
+ ulid: { validate: (ctx) => Str2.isUlid(ctx.value), messageKey: () => "ulid" },
1362
+ ascii: { validate: (ctx) => isText(ctx.value) && ASCII_RE.test(String(ctx.value)), messageKey: () => "ascii" },
1363
+ hex_color: { validate: (ctx) => typeof ctx.value === "string" && HEX_COLOR_RE.test(ctx.value), messageKey: () => "hex_color" },
1364
+ /** Text that parses as JSON (a JSON body's own numbers and objects are not "a JSON string"). */
1365
+ json: {
621
1366
  validate: (ctx) => {
622
1367
  if (typeof ctx.value !== "string") return false;
623
1368
  try {
624
- new URL(ctx.value);
1369
+ JSON.parse(ctx.value);
625
1370
  return true;
626
1371
  } catch {
627
1372
  return false;
628
1373
  }
629
1374
  },
630
- messageKey: () => "url"
1375
+ messageKey: () => "json"
1376
+ },
1377
+ starts_with: { validate: (ctx) => edges(ctx.value, ctx.parameters, false), messageKey: () => "starts_with", replacements: listed3 },
1378
+ ends_with: { validate: (ctx) => edges(ctx.value, ctx.parameters, true), messageKey: () => "ends_with", replacements: listed3 },
1379
+ doesnt_start_with: {
1380
+ validate: (ctx) => isText(ctx.value) && !edges(ctx.value, ctx.parameters, false),
1381
+ messageKey: () => "doesnt_start_with",
1382
+ replacements: listed3
1383
+ },
1384
+ doesnt_end_with: {
1385
+ validate: (ctx) => isText(ctx.value) && !edges(ctx.value, ctx.parameters, true),
1386
+ messageKey: () => "doesnt_end_with",
1387
+ replacements: listed3
631
1388
  },
632
- uuid: { validate: (ctx) => typeof ctx.value === "string" && UUID_RE.test(ctx.value), messageKey: () => "uuid" },
633
1389
  alpha: { validate: (ctx) => typeof ctx.value === "string" && ALPHA_RE.test(ctx.value), messageKey: () => "alpha" },
634
1390
  alpha_num: {
635
1391
  validate: (ctx) => typeof ctx.value === "string" && ALPHA_NUM_RE.test(ctx.value),
@@ -651,46 +1407,120 @@ var formatRules = {
651
1407
  validate: (ctx) => typeof ctx.value === "string" && compileRegex(ctx.parameters[0] ?? "").test(ctx.value),
652
1408
  messageKey: () => "regex"
653
1409
  },
1410
+ /** Laravel's validateNotRegex(): text or a number that the pattern does not match. */
1411
+ not_regex: {
1412
+ validate: (ctx) => isText(ctx.value) && !compileRegex(ctx.parameters[0] ?? "").test(String(ctx.value)),
1413
+ messageKey: () => "not_regex"
1414
+ },
1415
+ /** `confirmed` matches `<field>_confirmation`; `confirmed:repeat_password` names the other field (Laravel 12). */
654
1416
  confirmed: {
655
- validate: (ctx) => ctx.value === Arr2.get(ctx.data, `${ctx.attribute}_confirmation`),
1417
+ validate: (ctx) => ctx.value === Arr7.get(ctx.data, ctx.parameters[0] || `${ctx.attribute}_confirmation`),
656
1418
  messageKey: () => "confirmed"
657
1419
  },
658
1420
  same: {
659
- validate: (ctx) => ctx.value === Arr2.get(ctx.data, ctx.parameters[0] ?? ""),
1421
+ validate: (ctx) => ctx.value === Arr7.get(ctx.data, ctx.parameters[0] ?? ""),
660
1422
  messageKey: () => "same",
661
- replacements: (ctx) => ({ other: defaultAttributeName(ctx.parameters[0] ?? "") })
1423
+ replacements: (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") })
662
1424
  },
663
1425
  different: {
664
- validate: (ctx) => ctx.value !== Arr2.get(ctx.data, ctx.parameters[0] ?? ""),
1426
+ validate: (ctx) => ctx.value !== Arr7.get(ctx.data, ctx.parameters[0] ?? ""),
665
1427
  messageKey: () => "different",
666
- replacements: (ctx) => ({ other: defaultAttributeName(ctx.parameters[0] ?? "") })
1428
+ replacements: (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") })
667
1429
  },
668
- accepted: { implicit: true, validate: (ctx) => ACCEPTED_VALUES.includes(ctx.value), messageKey: () => "accepted" },
669
1430
  digits: {
670
1431
  validate: (ctx) => typeof ctx.value !== "object" && new RegExp(`^\\d{${ctx.parameters[0]}}$`).test(String(ctx.value)),
671
1432
  messageKey: () => "digits",
672
1433
  replacements: (ctx) => ({ digits: ctx.parameters[0] ?? "" })
1434
+ }
1435
+ };
1436
+
1437
+ // rules/network.ts
1438
+ import { lookup } from "dns/promises";
1439
+ import { isIP, isIPv4, isIPv6 } from "net";
1440
+ var MAC_RE = /^(?:[0-9a-f]{2}([-:])(?:[0-9a-f]{2}\1){4}[0-9a-f]{2}|[0-9a-f]{4}\.[0-9a-f]{4}\.[0-9a-f]{4})$/i;
1441
+ var resolver = { lookup };
1442
+ async function hasAddress(value) {
1443
+ if (typeof value !== "string") return false;
1444
+ let host;
1445
+ try {
1446
+ host = new URL(value).hostname;
1447
+ } catch {
1448
+ return false;
1449
+ }
1450
+ if (host === "") return false;
1451
+ try {
1452
+ return (await resolver.lookup(host.replace(/^\[|\]$/g, ""), { all: true })).length > 0;
1453
+ } catch {
1454
+ return false;
1455
+ }
1456
+ }
1457
+ var networkRules = {
1458
+ ip: { validate: (ctx) => typeof ctx.value === "string" && isIP(ctx.value) !== 0, messageKey: () => "ip" },
1459
+ ipv4: { validate: (ctx) => typeof ctx.value === "string" && isIPv4(ctx.value), messageKey: () => "ipv4" },
1460
+ ipv6: { validate: (ctx) => typeof ctx.value === "string" && isIPv6(ctx.value), messageKey: () => "ipv6" },
1461
+ mac_address: { validate: (ctx) => typeof ctx.value === "string" && MAC_RE.test(ctx.value), messageKey: () => "mac_address" },
1462
+ active_url: { validate: (ctx) => hasAddress(ctx.value), messageKey: () => "active_url" }
1463
+ };
1464
+
1465
+ // rules/numeric.ts
1466
+ var onlyDigits = (value) => (typeof value === "string" || typeof value === "number") && /^\d+$/.test(String(value));
1467
+ var MAX_EXPONENT = 1e3;
1468
+ function decimal(text) {
1469
+ const [, sign = "", whole = "", fraction = "", power = "0"] = /^\s*([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?\s*$/.exec(text) ?? [];
1470
+ if (Math.abs(Number(power)) > MAX_EXPONENT) return void 0;
1471
+ return { digits: BigInt(`${sign}${whole}${fraction}` || "0"), exponent: Number(power) - fraction.length };
1472
+ }
1473
+ function isMultipleOf(value, divisor) {
1474
+ if (!isNumeric(value) || !isNumeric(divisor)) return false;
1475
+ const a = decimal(String(value));
1476
+ const b = decimal(divisor);
1477
+ if (a === void 0 || b === void 0) return false;
1478
+ const exponent = Math.min(a.exponent, b.exponent);
1479
+ const numerator = a.digits * 10n ** BigInt(a.exponent - exponent);
1480
+ const denominator = b.digits * 10n ** BigInt(b.exponent - exponent);
1481
+ if (denominator === 0n) return false;
1482
+ return numerator % denominator === 0n;
1483
+ }
1484
+ function hasDecimals(value, min, max) {
1485
+ if (!isNumeric(value)) return false;
1486
+ const match = /^[+-]?\d*(?:\.(\d*))?$/.exec(String(value).trim());
1487
+ if (match === null) return false;
1488
+ const places = match[1] ?? "";
1489
+ return max === void 0 ? places.length === Number(min) : places.length >= Number(min) && places.length <= Number(max);
1490
+ }
1491
+ var length = (value) => String(value).length;
1492
+ var numericRules = {
1493
+ decimal: {
1494
+ validate: (ctx) => hasDecimals(ctx.value, ctx.parameters[0] ?? "", ctx.parameters[1]),
1495
+ messageKey: () => "decimal",
1496
+ replacements: (ctx) => ({ decimal: ctx.parameters[1] === void 0 ? ctx.parameters[0] ?? "" : `${ctx.parameters[0]}-${ctx.parameters[1]}` })
673
1497
  },
674
- date: {
675
- validate: (ctx) => typeof ctx.value === "string" && !Number.isNaN(parseDate(ctx.value)),
676
- messageKey: () => "date"
1498
+ digits_between: {
1499
+ validate: (ctx) => onlyDigits(ctx.value) && length(ctx.value) >= Number(ctx.parameters[0]) && length(ctx.value) <= Number(ctx.parameters[1]),
1500
+ messageKey: () => "digits_between",
1501
+ replacements: (ctx) => ({ min: ctx.parameters[0] ?? "", max: ctx.parameters[1] ?? "" })
1502
+ },
1503
+ max_digits: {
1504
+ validate: (ctx) => onlyDigits(ctx.value) && length(ctx.value) <= Number(ctx.parameters[0]),
1505
+ messageKey: () => "max_digits",
1506
+ replacements: (ctx) => ({ max: ctx.parameters[0] ?? "" })
677
1507
  },
678
- before: {
679
- validate: (ctx) => compareDate(ctx, "before"),
680
- messageKey: () => "before",
681
- replacements: (ctx) => ({ date: ctx.parameters[0] ?? "" })
1508
+ min_digits: {
1509
+ validate: (ctx) => onlyDigits(ctx.value) && length(ctx.value) >= Number(ctx.parameters[0]),
1510
+ messageKey: () => "min_digits",
1511
+ replacements: (ctx) => ({ min: ctx.parameters[0] ?? "" })
682
1512
  },
683
- after: {
684
- validate: (ctx) => compareDate(ctx, "after"),
685
- messageKey: () => "after",
686
- replacements: (ctx) => ({ date: ctx.parameters[0] ?? "" })
1513
+ multiple_of: {
1514
+ validate: (ctx) => isMultipleOf(ctx.value, ctx.parameters[0] ?? ""),
1515
+ messageKey: () => "multiple_of",
1516
+ replacements: (ctx) => ({ value: ctx.parameters[0] ?? "" })
687
1517
  }
688
1518
  };
689
1519
 
690
1520
  // rules/presence.ts
691
- import { Arr as Arr3 } from "@orieljs/support";
1521
+ import { Arr as Arr8 } from "@orieljs/support";
692
1522
  function isFieldPresent(field, data) {
693
- return Arr3.has(data, field) && !isEmptyValue(Arr3.get(data, field));
1523
+ return Arr8.has(data, field) && !isEmptyValue(Arr8.get(data, field));
694
1524
  }
695
1525
  function anyPresent(fields, data) {
696
1526
  return fields.some((field) => isFieldPresent(field, data));
@@ -698,6 +1528,13 @@ function anyPresent(fields, data) {
698
1528
  function allPresent(fields, data) {
699
1529
  return fields.every((field) => isFieldPresent(field, data));
700
1530
  }
1531
+ var hasAnyKey = (fields, data) => fields.some((field) => Arr8.has(data, field));
1532
+ var hasEveryKey = (fields, data) => fields.length > 0 && fields.every((field) => Arr8.has(data, field));
1533
+ var otherAndFirstValue = (ctx) => ({
1534
+ other: displayName(ctx, ctx.parameters[0] ?? ""),
1535
+ value: displayableValue(ctx.parameters[1])
1536
+ });
1537
+ var otherName = (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") });
701
1538
  var presenceRules = {
702
1539
  required: {
703
1540
  implicit: true,
@@ -706,89 +1543,160 @@ var presenceRules = {
706
1543
  },
707
1544
  required_if: {
708
1545
  implicit: true,
709
- validate: (ctx) => {
710
- const [other, ...values] = ctx.parameters;
711
- const applies = values.includes(String(Arr3.get(ctx.data, other)));
712
- return !applies || !isEmptyValue(ctx.value);
713
- },
1546
+ // Laravel: nothing is required while the other field is missing.
1547
+ validate: (ctx) => !Arr8.has(ctx.data, ctx.parameters[0] ?? "") || !otherFieldMatches(ctx) || !isEmptyValue(ctx.value),
714
1548
  messageKey: () => "required_if",
715
- replacements: (ctx) => ({ other: ctx.parameters[0] ?? "", value: ctx.parameters.slice(1).join(", ") })
1549
+ replacements: otherAndValue
716
1550
  },
717
1551
  required_unless: {
718
1552
  implicit: true,
719
- validate: (ctx) => {
720
- const [other, ...values] = ctx.parameters;
721
- const applies = !values.includes(String(Arr3.get(ctx.data, other)));
722
- return !applies || !isEmptyValue(ctx.value);
723
- },
1553
+ validate: (ctx) => otherFieldMatches(ctx) || !isEmptyValue(ctx.value),
724
1554
  messageKey: () => "required_unless",
725
- replacements: (ctx) => ({ other: ctx.parameters[0] ?? "", values: ctx.parameters.slice(1).join(", ") })
1555
+ replacements: otherAndValues
726
1556
  },
727
1557
  required_with: {
728
1558
  implicit: true,
729
1559
  validate: (ctx) => !anyPresent(ctx.parameters, ctx.data) || !isEmptyValue(ctx.value),
730
1560
  messageKey: () => "required_with",
731
- replacements: (ctx) => ({ values: ctx.parameters.join(" / ") })
1561
+ replacements: fieldList
732
1562
  },
733
1563
  required_with_all: {
734
1564
  implicit: true,
735
1565
  validate: (ctx) => !allPresent(ctx.parameters, ctx.data) || !isEmptyValue(ctx.value),
736
1566
  messageKey: () => "required_with_all",
737
- replacements: (ctx) => ({ values: ctx.parameters.join(" / ") })
1567
+ replacements: fieldList
738
1568
  },
739
1569
  required_without: {
740
1570
  implicit: true,
741
1571
  validate: (ctx) => ctx.parameters.every((f) => isFieldPresent(f, ctx.data)) || !isEmptyValue(ctx.value),
742
1572
  messageKey: () => "required_without",
743
- replacements: (ctx) => ({ values: ctx.parameters.join(" / ") })
1573
+ replacements: fieldList
744
1574
  },
745
1575
  required_without_all: {
746
1576
  implicit: true,
747
1577
  validate: (ctx) => anyPresent(ctx.parameters, ctx.data) || !isEmptyValue(ctx.value),
748
1578
  messageKey: () => "required_without_all",
749
- replacements: (ctx) => ({ values: ctx.parameters.join(" / ") })
1579
+ replacements: fieldList
1580
+ },
1581
+ required_if_accepted: {
1582
+ implicit: true,
1583
+ validate: (ctx) => !otherAccepted(ctx.data, ctx.parameters[0]) || !isEmptyValue(ctx.value),
1584
+ messageKey: () => "required_if_accepted",
1585
+ replacements: otherName
1586
+ },
1587
+ required_if_declined: {
1588
+ implicit: true,
1589
+ validate: (ctx) => !otherDeclined(ctx.data, ctx.parameters[0]) || !isEmptyValue(ctx.value),
1590
+ messageKey: () => "required_if_declined",
1591
+ replacements: otherName
1592
+ },
1593
+ /** The key is there, whatever its value (null and '' included). */
1594
+ present: { implicit: true, validate: (ctx) => Arr8.has(ctx.data, ctx.attribute), messageKey: () => "present" },
1595
+ present_if: {
1596
+ implicit: true,
1597
+ validate: (ctx) => !otherFieldMatches(ctx) || Arr8.has(ctx.data, ctx.attribute),
1598
+ messageKey: () => "present_if",
1599
+ replacements: otherAndValue
1600
+ },
1601
+ present_unless: {
1602
+ implicit: true,
1603
+ validate: (ctx) => otherFieldMatches(ctx) || Arr8.has(ctx.data, ctx.attribute),
1604
+ messageKey: () => "present_unless",
1605
+ replacements: otherAndFirstValue
1606
+ },
1607
+ present_with: {
1608
+ implicit: true,
1609
+ validate: (ctx) => !hasAnyKey(ctx.parameters, ctx.data) || Arr8.has(ctx.data, ctx.attribute),
1610
+ messageKey: () => "present_with",
1611
+ replacements: fieldList
1612
+ },
1613
+ present_with_all: {
1614
+ implicit: true,
1615
+ validate: (ctx) => !hasEveryKey(ctx.parameters, ctx.data) || Arr8.has(ctx.data, ctx.attribute),
1616
+ messageKey: () => "present_with_all",
1617
+ replacements: fieldList
1618
+ },
1619
+ /** A key that is there must not be empty; a missing key passes. */
1620
+ filled: { implicit: true, validate: (ctx) => !Arr8.has(ctx.data, ctx.attribute) || !isEmptyValue(ctx.value), messageKey: () => "filled" },
1621
+ /** The key is not there at all (null counts as there). */
1622
+ missing: { implicit: true, validate: (ctx) => !Arr8.has(ctx.data, ctx.attribute), messageKey: () => "missing" },
1623
+ missing_if: {
1624
+ implicit: true,
1625
+ validate: (ctx) => !otherFieldMatches(ctx) || !Arr8.has(ctx.data, ctx.attribute),
1626
+ messageKey: () => "missing_if",
1627
+ replacements: otherAndValue
1628
+ },
1629
+ missing_unless: {
1630
+ implicit: true,
1631
+ validate: (ctx) => otherFieldMatches(ctx) || !Arr8.has(ctx.data, ctx.attribute),
1632
+ messageKey: () => "missing_unless",
1633
+ replacements: otherAndFirstValue
1634
+ },
1635
+ missing_with: {
1636
+ implicit: true,
1637
+ validate: (ctx) => !hasAnyKey(ctx.parameters, ctx.data) || !Arr8.has(ctx.data, ctx.attribute),
1638
+ messageKey: () => "missing_with",
1639
+ replacements: fieldList
1640
+ },
1641
+ missing_with_all: {
1642
+ implicit: true,
1643
+ validate: (ctx) => !hasEveryKey(ctx.parameters, ctx.data) || !Arr8.has(ctx.data, ctx.attribute),
1644
+ messageKey: () => "missing_with_all",
1645
+ replacements: fieldList
750
1646
  }
751
1647
  };
752
1648
 
753
- // rules/type.ts
754
- var BOOLEAN_VALUES = [true, false, 0, 1, "0", "1"];
755
- var NUMERIC_STRING_RE = /^\s*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\s*$/;
756
- function isNumeric(value) {
757
- if (typeof value === "number") return Number.isFinite(value);
758
- return typeof value === "string" && NUMERIC_STRING_RE.test(value);
759
- }
760
- var typeRules = {
761
- string: { validate: (ctx) => typeof ctx.value === "string", messageKey: () => "string" },
762
- integer: {
763
- validate: (ctx) => typeof ctx.value !== "object" && /^-?\d+$/.test(String(ctx.value).trim()),
764
- messageKey: () => "integer"
1649
+ // rules/prohibited.ts
1650
+ import { Arr as Arr9 } from "@orieljs/support";
1651
+ var prohibitedRules = {
1652
+ prohibited: { validate: (ctx) => isEmptyValue(ctx.value), messageKey: () => "prohibited" },
1653
+ prohibited_if: {
1654
+ validate: (ctx) => !otherFieldMatches(ctx) || isEmptyValue(ctx.value),
1655
+ messageKey: () => "prohibited_if",
1656
+ replacements: otherAndValue
765
1657
  },
766
- numeric: {
767
- validate: (ctx) => isNumeric(ctx.value),
768
- messageKey: () => "numeric"
1658
+ prohibited_unless: {
1659
+ validate: (ctx) => otherFieldMatches(ctx) || isEmptyValue(ctx.value),
1660
+ messageKey: () => "prohibited_unless",
1661
+ replacements: otherAndValues
1662
+ },
1663
+ prohibited_if_accepted: {
1664
+ validate: (ctx) => !otherAccepted(ctx.data, ctx.parameters[0]) || isEmptyValue(ctx.value),
1665
+ messageKey: () => "prohibited_if_accepted",
1666
+ replacements: (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") })
1667
+ },
1668
+ prohibited_if_declined: {
1669
+ validate: (ctx) => !otherDeclined(ctx.data, ctx.parameters[0]) || isEmptyValue(ctx.value),
1670
+ messageKey: () => "prohibited_if_declined",
1671
+ replacements: (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") })
769
1672
  },
770
- boolean: { validate: (ctx) => BOOLEAN_VALUES.includes(ctx.value), messageKey: () => "boolean" },
771
- array: { validate: (ctx) => Array.isArray(ctx.value), messageKey: () => "array" }
1673
+ /** While this field has a value, every field named must be missing or empty. */
1674
+ prohibits: {
1675
+ validate: (ctx) => isEmptyValue(ctx.value) || ctx.parameters.every((field) => isEmptyValue(Arr9.get(ctx.data, field))),
1676
+ messageKey: () => "prohibits",
1677
+ replacements: (ctx) => ({ other: fieldList(ctx).values })
1678
+ }
772
1679
  };
773
1680
 
774
1681
  // rules/size.ts
1682
+ import { Arr as Arr10 } from "@orieljs/support";
775
1683
  function hasAny(ctx, names) {
776
1684
  return names.some((name) => ctx.ruleNames.includes(name));
777
1685
  }
778
- function isPlainObject(value) {
1686
+ var NUMERIC_RULES = ["numeric", "integer", "decimal"];
1687
+ function isPlainObject2(value) {
779
1688
  if (typeof value !== "object" || value === null) return false;
780
1689
  const proto = Object.getPrototypeOf(value);
781
1690
  return proto === Object.prototype || proto === null;
782
1691
  }
783
- function measure(ctx) {
784
- const { value } = ctx;
785
- if (hasAny(ctx, ["numeric", "integer"])) {
1692
+ function measure(ctx, value = ctx.value, numeric = hasAny(ctx, NUMERIC_RULES)) {
1693
+ if (numeric) {
786
1694
  return { type: "numeric", size: isNumeric(value) ? Number(value) : stringLength(value) };
787
1695
  }
788
1696
  if (Array.isArray(value)) return { type: "array", size: value.length };
789
1697
  if (isUploadedFileLike(value)) return { type: "file", size: value.size / 1024 };
790
1698
  if (hasAny(ctx, ["file", "image"])) return { type: "file", size: Number.NaN };
791
- if (isPlainObject(value)) return { type: "array", size: Object.keys(value).length };
1699
+ if (isPlainObject2(value)) return { type: "array", size: Object.keys(value).length };
792
1700
  if (hasAny(ctx, ["array"])) return { type: "array", size: stringLength(value) };
793
1701
  return { type: "string", size: stringLength(value) };
794
1702
  }
@@ -799,6 +1707,41 @@ function stringLength(value) {
799
1707
  function sizeOf(ctx) {
800
1708
  return measure(ctx).size;
801
1709
  }
1710
+ var COMPARE2 = {
1711
+ gt: (a, b) => a > b,
1712
+ gte: (a, b) => a >= b,
1713
+ lt: (a, b) => a < b,
1714
+ lte: (a, b) => a <= b
1715
+ };
1716
+ function kind(value) {
1717
+ if (value === null || value === void 0) return "null";
1718
+ if (Array.isArray(value) || isPlainObject2(value)) return "array";
1719
+ if (typeof value === "number") return Number.isInteger(value) ? "integer" : "double";
1720
+ return typeof value;
1721
+ }
1722
+ function compareSizes(ctx, rule) {
1723
+ const parameter = ctx.parameters[0] ?? "";
1724
+ const other = Arr10.get(ctx.data, parameter);
1725
+ const numeric = hasAny(ctx, NUMERIC_RULES) || isNumeric(ctx.value);
1726
+ if ((other === null || other === void 0) && isNumeric(ctx.value) && isNumeric(parameter)) {
1727
+ return COMPARE2[rule](Number(String(ctx.value).trim()), Number(parameter.trim()));
1728
+ }
1729
+ if (isNumeric(parameter)) return false;
1730
+ if (numeric && isNumeric(ctx.value) && isNumeric(other)) return COMPARE2[rule](Number(String(ctx.value).trim()), Number(String(other).trim()));
1731
+ if (kind(ctx.value) !== kind(other)) return false;
1732
+ return COMPARE2[rule](measure(ctx, ctx.value, numeric).size, measure(ctx, other, numeric).size);
1733
+ }
1734
+ function comparedValue(ctx) {
1735
+ const parameter = ctx.parameters[0] ?? "";
1736
+ const other = Arr10.get(ctx.data, parameter);
1737
+ if (other === null || other === void 0) return { value: displayName(ctx, parameter) };
1738
+ return { value: String(measure(ctx, other, hasAny(ctx, NUMERIC_RULES) || isNumeric(ctx.value)).size) };
1739
+ }
1740
+ var comparison2 = (rule) => ({
1741
+ validate: (ctx) => compareSizes(ctx, rule),
1742
+ messageKey: (ctx) => `${rule}.${measure(ctx, ctx.value, hasAny(ctx, NUMERIC_RULES) || isNumeric(ctx.value)).type}`,
1743
+ replacements: comparedValue
1744
+ });
802
1745
  function sizeType(ctx) {
803
1746
  return measure(ctx).type;
804
1747
  }
@@ -826,16 +1769,100 @@ var sizeRules = {
826
1769
  messageKey: (ctx) => `size.${sizeType(ctx)}`,
827
1770
  replacements: (ctx) => ({ size: ctx.parameters[0] ?? "" })
828
1771
  },
829
- in: { validate: (ctx) => ctx.parameters.includes(String(ctx.value)), messageKey: () => "in" },
830
- not_in: { validate: (ctx) => !ctx.parameters.includes(String(ctx.value)), messageKey: () => "not_in" }
1772
+ gt: comparison2("gt"),
1773
+ gte: comparison2("gte"),
1774
+ lt: comparison2("lt"),
1775
+ lte: comparison2("lte"),
1776
+ in: { validate: (ctx) => isListed(ctx.value, ctx.parameters, ctx.ruleNames), messageKey: () => "in" },
1777
+ not_in: { validate: (ctx) => !isListed(ctx.value, ctx.parameters, ctx.ruleNames), messageKey: () => "not_in" }
1778
+ };
1779
+ function isListed(value, list, ruleNames) {
1780
+ if (Array.isArray(value) && ruleNames.includes("array")) {
1781
+ return value.every((item) => (typeof item !== "object" || item === null) && list.includes(String(item)));
1782
+ }
1783
+ return (typeof value !== "object" || value === null) && list.includes(String(value));
1784
+ }
1785
+
1786
+ // rules/timezone.ts
1787
+ var REGIONS = ["Africa", "America", "Antarctica", "Arctic", "Asia", "Atlantic", "Australia", "Europe", "Indian", "Pacific"];
1788
+ var ZONE_RE = /^[A-Z][A-Za-z_-]*(?:\/[A-Z][A-Za-z0-9_+-]*)+$/;
1789
+ var ICU_NAMES = {
1790
+ "Africa/Asmera": "Africa/Asmara",
1791
+ "America/Buenos_Aires": "America/Argentina/Buenos_Aires",
1792
+ "America/Catamarca": "America/Argentina/Catamarca",
1793
+ "America/Cordoba": "America/Argentina/Cordoba",
1794
+ "America/Jujuy": "America/Argentina/Jujuy",
1795
+ "America/Mendoza": "America/Argentina/Mendoza",
1796
+ "America/Coral_Harbour": "America/Atikokan",
1797
+ "America/Indianapolis": "America/Indiana/Indianapolis",
1798
+ "America/Louisville": "America/Kentucky/Louisville",
1799
+ "America/Godthab": "America/Nuuk",
1800
+ "Asia/Saigon": "Asia/Ho_Chi_Minh",
1801
+ "Asia/Katmandu": "Asia/Kathmandu",
1802
+ "Asia/Calcutta": "Asia/Kolkata",
1803
+ "Asia/Rangoon": "Asia/Yangon",
1804
+ "Atlantic/Faeroe": "Atlantic/Faroe",
1805
+ "Europe/Kiev": "Europe/Kyiv",
1806
+ "Pacific/Truk": "Pacific/Chuuk",
1807
+ "Pacific/Enderbury": "Pacific/Kanton",
1808
+ "Pacific/Ponape": "Pacific/Pohnpei"
1809
+ };
1810
+ function resolve(zone) {
1811
+ try {
1812
+ return new Intl.DateTimeFormat("en-US", { timeZone: zone }).resolvedOptions().timeZone;
1813
+ } catch {
1814
+ return void 0;
1815
+ }
1816
+ }
1817
+ function canonical(zone) {
1818
+ const name = resolve(zone);
1819
+ return name === void 0 ? void 0 : ICU_NAMES[name] ?? name;
1820
+ }
1821
+ function isInOwnCase(value, name) {
1822
+ const lower2 = value.toLowerCase();
1823
+ for (const spelling of [name, ICU_NAMES[name]]) {
1824
+ if (spelling !== void 0 && spelling.toLowerCase() === lower2) return spelling === value;
1825
+ }
1826
+ return true;
1827
+ }
1828
+ function countryZones(country) {
1829
+ const locale = new Intl.Locale(`und-${country.toUpperCase()}`);
1830
+ return locale.getTimeZones?.() ?? locale.timeZones ?? [];
1831
+ }
1832
+ var inRegion = (zone, region) => zone === "UTC" ? region === void 0 || region === "UTC" : ZONE_RE.test(zone) && (region === void 0 ? REGIONS : [region]).includes(zone.split("/")[0]);
1833
+ function isTimezone(value, group = "all", country) {
1834
+ if (typeof value !== "string") return false;
1835
+ const resolved = resolve(value);
1836
+ if (resolved === void 0 || !isInOwnCase(value, resolved)) return false;
1837
+ const name = group.toLowerCase();
1838
+ if (name === "all") return inRegion(value);
1839
+ if (name === "all_with_bc") return /^[A-Z]/.test(value);
1840
+ if (name === "per_country") {
1841
+ const zone = canonical(value);
1842
+ return inRegion(value) && countryZones(country ?? "").some((candidate) => canonical(candidate) === zone);
1843
+ }
1844
+ const region = name === "utc" ? "UTC" : REGIONS.find((candidate) => candidate.toLowerCase() === name);
1845
+ if (region === void 0) throw new InvalidRuleParameterError("timezone", group, "expected all, all_with_bc, per_country, utc or a region such as Europe");
1846
+ return inRegion(value, region);
1847
+ }
1848
+ var timezoneRules = {
1849
+ timezone: { validate: (ctx) => isTimezone(ctx.value, ctx.parameters[0], ctx.parameters[1]), messageKey: () => "timezone" }
831
1850
  };
832
1851
 
833
1852
  // registry.ts
834
1853
  var builtinRules = {
835
1854
  ...presenceRules,
1855
+ ...acceptanceRules,
1856
+ ...prohibitedRules,
1857
+ ...excludeRules,
836
1858
  ...typeRules,
1859
+ ...arrayRules,
837
1860
  ...formatRules,
1861
+ ...dateRules,
1862
+ ...networkRules,
1863
+ ...timezoneRules,
838
1864
  ...sizeRules,
1865
+ ...numericRules,
839
1866
  ...fileRules,
840
1867
  ...databaseRules,
841
1868
  ...authRules
@@ -843,6 +1870,9 @@ var builtinRules = {
843
1870
 
844
1871
  // rules/password.ts
845
1872
  var defaultRule;
1873
+ function hasMixedCase(value) {
1874
+ return value.split(/[\n\r\u2028\u2029]/).some((line) => /\p{Ll}/u.test(line) && /\p{Lu}/u.test(line));
1875
+ }
846
1876
  var PasswordRule = class _PasswordRule {
847
1877
  #min;
848
1878
  #max;
@@ -893,14 +1923,13 @@ var PasswordRule = class _PasswordRule {
893
1923
  this.#symbols = true;
894
1924
  return this;
895
1925
  }
896
- validate(attribute, value, fail) {
897
- const name = defaultAttributeName(attribute);
898
- const say = (key, replacements = {}) => fail(applyReplacements(DEFAULT_MESSAGES[key], { attribute: name, ...replacements }));
1926
+ validate(_attribute, value, fail) {
1927
+ const say = (key, replacements = {}) => fail(applyReplacements(DEFAULT_MESSAGES[key], replacements));
899
1928
  if (typeof value !== "string") return say("string");
900
- const length = [...value].length;
901
- if (length < this.#min) say("min.string", { min: String(this.#min) });
902
- if (this.#max !== void 0 && length > this.#max) say("max.string", { max: String(this.#max) });
903
- if (this.#mixedCase && !/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u.test(value)) say("password.mixed");
1929
+ const length2 = [...value].length;
1930
+ if (length2 < this.#min) say("min.string", { min: String(this.#min) });
1931
+ if (this.#max !== void 0 && length2 > this.#max) say("max.string", { max: String(this.#max) });
1932
+ if (this.#mixedCase && !hasMixedCase(value)) say("password.mixed");
904
1933
  if (this.#letters && !/\p{L}/u.test(value)) say("password.letters");
905
1934
  if (this.#symbols && !/\p{Z}|\p{S}|\p{P}/u.test(value)) say("password.symbols");
906
1935
  if (this.#numbers && !/\p{N}/u.test(value)) say("password.numbers");
@@ -918,11 +1947,15 @@ var PasswordRule = class _PasswordRule {
918
1947
 
919
1948
  // Rule.ts
920
1949
  var BuiltinRuleObject = class {
1950
+ /** The parameters its message shows (`:values` of Rule.doesntContain()), as the string rule's would be. */
1951
+ messageParameters() {
1952
+ return [];
1953
+ }
921
1954
  // Declares all 4 contract parameters so a subclass's inferred type (from `Rule.in(...)` etc.) still accepts a
922
1955
  // call with the full 4-arg ValidationRule.validate signature, as Rule.test.ts makes.
923
1956
  async validate(attribute, value, fail, data, expandedFromWildcard = false) {
924
1957
  if (await this.passes(attribute, value, data, expandedFromWildcard)) return;
925
- fail(applyReplacements(DEFAULT_MESSAGES[this.ruleName], { attribute: defaultAttributeName(attribute) }));
1958
+ fail(applyReplacements(DEFAULT_MESSAGES[this.ruleName], { attribute: defaultAttributeName(attribute), values: this.messageParameters().join(", ") }));
926
1959
  }
927
1960
  };
928
1961
  var InRule = class extends BuiltinRuleObject {
@@ -932,8 +1965,145 @@ var InRule = class extends BuiltinRuleObject {
932
1965
  }
933
1966
  values;
934
1967
  ruleName = "in";
1968
+ passes(_attribute, value, _data, _expanded, ruleNames = []) {
1969
+ return isListed(value, this.values.map(String), ruleNames);
1970
+ }
1971
+ };
1972
+ var NotInRule = class extends BuiltinRuleObject {
1973
+ constructor(values) {
1974
+ super();
1975
+ this.values = values;
1976
+ }
1977
+ values;
1978
+ ruleName = "not_in";
1979
+ passes(_attribute, value, _data, _expanded, ruleNames = []) {
1980
+ return !isListed(value, this.values.map(String), ruleNames);
1981
+ }
1982
+ };
1983
+ var enumValues = (type) => Object.entries(type).filter(([key, value]) => !(typeof value === "string" && typeof type[value] === "number" && String(type[value]) === key)).map(([, value]) => value);
1984
+ var EnumRule = class extends BuiltinRuleObject {
1985
+ constructor(type) {
1986
+ super();
1987
+ this.type = type;
1988
+ }
1989
+ type;
1990
+ ruleName = "enum";
1991
+ #only = [];
1992
+ #except = [];
1993
+ only(values) {
1994
+ this.#only = Array.isArray(values) ? [...values] : [values];
1995
+ return this;
1996
+ }
1997
+ except(values) {
1998
+ this.#except = Array.isArray(values) ? [...values] : [values];
1999
+ return this;
2000
+ }
2001
+ passes(_attribute, value) {
2002
+ const match = enumValues(this.type).find((candidate) => candidate === value || typeof candidate === "number" && typeof value === "string" && String(candidate) === value);
2003
+ if (match === void 0) return false;
2004
+ return this.#only.length > 0 ? this.#only.includes(match) : !this.#except.includes(match);
2005
+ }
2006
+ };
2007
+ var ContainsRule = class extends BuiltinRuleObject {
2008
+ constructor(ruleName, values) {
2009
+ super();
2010
+ this.ruleName = ruleName;
2011
+ this.values = values;
2012
+ }
2013
+ ruleName;
2014
+ values;
935
2015
  passes(_attribute, value) {
936
- return this.values.map(String).includes(String(value));
2016
+ if (!Array.isArray(value)) return false;
2017
+ const found = (needle) => value.some((item) => alike(item, needle));
2018
+ return this.ruleName === "contains" ? this.values.every(found) : !this.values.some(found);
2019
+ }
2020
+ messageParameters() {
2021
+ return this.values.map(String);
2022
+ }
2023
+ };
2024
+ var ExpandingRuleObject = class {
2025
+ validate() {
2026
+ throw new Error(`${this.constructor.name} stands for string rules: use it in a Validator's rules.`);
2027
+ }
2028
+ };
2029
+ var ConditionalRule = class extends ExpandingRuleObject {
2030
+ constructor(rule, condition) {
2031
+ super();
2032
+ this.rule = rule;
2033
+ this.condition = condition;
2034
+ }
2035
+ rule;
2036
+ condition;
2037
+ expandRules() {
2038
+ return (typeof this.condition === "function" ? this.condition() : this.condition) ? this.rule : "";
2039
+ }
2040
+ };
2041
+ var ConditionalRules = class extends ExpandingRuleObject {
2042
+ constructor(condition, rules, defaultRules, negate = false) {
2043
+ super();
2044
+ this.condition = condition;
2045
+ this.rules = rules;
2046
+ this.defaultRules = defaultRules;
2047
+ this.negate = negate;
2048
+ }
2049
+ condition;
2050
+ rules;
2051
+ defaultRules;
2052
+ negate;
2053
+ expandRules(data) {
2054
+ const holds = (typeof this.condition === "function" ? this.condition(data) : this.condition) !== this.negate;
2055
+ const chosen = holds ? this.rules : this.defaultRules;
2056
+ return typeof chosen === "function" ? chosen(data) : chosen;
2057
+ }
2058
+ };
2059
+ var ArrayRule = class extends ExpandingRuleObject {
2060
+ constructor(keys) {
2061
+ super();
2062
+ this.keys = keys;
2063
+ }
2064
+ keys;
2065
+ expandRules() {
2066
+ return this.keys.length === 0 ? "array" : `array:${this.keys.join(",")}`;
2067
+ }
2068
+ };
2069
+ var DimensionsRule = class extends ExpandingRuleObject {
2070
+ #constraints;
2071
+ constructor(constraints) {
2072
+ super();
2073
+ this.#constraints = { ...constraints };
2074
+ }
2075
+ width(pixels) {
2076
+ return this.set("width", pixels);
2077
+ }
2078
+ height(pixels) {
2079
+ return this.set("height", pixels);
2080
+ }
2081
+ minWidth(pixels) {
2082
+ return this.set("min_width", pixels);
2083
+ }
2084
+ minHeight(pixels) {
2085
+ return this.set("min_height", pixels);
2086
+ }
2087
+ maxWidth(pixels) {
2088
+ return this.set("max_width", pixels);
2089
+ }
2090
+ maxHeight(pixels) {
2091
+ return this.set("max_height", pixels);
2092
+ }
2093
+ /** Width over height: `ratio(3 / 2)`. */
2094
+ ratio(ratio) {
2095
+ return this.set("ratio", ratio);
2096
+ }
2097
+ /** `dimensions:max_width=1000,ratio=1.5`, Laravel's __toString(). */
2098
+ toString() {
2099
+ return `dimensions:${Object.entries(this.#constraints).map(([key, value]) => `${key}=${value}`).join(",")}`;
2100
+ }
2101
+ expandRules() {
2102
+ return this.toString();
2103
+ }
2104
+ set(key, value) {
2105
+ this.#constraints[key] = value;
2106
+ return this;
937
2107
  }
938
2108
  };
939
2109
  function isModelInstance(value) {
@@ -986,7 +2156,30 @@ var ExistsRule = class extends BuiltinRuleObject {
986
2156
  }
987
2157
  };
988
2158
  var Rule = {
2159
+ /** One of `values`, compared as text (as `in:a,b`); typed as those values: `Rule.in(['draft', 'publish'])`. */
989
2160
  in: (values) => new InRule(values),
2161
+ /** None of `values`, compared as text (as `not_in:a,b`). */
2162
+ notIn: (values) => new NotInRule(values),
2163
+ /** One of a TypeScript enum's values (or an `as const` object's): `Rule.enum(Status)`, `.only([…])`, `.except([…])`. */
2164
+ enum: (type) => new EnumRule(type),
2165
+ /** An array holding every one of `values`. */
2166
+ contains: (values) => new ContainsRule("contains", values),
2167
+ /** An array holding none of `values`. */
2168
+ doesntContain: (values) => new ContainsRule("doesnt_contain", values),
2169
+ /** `required` when the condition (or the callback) holds. */
2170
+ requiredIf: (condition) => new ConditionalRule("required", condition),
2171
+ /** `exclude` when the condition (or the callback) holds; the key is optional, as `exclude_if` makes it. */
2172
+ excludeIf: (condition) => new ConditionalRule("exclude", condition),
2173
+ /** `prohibited` when the condition (or the callback) holds. */
2174
+ prohibitedIf: (condition) => new ConditionalRule("prohibited", condition),
2175
+ /** `rules` when the condition (or the callback, given the data) holds, else `defaultRules`. */
2176
+ when: (condition, rules, defaultRules = []) => new ConditionalRules(condition, rules, defaultRules),
2177
+ /** `rules` unless the condition (or the callback, given the data) holds, else `defaultRules`. */
2178
+ unless: (condition, rules, defaultRules = []) => new ConditionalRules(condition, rules, defaultRules, true),
2179
+ /** `array`, or with keys `array:name,email` (an object whose keys are among those named). */
2180
+ array: (keys = []) => new ArrayRule(keys),
2181
+ /** The dimensions rule: `Rule.dimensions().maxWidth(1000).ratio(3 / 2)`, or `Rule.dimensions({ min_width: 100 })`. */
2182
+ dimensions: (constraints = {}) => new DimensionsRule(constraints),
990
2183
  unique: (table, column) => new UniqueRule(table, column),
991
2184
  exists: (table, column) => new ExistsRule(table, column),
992
2185
  /** Laravel's Password::defaults(): the application's password rule (at least 8 characters unless PasswordRule.defaults() changed it). */
@@ -994,8 +2187,64 @@ var Rule = {
994
2187
  };
995
2188
 
996
2189
  // Validator.ts
997
- import { Arr as Arr4 } from "@orieljs/support";
2190
+ import { Arr as Arr11 } from "@orieljs/support";
998
2191
  var SKIPPED_MODIFIERS = /* @__PURE__ */ new Set(["bail", "nullable", "sometimes"]);
2192
+ var DEPENDENT_RULES = /* @__PURE__ */ new Set([
2193
+ "after",
2194
+ "after_or_equal",
2195
+ "before",
2196
+ "before_or_equal",
2197
+ "confirmed",
2198
+ "different",
2199
+ "exclude_if",
2200
+ "exclude_unless",
2201
+ "exclude_with",
2202
+ "exclude_without",
2203
+ "gt",
2204
+ "gte",
2205
+ "lt",
2206
+ "lte",
2207
+ "accepted_if",
2208
+ "declined_if",
2209
+ "required_if",
2210
+ "required_if_accepted",
2211
+ "required_if_declined",
2212
+ "required_unless",
2213
+ "required_with",
2214
+ "required_with_all",
2215
+ "required_without",
2216
+ "required_without_all",
2217
+ "present_if",
2218
+ "present_unless",
2219
+ "present_with",
2220
+ "present_with_all",
2221
+ "prohibited",
2222
+ "prohibited_if",
2223
+ "prohibited_if_accepted",
2224
+ "prohibited_if_declined",
2225
+ "prohibited_unless",
2226
+ "prohibits",
2227
+ "missing_if",
2228
+ "missing_unless",
2229
+ "missing_with",
2230
+ "missing_with_all",
2231
+ "same",
2232
+ "unique"
2233
+ ]);
2234
+ function wildcardKeys(pattern, field) {
2235
+ const fieldSegments = field.split(".");
2236
+ return pattern.split(".").flatMap((segment, index) => segment === "*" ? [fieldSegments[index] ?? "*"] : []);
2237
+ }
2238
+ function fillWildcards(parameters, keys) {
2239
+ if (keys.length === 0) return parameters;
2240
+ return parameters.map((parameter) => {
2241
+ let index = 0;
2242
+ return parameter.replace(/\*/g, () => keys[index++] ?? "*");
2243
+ });
2244
+ }
2245
+ function patternMatcher(pattern) {
2246
+ return new RegExp(`^${pattern.split(".").map((segment) => segment === "*" ? "[^.]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\.")}$`);
2247
+ }
999
2248
  function buildNested(flat) {
1000
2249
  const root = {};
1001
2250
  const created = /* @__PURE__ */ new Set([root]);
@@ -1032,6 +2281,49 @@ function cloneContainer(existing) {
1032
2281
  }
1033
2282
  return container;
1034
2283
  }
2284
+ var EXCLUDED = /* @__PURE__ */ Symbol("excluded");
2285
+ function withoutPaths(data, paths) {
2286
+ const root = { ...data };
2287
+ const copies = /* @__PURE__ */ new Set([root]);
2288
+ for (const path of paths) {
2289
+ const parts = path.split(".");
2290
+ const leaf = parts.pop();
2291
+ let node = root;
2292
+ for (const part of parts) {
2293
+ const child = Object.hasOwn(node, part) ? node[part] : void 0;
2294
+ if (!isPlainContainer(child)) {
2295
+ node = void 0;
2296
+ break;
2297
+ }
2298
+ const copy = copies.has(child) ? child : Array.isArray(child) ? [...child] : { ...child };
2299
+ copies.add(copy);
2300
+ node[part] = copy;
2301
+ node = copy;
2302
+ }
2303
+ if (node === void 0 || !Object.hasOwn(node, leaf)) continue;
2304
+ if (Array.isArray(node)) node[Number(leaf)] = EXCLUDED;
2305
+ else delete node[leaf];
2306
+ }
2307
+ return root;
2308
+ }
2309
+ function dropExcluded(node, seen = /* @__PURE__ */ new Set()) {
2310
+ if (typeof node !== "object" || node === null || seen.has(node)) return;
2311
+ seen.add(node);
2312
+ if (Array.isArray(node)) {
2313
+ if (node.includes(EXCLUDED)) {
2314
+ let kept = 0;
2315
+ for (const item of node) if (item !== EXCLUDED) node[kept++] = item;
2316
+ node.length = kept;
2317
+ }
2318
+ } else if (isPlainContainer(node)) {
2319
+ for (const [key, value] of Object.entries(node)) if (value === EXCLUDED) delete node[key];
2320
+ }
2321
+ if (isPlainContainer(node)) for (const value of Object.values(node)) dropExcluded(value, seen);
2322
+ }
2323
+ function segmentEnd(path, start) {
2324
+ const dot = path.indexOf(".", start);
2325
+ return dot === -1 ? path.length : dot;
2326
+ }
1035
2327
  function arrayify(node, created) {
1036
2328
  if (node === null || typeof node !== "object" || !created.has(node)) return node;
1037
2329
  const entries = Object.entries(node).map(([k, v]) => [k, arrayify(v, created)]);
@@ -1057,11 +2349,18 @@ var ValidatorImpl = class {
1057
2349
  bag;
1058
2350
  ranPromise;
1059
2351
  afterCallbacks = [];
2352
+ parsedRules;
2353
+ /** The fields an exclude rule took out (with their children). */
2354
+ excluded = /* @__PURE__ */ new Set();
2355
+ /** The same fields as a tree of segment runs from the root, so a lookup reads the field once. */
2356
+ excludedRuns = { run: "", next: /* @__PURE__ */ new Map(), excluded: false };
2357
+ /** What the rules keep while one pass runs (RuleContext.cache); a new pass starts with a new one. */
2358
+ cache = /* @__PURE__ */ new Map();
1060
2359
  after(callback) {
1061
2360
  this.afterCallbacks.push(callback);
1062
2361
  return this;
1063
2362
  }
1064
- /** Not part of the Validator contract — lets an after() hook add a failure (MessageBag itself is read-only). */
2363
+ /** Lets an after() hook add a failure. */
1065
2364
  fail(field, message) {
1066
2365
  this.bag ??= new MessageBagImpl();
1067
2366
  this.bag.add(field, message);
@@ -1078,34 +2377,138 @@ var ValidatorImpl = class {
1078
2377
  }
1079
2378
  async validated() {
1080
2379
  if (!await this.passes()) throw new ValidationErrorImpl(this.bag.toObject(), this.errorBagName);
2380
+ const data = this.excluded.size === 0 ? this.data : withoutPaths(this.data, this.excluded);
1081
2381
  const flat = {};
1082
2382
  for (const pattern of Object.keys(this.rules)) {
1083
2383
  for (const field of this.fieldsFor(pattern)) {
1084
- if (Arr4.has(this.data, field)) flat[field] = Arr4.get(this.data, field);
2384
+ if (!this.isExcluded(field) && Arr11.has(data, field)) flat[field] = Arr11.get(data, field);
1085
2385
  }
1086
2386
  }
1087
- return buildNested(flat);
2387
+ for (const path of this.excluded) {
2388
+ const parent = path.includes(".") ? path.slice(0, path.lastIndexOf(".")) : void 0;
2389
+ if (parent === void 0 || this.isExcluded(parent) || !Arr11.has(this.data, path)) continue;
2390
+ if (Array.isArray(Arr11.get(this.data, parent))) flat[path] = EXCLUDED;
2391
+ }
2392
+ const nested = buildNested(flat);
2393
+ if (this.excluded.size > 0) dropExcluded(nested);
2394
+ return nested;
2395
+ }
2396
+ /** Takes the field (with its children) out of validation and of the validated data. */
2397
+ exclude(field) {
2398
+ this.excluded.add(field);
2399
+ let node = this.excludedRuns;
2400
+ for (let start = 0; ; ) {
2401
+ const first = field.slice(start, segmentEnd(field, start));
2402
+ let next = node.next.get(first);
2403
+ if (next === void 0) {
2404
+ node.next.set(first, { run: field.slice(start), next: /* @__PURE__ */ new Map(), excluded: true });
2405
+ return;
2406
+ }
2407
+ let same = 0;
2408
+ while (same < next.run.length && start + same < field.length && next.run[same] === field[start + same]) same++;
2409
+ const runEnds = same === next.run.length || next.run[same] === ".";
2410
+ const fieldEnds = start + same === field.length || field[start + same] === ".";
2411
+ if (!runEnds || !fieldEnds) same = next.run.lastIndexOf(".", same - 1);
2412
+ if (same < next.run.length) {
2413
+ const rest = next.run.slice(same + 1);
2414
+ const shared = { run: next.run.slice(0, same), next: /* @__PURE__ */ new Map([[rest.slice(0, segmentEnd(rest, 0)), next]]), excluded: false };
2415
+ next.run = rest;
2416
+ node.next.set(first, shared);
2417
+ next = shared;
2418
+ }
2419
+ start += same;
2420
+ if (start === field.length) {
2421
+ next.excluded = true;
2422
+ return;
2423
+ }
2424
+ node = next;
2425
+ start++;
2426
+ }
2427
+ }
2428
+ /**
2429
+ * Laravel's shouldBeExcluded(): the field, or one of its parents, was taken out by an exclude rule. The field's
2430
+ * segments are followed from the root (so `user` takes out `user.name`, not `username`) and each is read once,
2431
+ * however many fields were excluded and however many dots the field holds.
2432
+ */
2433
+ isExcluded(field) {
2434
+ if (this.excluded.size === 0) return false;
2435
+ let node = this.excludedRuns;
2436
+ for (let start = 0; ; ) {
2437
+ const next = node.next.get(field.slice(start, segmentEnd(field, start)));
2438
+ if (next === void 0) return false;
2439
+ const end = start + next.run.length;
2440
+ if (!field.startsWith(next.run, start) || end < field.length && field[end] !== ".") return false;
2441
+ if (next.excluded) return true;
2442
+ if (end === field.length) return false;
2443
+ node = next;
2444
+ start = end + 1;
2445
+ }
1088
2446
  }
1089
2447
  fieldsFor(pattern) {
1090
2448
  return pattern.includes("*") ? expandWildcardKeys(pattern, this.data) : [pattern];
1091
2449
  }
2450
+ parsed() {
2451
+ this.parsedRules ??= new Map(Object.entries(this.rules).map(([pattern, spec]) => [pattern, parseRuleSpec(spec, this.data)]));
2452
+ return this.parsedRules;
2453
+ }
2454
+ /** The rule key a concrete field falls under: itself, else the first wildcard key that matches it. */
2455
+ patternOf(field) {
2456
+ const parsed = this.parsed();
2457
+ if (parsed.has(field)) return field;
2458
+ for (const pattern of parsed.keys()) if (pattern.includes("*") && patternMatcher(pattern).test(field)) return pattern;
2459
+ return void 0;
2460
+ }
2461
+ /** Laravel's getRule(): the parameters of rule `name` on `field`, or undefined when the field has no such rule. */
2462
+ ruleParameters(field, name) {
2463
+ const pattern = this.patternOf(field);
2464
+ const rule = pattern === void 0 ? void 0 : this.parsed().get(pattern).find((r) => r.kind === "named" && r.name === name);
2465
+ return rule?.kind === "named" ? rule.parameters : void 0;
2466
+ }
2467
+ /** Laravel's getDisplayableAttribute(): a custom attribute for the field or its rule key, else the humanized key. */
2468
+ displayName(field) {
2469
+ const pattern = this.patternOf(field);
2470
+ return this.customAttributes[field] ?? (pattern === void 0 ? void 0 : this.customAttributes[pattern]) ?? defaultAttributeName(field);
2471
+ }
2472
+ context(field, pattern, value, parameters, ruleNames) {
2473
+ return {
2474
+ attribute: field,
2475
+ expandedFromWildcard: pattern.includes("*"),
2476
+ value,
2477
+ parameters,
2478
+ data: this.data,
2479
+ ruleNames,
2480
+ pattern,
2481
+ cache: this.cache,
2482
+ getRule: (other, name) => this.ruleParameters(other, name),
2483
+ displayName: (other) => this.displayName(other)
2484
+ };
2485
+ }
1092
2486
  async run() {
1093
2487
  this.bag = new MessageBagImpl();
1094
- for (const [pattern, spec] of Object.entries(this.rules)) {
1095
- const parsed = parseRuleSpec(spec);
2488
+ this.cache = /* @__PURE__ */ new Map();
2489
+ for (const [pattern, parsed] of this.parsed()) {
1096
2490
  const names = parsed.filter((r) => r.kind === "named").map((r) => r.name);
1097
2491
  const bail = names.includes("bail");
1098
2492
  const nullable = names.includes("nullable");
1099
2493
  const sometimes = names.includes("sometimes");
1100
2494
  for (const field of this.fieldsFor(pattern)) {
1101
- const present = Arr4.has(this.data, field);
2495
+ if (this.isExcluded(field)) continue;
2496
+ const present = Arr11.has(this.data, field);
1102
2497
  if (sometimes && !present) continue;
1103
- const value = Arr4.get(this.data, field);
2498
+ const value = Arr11.get(this.data, field);
1104
2499
  const isNull = value === null || value === void 0;
1105
2500
  const isBlank = typeof value === "string" && value.trim() === "";
1106
2501
  for (const rule of parsed) {
1107
2502
  if (rule.kind === "named" && SKIPPED_MODIFIERS.has(rule.name)) continue;
1108
- const implicit = rule.kind === "named" && (this.registry[rule.name]?.implicit ?? false);
2503
+ const builtin = rule.kind === "named" ? this.registry[rule.name] : void 0;
2504
+ if (builtin?.excludes) {
2505
+ if (!await this.passesExcludeRule(rule, builtin, field, pattern, value, names)) {
2506
+ this.exclude(field);
2507
+ break;
2508
+ }
2509
+ continue;
2510
+ }
2511
+ const implicit = builtin?.implicit ?? false;
1109
2512
  if (!implicit && (!present || isBlank)) continue;
1110
2513
  if (!implicit && nullable && isNull) continue;
1111
2514
  const messages = await this.runRule(rule, field, pattern, value, names);
@@ -1119,21 +2522,26 @@ var ValidatorImpl = class {
1119
2522
  for (const callback of this.afterCallbacks) await callback(this);
1120
2523
  return this.bag.isEmpty();
1121
2524
  }
2525
+ async passesExcludeRule(rule, builtin, field, pattern, value, ruleNames) {
2526
+ return builtin.validate(this.context(field, pattern, value, fillWildcards(rule.parameters, wildcardKeys(pattern, field)), ruleNames));
2527
+ }
1122
2528
  async runRule(rule, field, pattern, value, ruleNames) {
1123
2529
  const expanded = pattern.includes("*");
1124
2530
  if (rule.kind === "object") {
1125
2531
  if (rule.rule instanceof BuiltinRuleObject) {
1126
- if (await rule.rule.passes(field, value, this.data, expanded)) return [];
1127
- const ctx2 = { attribute: field, expandedFromWildcard: expanded, value, parameters: [], data: this.data, ruleNames };
2532
+ if (await rule.rule.passes(field, value, this.data, expanded, ruleNames)) return [];
2533
+ const ctx2 = this.context(field, pattern, value, rule.rule.messageParameters(), ruleNames);
1128
2534
  return [this.failureMessage(rule.rule.ruleName, this.registry[rule.rule.ruleName], ctx2, pattern)];
1129
2535
  }
1130
2536
  const failures = [];
1131
- await rule.rule.validate(field, value, (message) => void failures.push(message), this.data);
2537
+ const attribute = this.displayName(field);
2538
+ await rule.rule.validate(field, value, (message) => void failures.push(applyReplacements(message, { attribute })), this.data);
1132
2539
  return failures;
1133
2540
  }
1134
2541
  const builtin = this.registry[rule.name];
1135
2542
  if (!builtin) throw new UnknownValidationRuleError(rule.name);
1136
- const ctx = { attribute: field, expandedFromWildcard: expanded, value, parameters: rule.parameters, data: this.data, ruleNames };
2543
+ const parameters = DEPENDENT_RULES.has(rule.name) ? fillWildcards(rule.parameters, wildcardKeys(pattern, field)) : rule.parameters;
2544
+ const ctx = this.context(field, pattern, value, parameters, ruleNames);
1137
2545
  if (await builtin.validate(ctx)) return [];
1138
2546
  return [this.failureMessage(rule.name, builtin, ctx, pattern)];
1139
2547
  }