@orieljs/validation 0.1.0-alpha.1 → 0.1.0-alpha.10

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
@@ -13,6 +13,23 @@ var ValidationErrorImpl = class extends Error {
13
13
  status = 422;
14
14
  name = "ValidationError";
15
15
  };
16
+ var ValidationException = class _ValidationException extends ValidationErrorImpl {
17
+ /** Messages per field, each a string or a list. */
18
+ static withMessages(messages) {
19
+ const errors = Object.fromEntries(Object.entries(messages).map(([field, list]) => [field, typeof list === "string" ? [list] : [...list]]));
20
+ return new _ValidationException(errors);
21
+ }
22
+ constructor(errors, errorBag = "default", redirectTo) {
23
+ super(errors, errorBag, redirectTo);
24
+ this.message = summarize(errors);
25
+ }
26
+ };
27
+ function summarize(errors) {
28
+ const messages = Object.values(errors).flat();
29
+ if (messages.length === 0) return "The given data was invalid.";
30
+ const more = messages.length - 1;
31
+ return more === 0 ? messages[0] : `${messages[0]} (and ${more} more ${more === 1 ? "error" : "errors"})`;
32
+ }
16
33
  var AuthorizationErrorImpl = class extends Error {
17
34
  kind = "Authorization";
18
35
  status = 403;
@@ -54,15 +71,17 @@ var FormRequest = class {
54
71
  /**
55
72
  * `const data = await StorePostRequest.validate()` (spec §4.3). Resolves the current request and the
56
73
  * ValidationFactory from the active container, runs authorize() first (throwing AuthorizationErrorImpl if it
57
- * returns false), then validates request.all() against rules()/messages()/attributes() and returns the typed
58
- * 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>`.
59
78
  */
60
79
  static async validate() {
61
80
  const instance = new this();
62
81
  if (!await instance.authorize()) throw new AuthorizationErrorImpl();
63
82
  const request = inject(HttpRequest);
64
83
  const factory = inject(ValidationFactory);
65
- 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());
66
85
  return validator.validated();
67
86
  }
68
87
  };
@@ -70,7 +89,7 @@ var FormRequest = class {
70
89
  // messages.ts
71
90
  import { Str } from "@orieljs/support";
72
91
  function defaultAttributeName(key) {
73
- 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(".");
74
93
  }
75
94
  function applyReplacements(template, replacements) {
76
95
  return Object.entries(replacements).reduce((message, [key, value]) => message.replaceAll(`:${key}`, value), template);
@@ -83,33 +102,97 @@ var DEFAULT_MESSAGES = {
83
102
  required_with_all: "The :attribute field is required when :values are present.",
84
103
  required_without: "The :attribute field is required when :values is not present.",
85
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.",
86
124
  string: "The :attribute field must be a string.",
87
125
  integer: "The :attribute field must be an integer.",
88
126
  numeric: "The :attribute field must be a number.",
89
127
  boolean: "The :attribute field must be true or false.",
90
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.",
91
136
  email: "The :attribute field must be a valid email address.",
92
137
  url: "The :attribute field must be a valid URL.",
93
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.",
94
153
  alpha: "The :attribute field must only contain letters.",
95
154
  alpha_num: "The :attribute field must only contain letters and numbers.",
96
- 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.",
97
156
  regex: "The :attribute field format is invalid.",
157
+ not_regex: "The :attribute field format is invalid.",
158
+ lowercase: "The :attribute field must be lowercase.",
159
+ uppercase: "The :attribute field must be uppercase.",
160
+ current_password: "The password is incorrect.",
161
+ "password.letters": "The :attribute field must contain at least one letter.",
162
+ "password.mixed": "The :attribute field must contain at least one uppercase and one lowercase letter.",
163
+ "password.numbers": "The :attribute field must contain at least one number.",
164
+ "password.symbols": "The :attribute field must contain at least one symbol.",
98
165
  confirmed: "The :attribute field confirmation does not match.",
99
166
  same: "The :attribute field must match :other.",
100
- different: "The :attribute field must be different from :other.",
167
+ different: "The :attribute field and :other must be different.",
101
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.",
102
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.",
103
178
  date: "The :attribute field must be a valid date.",
104
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.",
105
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.",
106
185
  in: "The selected :attribute is invalid.",
107
186
  not_in: "The selected :attribute is invalid.",
187
+ enum: "The selected :attribute is invalid.",
108
188
  unique: "The :attribute has already been taken.",
109
189
  exists: "The selected :attribute is invalid.",
110
190
  file: "The :attribute field must be a file.",
111
191
  image: "The :attribute field must be an image.",
112
192
  mimes: "The :attribute field must be a file of type: :values.",
193
+ mimetypes: "The :attribute field must be a file of type: :values.",
194
+ extensions: "The :attribute field must have one of the following extensions: :values.",
195
+ dimensions: "The :attribute field has invalid image dimensions.",
113
196
  "max.string": "The :attribute field must not be greater than :max characters.",
114
197
  "max.numeric": "The :attribute field must not be greater than :max.",
115
198
  "max.array": "The :attribute field must not have more than :max items.",
@@ -125,13 +208,29 @@ var DEFAULT_MESSAGES = {
125
208
  "size.string": "The :attribute field must be :size characters.",
126
209
  "size.numeric": "The :attribute field must be :size.",
127
210
  "size.array": "The :attribute field must contain :size items.",
128
- "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."
129
228
  };
130
229
 
131
230
  // MessageBag.ts
132
231
  var MessageBagImpl = class {
133
- items = {};
134
- /** 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);
135
234
  add(key, message) {
136
235
  ;
137
236
  (this.items[key] ??= []).push(message);
@@ -163,38 +262,8 @@ var MessageBagImpl = class {
163
262
  }
164
263
  };
165
264
 
166
- // rules/database.ts
167
- import { DatabaseManager } from "@orieljs/contracts";
168
- import { inject as inject2 } from "@orieljs/container";
169
- function guessColumn(attribute, expandedFromWildcard = false) {
170
- if (!expandedFromWildcard) return attribute;
171
- return attribute.split(".").filter((segment) => !/^\d+$/.test(segment)).at(-1) ?? attribute;
172
- }
173
- function isNoExcept(except) {
174
- return except === void 0 || except === null || except === "" || except === "NULL";
175
- }
176
- async function matchingRecordExists(table, column, value, except, idColumn = "id") {
177
- const db = inject2(DatabaseManager);
178
- let query = db.connection().table(table).where(column, value);
179
- if (!isNoExcept(except)) query = query.where(idColumn, "!=", except);
180
- return query.exists();
181
- }
182
- var databaseRules = {
183
- unique: {
184
- validate: async (ctx) => {
185
- const [table, column, except, idColumn] = ctx.parameters;
186
- return !await matchingRecordExists(table, column || guessColumn(ctx.attribute, ctx.expandedFromWildcard), ctx.value, except, idColumn || "id");
187
- },
188
- messageKey: () => "unique"
189
- },
190
- exists: {
191
- validate: async (ctx) => {
192
- const [table, column] = ctx.parameters;
193
- return matchingRecordExists(table, column || guessColumn(ctx.attribute, ctx.expandedFromWildcard), ctx.value);
194
- },
195
- messageKey: () => "exists"
196
- }
197
- };
265
+ // rules/acceptance.ts
266
+ import { Arr as Arr3 } from "@orieljs/support";
198
267
 
199
268
  // parse.ts
200
269
  import { Arr } from "@orieljs/support";
@@ -205,18 +274,26 @@ function splitOnce(value, separator) {
205
274
  const index = value.indexOf(separator);
206
275
  return index === -1 ? [value, void 0] : [value.slice(0, index), value.slice(index + 1)];
207
276
  }
208
- function parseRuleSpec(spec) {
277
+ function isExpandingRule(item) {
278
+ return typeof item === "object" && item !== null && typeof item.expandRules === "function";
279
+ }
280
+ function parseRuleSpec(spec, data = {}) {
209
281
  const items = typeof spec === "string" ? spec.split("|") : spec;
210
- return items.filter((item) => item !== "").map((item) => {
211
- if (typeof item !== "string") return { kind: "object", rule: item };
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 }];
212
285
  const [name, raw] = splitOnce(item, ":");
213
- return { kind: "named", name, parameters: raw === void 0 ? [] : splitParameters(name, raw) };
286
+ return [{ kind: "named", name, parameters: raw === void 0 ? [] : splitParameters(name, raw) }];
214
287
  });
215
288
  }
216
289
  function isEmptyValue(value) {
217
290
  if (value === null || value === void 0) return true;
218
- if (typeof value === "string") return value === "";
291
+ if (typeof value === "string") return value.trim() === "";
219
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
+ }
220
297
  return false;
221
298
  }
222
299
  function isUploadedFileLike(value) {
@@ -246,45 +323,566 @@ function expandWildcardKeys(pattern, data) {
246
323
  return paths;
247
324
  }
248
325
 
249
- // rules/file.ts
250
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set(["jpg", "jpeg", "png", "bmp", "gif", "svg", "webp"]);
251
- var fileRules = {
252
- file: { validate: (ctx) => isUploadedFileLike(ctx.value), messageKey: () => "file" },
253
- image: {
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: {
254
519
  validate: (ctx) => {
255
- if (!isUploadedFileLike(ctx.value)) return false;
256
- if (ctx.value.mimeType.startsWith("image/")) return true;
257
- return IMAGE_EXTENSIONS.has(ctx.value.extension().toLowerCase());
520
+ const items = ctx.value;
521
+ return Array.isArray(items) && ctx.parameters.every((needle) => items.some((item) => alike(item, needle)));
258
522
  },
259
- messageKey: () => "image"
523
+ messageKey: () => "contains"
260
524
  },
261
- mimes: {
525
+ doesnt_contain: {
262
526
  validate: (ctx) => {
263
- if (!isUploadedFileLike(ctx.value)) return false;
264
- const allowed = ctx.parameters.map((p) => p.toLowerCase());
265
- return allowed.includes(ctx.value.extension().toLowerCase());
527
+ const items = ctx.value;
528
+ return Array.isArray(items) && !ctx.parameters.some((needle) => items.some((item) => alike(item, needle)));
266
529
  },
267
- messageKey: () => "mimes",
268
- replacements: (ctx) => ({ values: ctx.parameters.join(", ") })
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] ?? "") })
269
550
  }
270
551
  };
271
552
 
272
- // rules/format.ts
273
- import { Arr as Arr2 } from "@orieljs/support";
274
- var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
275
- 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;
276
- var ALPHA_RE = /^[A-Za-z]+$/;
277
- var ALPHA_NUM_RE = /^[A-Za-z0-9]+$/;
278
- var ALPHA_DASH_RE = /^[A-Za-z0-9_-]+$/;
279
- var ACCEPTED_VALUES = ["yes", "on", 1, "1", true, "true"];
280
- function compileRegex(raw) {
281
- const match = /^\/(.*)\/([a-z]*)$/.exec(raw);
282
- try {
283
- return match ? new RegExp(match[1], match[2]) : new RegExp(raw);
284
- } catch (error) {
285
- throw new InvalidRuleParameterError("regex", raw, error instanceof Error ? error.message : String(error));
553
+ // rules/auth.ts
554
+ import { AuthFactory, Hasher } from "@orieljs/contracts";
555
+ import { inject as inject2 } from "@orieljs/container";
556
+ var authRules = {
557
+ current_password: {
558
+ validate: async (ctx) => {
559
+ const guard = inject2(AuthFactory).guard(ctx.parameters[0]);
560
+ if (await guard.guest()) return false;
561
+ const user = await guard.user();
562
+ return user !== void 0 && typeof ctx.value === "string" && inject2(Hasher).check(ctx.value, user.getAuthPassword());
563
+ },
564
+ messageKey: () => "current_password"
286
565
  }
566
+ };
567
+
568
+ // rules/database.ts
569
+ import { DatabaseManager } from "@orieljs/contracts";
570
+ import { inject as inject3 } from "@orieljs/container";
571
+ function guessColumn(attribute, expandedFromWildcard = false) {
572
+ if (!expandedFromWildcard) return attribute;
573
+ return attribute.split(".").filter((segment) => !/^\d+$/.test(segment)).at(-1) ?? attribute;
574
+ }
575
+ function isNoExcept(except) {
576
+ return except === void 0 || except === null || except === "" || except === "NULL";
577
+ }
578
+ async function matchingRecordExists(table, column, value, except, idColumn = "id") {
579
+ const db = inject3(DatabaseManager);
580
+ let query = db.connection().table(table).where(column, value);
581
+ if (!isNoExcept(except)) query = query.where(idColumn, "!=", except);
582
+ return query.exists();
583
+ }
584
+ var databaseRules = {
585
+ unique: {
586
+ validate: async (ctx) => {
587
+ const [table, column, except, idColumn] = ctx.parameters;
588
+ return !await matchingRecordExists(table, column || guessColumn(ctx.attribute, ctx.expandedFromWildcard), ctx.value, except, idColumn || "id");
589
+ },
590
+ messageKey: () => "unique"
591
+ },
592
+ exists: {
593
+ validate: async (ctx) => {
594
+ const [table, column] = ctx.parameters;
595
+ return matchingRecordExists(table, column || guessColumn(ctx.attribute, ctx.expandedFromWildcard), ctx.value);
596
+ },
597
+ messageKey: () => "exists"
598
+ }
599
+ };
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;
287
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
288
886
  var DATE_SHAPES = [
289
887
  /^\d{4}-\d{1,2}-\d{1,2}(?:[T\s].*)?$/,
290
888
  /^\d{1,2}[/.-]\d{1,2}[/.-]\d{2,4}(?:\s.*)?$/,
@@ -350,41 +948,444 @@ function relativeDate(param) {
350
948
  return date.getTime();
351
949
  }
352
950
  var FIELD_REFERENCE_RE = /^[A-Za-z_][\w-]*(?:\.[\w-]+)*$/;
353
- function resolveDateParam(rule, param, data) {
354
- const fieldValue = Arr2.get(data, param);
355
- if (typeof fieldValue === "string") {
356
- const parsed = parseDate(fieldValue);
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);
357
956
  if (!Number.isNaN(parsed)) return parsed;
358
957
  }
359
958
  const relative = relativeDate(param);
360
959
  if (relative !== void 0) return relative;
361
- const literal = parseDate(param);
960
+ const literal = readDate(param, format2);
362
961
  if (!Number.isNaN(literal)) return literal;
363
- if (FIELD_REFERENCE_RE.test(param)) return Number.NaN;
962
+ if (FIELD_REFERENCE_RE.test(param)) return null;
364
963
  throw new InvalidRuleParameterError(rule, param, 'expected a date, today/tomorrow/yesterday/now, "+N unit", or a field name');
365
964
  }
366
- function compareDate(ctx, rule) {
367
- const target = resolveDateParam(rule, ctx.parameters[0] ?? "", ctx.data);
368
- if (typeof ctx.value !== "string") return false;
369
- const value = parseDate(ctx.value);
370
- if (Number.isNaN(value) || Number.isNaN(target)) return false;
371
- return rule === "before" ? value < target : value > target;
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
+
1052
+ // rules/file.ts
1053
+ import { FileType } from "@orieljs/support";
1054
+
1055
+ // files/dimensions.ts
1056
+ var FRAME_MARKERS = /* @__PURE__ */ new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
1057
+ var MAX_JPEG_STEPS = 1024;
1058
+ var MAX_BOXES = 1024;
1059
+ var fourcc = (view, offset) => String.fromCharCode(view.getUint8(offset), view.getUint8(offset + 1), view.getUint8(offset + 2), view.getUint8(offset + 3));
1060
+ var u24le = (view, offset) => view.getUint8(offset) | view.getUint8(offset + 1) << 8 | view.getUint8(offset + 2) << 16;
1061
+ var READERS = {
1062
+ "image/png": (view) => fourcc(view, 12) === "IHDR" ? { width: view.getUint32(16), height: view.getUint32(20) } : void 0,
1063
+ "image/gif": (view) => ({ width: view.getUint16(6, true), height: view.getUint16(8, true) }),
1064
+ "image/bmp": (view) => view.getUint32(14, true) === 12 ? { width: view.getUint16(18, true), height: view.getUint16(20, true) } : { width: Math.abs(view.getInt32(18, true)), height: Math.abs(view.getInt32(22, true)) },
1065
+ "image/jpeg": jpeg,
1066
+ "image/webp": webp,
1067
+ "image/avif": avif
1068
+ };
1069
+ function imageDimensions(bytes, mime) {
1070
+ const reader = READERS[mime];
1071
+ if (!reader) return void 0;
1072
+ try {
1073
+ const size = reader(new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength));
1074
+ return size && size.width > 0 && size.height > 0 ? size : void 0;
1075
+ } catch (error) {
1076
+ if (error instanceof RangeError) return void 0;
1077
+ throw error;
1078
+ }
1079
+ }
1080
+ function jpeg(view) {
1081
+ let offset = 2;
1082
+ for (let step = 0; step < MAX_JPEG_STEPS && offset + 4 <= view.byteLength; step++) {
1083
+ if (view.getUint8(offset) !== 255) return void 0;
1084
+ const marker = view.getUint8(offset + 1);
1085
+ if (marker === 255) offset += 1;
1086
+ else if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) offset += 2;
1087
+ else if (marker === 217 || marker === 218) return void 0;
1088
+ else if (FRAME_MARKERS.has(marker)) return { height: view.getUint16(offset + 5), width: view.getUint16(offset + 7) };
1089
+ else offset += 2 + view.getUint16(offset + 2);
1090
+ }
1091
+ return void 0;
1092
+ }
1093
+ function webp(view) {
1094
+ const chunk = fourcc(view, 12);
1095
+ if (chunk === "VP8 ") {
1096
+ if (view.getUint8(23) !== 157 || view.getUint8(24) !== 1 || view.getUint8(25) !== 42) return void 0;
1097
+ return { width: view.getUint16(26, true) & 16383, height: view.getUint16(28, true) & 16383 };
1098
+ }
1099
+ if (chunk === "VP8L") {
1100
+ if (view.getUint8(20) !== 47) return void 0;
1101
+ const bits = view.getUint32(21, true);
1102
+ return { width: (bits & 16383) + 1, height: (bits >>> 14 & 16383) + 1 };
1103
+ }
1104
+ if (chunk === "VP8X") return { width: u24le(view, 24) + 1, height: u24le(view, 27) + 1 };
1105
+ return void 0;
1106
+ }
1107
+ function* boxes(view, start, end) {
1108
+ let offset = start;
1109
+ for (let count = 0; count < MAX_BOXES && offset + 8 <= end; count++) {
1110
+ let size = view.getUint32(offset);
1111
+ let header = 8;
1112
+ if (size === 1) {
1113
+ size = Number(view.getBigUint64(offset + 8));
1114
+ header = 16;
1115
+ } else if (size === 0) {
1116
+ size = end - offset;
1117
+ }
1118
+ if (size < header || offset + size > end) return;
1119
+ yield { type: fourcc(view, offset + 4), start: offset + header, end: offset + size };
1120
+ offset += size;
1121
+ }
1122
+ }
1123
+ var findBox = (view, start, end, type) => {
1124
+ for (const box of boxes(view, start, end)) if (box.type === type) return box;
1125
+ return void 0;
1126
+ };
1127
+ function avif(view) {
1128
+ const meta = findBox(view, 0, view.byteLength, "meta");
1129
+ const iprp = meta && findBox(view, meta.start + 4, meta.end, "iprp");
1130
+ const ipco = iprp && findBox(view, iprp.start, iprp.end, "ipco");
1131
+ if (!ipco) return void 0;
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;
1137
+ }
1138
+ return largest;
1139
+ }
1140
+
1141
+ // rules/file.ts
1142
+ var { BINARY_AND_MARKUP_TYPES, detectFileType, EMPTY, RASTER_IMAGES, SVG, TEXT } = FileType;
1143
+ var inspections = /* @__PURE__ */ new WeakMap();
1144
+ function inspect(file) {
1145
+ let inspection = inspections.get(file);
1146
+ if (!inspection) {
1147
+ inspection = file.arrayBuffer().then((buffer) => {
1148
+ const bytes = new Uint8Array(buffer);
1149
+ const type = detectFileType(bytes);
1150
+ return { type, dimensions: RASTER_IMAGES.has(type.mime) ? imageDimensions(bytes, type.mime) : void 0 };
1151
+ });
1152
+ inspections.set(file, inspection);
1153
+ }
1154
+ return inspection;
1155
+ }
1156
+ function clientExtension(file) {
1157
+ const name = file.clientName.slice(Math.max(file.clientName.lastIndexOf("/"), file.clientName.lastIndexOf("\\")) + 1);
1158
+ const dot = name.lastIndexOf(".");
1159
+ return dot === -1 ? "" : name.slice(dot + 1).trim().toLowerCase();
1160
+ }
1161
+ var PHP_EXTENSIONS = /* @__PURE__ */ new Set(["php", "php3", "php4", "php5", "php7", "php8", "phtml", "phar"]);
1162
+ var blocksPhp = (file, allowed) => !allowed.includes("php") && PHP_EXTENSIONS.has(clientExtension(file));
1163
+ var TEXT_FORMATS = /* @__PURE__ */ new Set(["txt", "text", "csv", "tsv", "json", "md"]);
1164
+ var RECOGNISED_EXTENSIONS = new Set(BINARY_AND_MARKUP_TYPES.flatMap(({ extensions }) => extensions));
1165
+ var lower = (values) => values.map((value) => value.trim().toLowerCase());
1166
+ function meetsMimes(type, extension, allowed) {
1167
+ if (type === EMPTY) return false;
1168
+ if (type === TEXT && allowed.has("txt")) return true;
1169
+ if (type !== TEXT && type.extensions.length > 0) return type.extensions.some((name) => allowed.has(name));
1170
+ if (!allowed.has(extension) || RECOGNISED_EXTENSIONS.has(extension)) return false;
1171
+ return type === TEXT || !TEXT_FORMATS.has(extension);
1172
+ }
1173
+ function allowedExtensions(ctx) {
1174
+ const allowed = new Set(lower(ctx.parameters));
1175
+ if (allowed.has("jpg") || allowed.has("jpeg")) allowed.add("jpg").add("jpeg");
1176
+ return allowed;
1177
+ }
1178
+ var listed2 = (ctx) => ({ values: ctx.parameters.join(", ") });
1179
+ var DIMENSION_KEYS = ["width", "height", "min_width", "min_height", "max_width", "max_height", "ratio"];
1180
+ function namedParameter(parameter) {
1181
+ const at = parameter.indexOf("=");
1182
+ return at === -1 ? [parameter, ""] : [parameter.slice(0, at), parameter.slice(at + 1)];
1183
+ }
1184
+ function parseRatio(value) {
1185
+ const match = /^(\d+(?:\.\d+)?|\.\d+)(?:\/(\d+))?$/.exec(value.trim());
1186
+ if (!match) return void 0;
1187
+ const ratio = Number(match[1]) / Number(match[2] ?? 1);
1188
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : void 0;
1189
+ }
1190
+ function dimensionConstraints(parameters) {
1191
+ if (parameters.length === 0) throw new InvalidRuleParameterError("dimensions", "", `expected one or more of ${DIMENSION_KEYS.join(", ")}`);
1192
+ const constraints = {};
1193
+ for (const parameter of parameters) {
1194
+ const [key, value] = namedParameter(parameter);
1195
+ if (!DIMENSION_KEYS.includes(key)) {
1196
+ throw new InvalidRuleParameterError("dimensions", parameter, `expected one of ${DIMENSION_KEYS.join(", ")}`);
1197
+ }
1198
+ const number = key === "ratio" ? parseRatio(value) : /^\d+$/.test(value) ? Number(value) : void 0;
1199
+ if (number === void 0) {
1200
+ throw new InvalidRuleParameterError("dimensions", parameter, key === "ratio" ? "expected a ratio such as 3/2 or 1.5" : "expected a whole number of pixels");
1201
+ }
1202
+ constraints[key] = number;
1203
+ }
1204
+ return constraints;
1205
+ }
1206
+ function meetsDimensions(constraints, { width, height }) {
1207
+ const { width: exactWidth, height: exactHeight, min_width, max_width, min_height, max_height, ratio } = constraints;
1208
+ if (exactWidth !== void 0 && width !== exactWidth) return false;
1209
+ if (exactHeight !== void 0 && height !== exactHeight) return false;
1210
+ if (min_width !== void 0 && width < min_width) return false;
1211
+ if (max_width !== void 0 && width > max_width) return false;
1212
+ if (min_height !== void 0 && height < min_height) return false;
1213
+ if (max_height !== void 0 && height > max_height) return false;
1214
+ return ratio === void 0 || Math.abs(ratio - width / height) <= 1 / (Math.max((width + height) / 2, height) + 1);
1215
+ }
1216
+ var fileRules = {
1217
+ file: { validate: (ctx) => isUploadedFileLike(ctx.value), messageKey: () => "file" },
1218
+ /** A JPEG, PNG, GIF, WebP, AVIF or BMP by its bytes; SVG only as `image:allow_svg` (Laravel 12). */
1219
+ image: {
1220
+ validate: async (ctx) => {
1221
+ if (!isUploadedFileLike(ctx.value) || blocksPhp(ctx.value, [])) return false;
1222
+ const { type } = await inspect(ctx.value);
1223
+ return RASTER_IMAGES.has(type.mime) || type === SVG && ctx.parameters.includes("allow_svg");
1224
+ },
1225
+ messageKey: () => "image"
1226
+ },
1227
+ mimes: {
1228
+ validate: async (ctx) => {
1229
+ if (!isUploadedFileLike(ctx.value)) return false;
1230
+ const allowed = allowedExtensions(ctx);
1231
+ if (blocksPhp(ctx.value, [...allowed])) return false;
1232
+ return meetsMimes((await inspect(ctx.value)).type, clientExtension(ctx.value), allowed);
1233
+ },
1234
+ messageKey: () => "mimes",
1235
+ replacements: listed2
1236
+ },
1237
+ /**
1238
+ * The detected MIME type is listed, or its `type/*` wildcard is. `image/*` includes SVG (`image/svg+xml`), as in
1239
+ * Laravel: the rule for photos is `image`.
1240
+ */
1241
+ mimetypes: {
1242
+ validate: async (ctx) => {
1243
+ if (!isUploadedFileLike(ctx.value)) return false;
1244
+ const allowed = lower(ctx.parameters);
1245
+ if (blocksPhp(ctx.value, allowed)) return false;
1246
+ const { mime } = (await inspect(ctx.value)).type;
1247
+ return allowed.includes(mime) || allowed.includes(`${mime.split("/")[0]}/*`);
1248
+ },
1249
+ messageKey: () => "mimetypes",
1250
+ replacements: listed2
1251
+ },
1252
+ /**
1253
+ * The pixel size read from the image's header meets every constraint: width, height, min_width, max_width,
1254
+ * min_height, max_height, ratio. SVG and images whose header can't be read fail.
1255
+ */
1256
+ dimensions: {
1257
+ validate: async (ctx) => {
1258
+ const constraints = dimensionConstraints(ctx.parameters);
1259
+ if (!isUploadedFileLike(ctx.value)) return false;
1260
+ const { dimensions } = await inspect(ctx.value);
1261
+ return dimensions !== void 0 && meetsDimensions(constraints, dimensions);
1262
+ },
1263
+ messageKey: () => "dimensions",
1264
+ replacements: (ctx) => Object.fromEntries(ctx.parameters.map(namedParameter))
1265
+ },
1266
+ /** The client file name's extension is listed (Laravel 11). It checks the name only: pair it with mimes or image. */
1267
+ extensions: {
1268
+ validate: (ctx) => {
1269
+ if (!isUploadedFileLike(ctx.value)) return false;
1270
+ const allowed = lower(ctx.parameters);
1271
+ return !blocksPhp(ctx.value, allowed) && allowed.includes(clientExtension(ctx.value));
1272
+ },
1273
+ messageKey: () => "extensions",
1274
+ replacements: listed2
1275
+ }
1276
+ };
1277
+
1278
+ // rules/format.ts
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;
1282
+ var ALPHA_RE = /^[A-Za-z]+$/;
1283
+ var ALPHA_NUM_RE = /^[A-Za-z0-9]+$/;
1284
+ var ALPHA_DASH_RE = /^[A-Za-z0-9_-]+$/;
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(", ") });
1349
+ function compileRegex(raw) {
1350
+ const match = /^\/(.*)\/([a-z]*)$/.exec(raw);
1351
+ try {
1352
+ return match ? new RegExp(match[1], match[2]) : new RegExp(raw);
1353
+ } catch (error) {
1354
+ throw new InvalidRuleParameterError("regex", raw, error instanceof Error ? error.message : String(error));
1355
+ }
372
1356
  }
373
1357
  var formatRules = {
374
- email: { validate: (ctx) => typeof ctx.value === "string" && EMAIL_RE.test(ctx.value), messageKey: () => "email" },
375
- 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: {
376
1366
  validate: (ctx) => {
377
1367
  if (typeof ctx.value !== "string") return false;
378
1368
  try {
379
- new URL(ctx.value);
1369
+ JSON.parse(ctx.value);
380
1370
  return true;
381
1371
  } catch {
382
1372
  return false;
383
1373
  }
384
1374
  },
385
- 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
386
1388
  },
387
- uuid: { validate: (ctx) => typeof ctx.value === "string" && UUID_RE.test(ctx.value), messageKey: () => "uuid" },
388
1389
  alpha: { validate: (ctx) => typeof ctx.value === "string" && ALPHA_RE.test(ctx.value), messageKey: () => "alpha" },
389
1390
  alpha_num: {
390
1391
  validate: (ctx) => typeof ctx.value === "string" && ALPHA_NUM_RE.test(ctx.value),
@@ -394,50 +1395,132 @@ var formatRules = {
394
1395
  validate: (ctx) => typeof ctx.value === "string" && ALPHA_DASH_RE.test(ctx.value),
395
1396
  messageKey: () => "alpha_dash"
396
1397
  },
1398
+ lowercase: {
1399
+ validate: (ctx) => (typeof ctx.value === "string" || typeof ctx.value === "number") && String(ctx.value).toLowerCase() === String(ctx.value),
1400
+ messageKey: () => "lowercase"
1401
+ },
1402
+ uppercase: {
1403
+ validate: (ctx) => (typeof ctx.value === "string" || typeof ctx.value === "number") && String(ctx.value).toUpperCase() === String(ctx.value),
1404
+ messageKey: () => "uppercase"
1405
+ },
397
1406
  regex: {
398
1407
  validate: (ctx) => typeof ctx.value === "string" && compileRegex(ctx.parameters[0] ?? "").test(ctx.value),
399
1408
  messageKey: () => "regex"
400
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). */
401
1416
  confirmed: {
402
- 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`),
403
1418
  messageKey: () => "confirmed"
404
1419
  },
405
1420
  same: {
406
- validate: (ctx) => ctx.value === Arr2.get(ctx.data, ctx.parameters[0] ?? ""),
1421
+ validate: (ctx) => ctx.value === Arr7.get(ctx.data, ctx.parameters[0] ?? ""),
407
1422
  messageKey: () => "same",
408
- replacements: (ctx) => ({ other: defaultAttributeName(ctx.parameters[0] ?? "") })
1423
+ replacements: (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") })
409
1424
  },
410
1425
  different: {
411
- validate: (ctx) => ctx.value !== Arr2.get(ctx.data, ctx.parameters[0] ?? ""),
1426
+ validate: (ctx) => ctx.value !== Arr7.get(ctx.data, ctx.parameters[0] ?? ""),
412
1427
  messageKey: () => "different",
413
- replacements: (ctx) => ({ other: defaultAttributeName(ctx.parameters[0] ?? "") })
1428
+ replacements: (ctx) => ({ other: displayName(ctx, ctx.parameters[0] ?? "") })
414
1429
  },
415
- accepted: { implicit: true, validate: (ctx) => ACCEPTED_VALUES.includes(ctx.value), messageKey: () => "accepted" },
416
1430
  digits: {
417
1431
  validate: (ctx) => typeof ctx.value !== "object" && new RegExp(`^\\d{${ctx.parameters[0]}}$`).test(String(ctx.value)),
418
1432
  messageKey: () => "digits",
419
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]}` })
420
1497
  },
421
- date: {
422
- validate: (ctx) => typeof ctx.value === "string" && !Number.isNaN(parseDate(ctx.value)),
423
- 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] ?? "" })
424
1502
  },
425
- before: {
426
- validate: (ctx) => compareDate(ctx, "before"),
427
- messageKey: () => "before",
428
- replacements: (ctx) => ({ date: ctx.parameters[0] ?? "" })
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] ?? "" })
429
1507
  },
430
- after: {
431
- validate: (ctx) => compareDate(ctx, "after"),
432
- messageKey: () => "after",
433
- 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] ?? "" })
1512
+ },
1513
+ multiple_of: {
1514
+ validate: (ctx) => isMultipleOf(ctx.value, ctx.parameters[0] ?? ""),
1515
+ messageKey: () => "multiple_of",
1516
+ replacements: (ctx) => ({ value: ctx.parameters[0] ?? "" })
434
1517
  }
435
1518
  };
436
1519
 
437
1520
  // rules/presence.ts
438
- import { Arr as Arr3 } from "@orieljs/support";
1521
+ import { Arr as Arr8 } from "@orieljs/support";
439
1522
  function isFieldPresent(field, data) {
440
- return Arr3.has(data, field) && !isEmptyValue(Arr3.get(data, field));
1523
+ return Arr8.has(data, field) && !isEmptyValue(Arr8.get(data, field));
441
1524
  }
442
1525
  function anyPresent(fields, data) {
443
1526
  return fields.some((field) => isFieldPresent(field, data));
@@ -445,6 +1528,13 @@ function anyPresent(fields, data) {
445
1528
  function allPresent(fields, data) {
446
1529
  return fields.every((field) => isFieldPresent(field, data));
447
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] ?? "") });
448
1538
  var presenceRules = {
449
1539
  required: {
450
1540
  implicit: true,
@@ -453,89 +1543,160 @@ var presenceRules = {
453
1543
  },
454
1544
  required_if: {
455
1545
  implicit: true,
456
- validate: (ctx) => {
457
- const [other, ...values] = ctx.parameters;
458
- const applies = values.includes(String(Arr3.get(ctx.data, other)));
459
- return !applies || !isEmptyValue(ctx.value);
460
- },
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),
461
1548
  messageKey: () => "required_if",
462
- replacements: (ctx) => ({ other: ctx.parameters[0] ?? "", value: ctx.parameters.slice(1).join(", ") })
1549
+ replacements: otherAndValue
463
1550
  },
464
1551
  required_unless: {
465
1552
  implicit: true,
466
- validate: (ctx) => {
467
- const [other, ...values] = ctx.parameters;
468
- const applies = !values.includes(String(Arr3.get(ctx.data, other)));
469
- return !applies || !isEmptyValue(ctx.value);
470
- },
1553
+ validate: (ctx) => otherFieldMatches(ctx) || !isEmptyValue(ctx.value),
471
1554
  messageKey: () => "required_unless",
472
- replacements: (ctx) => ({ other: ctx.parameters[0] ?? "", values: ctx.parameters.slice(1).join(", ") })
1555
+ replacements: otherAndValues
473
1556
  },
474
1557
  required_with: {
475
1558
  implicit: true,
476
1559
  validate: (ctx) => !anyPresent(ctx.parameters, ctx.data) || !isEmptyValue(ctx.value),
477
1560
  messageKey: () => "required_with",
478
- replacements: (ctx) => ({ values: ctx.parameters.join(" / ") })
1561
+ replacements: fieldList
479
1562
  },
480
1563
  required_with_all: {
481
1564
  implicit: true,
482
1565
  validate: (ctx) => !allPresent(ctx.parameters, ctx.data) || !isEmptyValue(ctx.value),
483
1566
  messageKey: () => "required_with_all",
484
- replacements: (ctx) => ({ values: ctx.parameters.join(" / ") })
1567
+ replacements: fieldList
485
1568
  },
486
1569
  required_without: {
487
1570
  implicit: true,
488
1571
  validate: (ctx) => ctx.parameters.every((f) => isFieldPresent(f, ctx.data)) || !isEmptyValue(ctx.value),
489
1572
  messageKey: () => "required_without",
490
- replacements: (ctx) => ({ values: ctx.parameters.join(" / ") })
1573
+ replacements: fieldList
491
1574
  },
492
1575
  required_without_all: {
493
1576
  implicit: true,
494
1577
  validate: (ctx) => anyPresent(ctx.parameters, ctx.data) || !isEmptyValue(ctx.value),
495
1578
  messageKey: () => "required_without_all",
496
- 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
497
1646
  }
498
1647
  };
499
1648
 
500
- // rules/type.ts
501
- var BOOLEAN_VALUES = [true, false, 0, 1, "0", "1"];
502
- var NUMERIC_STRING_RE = /^\s*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\s*$/;
503
- function isNumeric(value) {
504
- if (typeof value === "number") return Number.isFinite(value);
505
- return typeof value === "string" && NUMERIC_STRING_RE.test(value);
506
- }
507
- var typeRules = {
508
- string: { validate: (ctx) => typeof ctx.value === "string", messageKey: () => "string" },
509
- integer: {
510
- validate: (ctx) => typeof ctx.value !== "object" && /^-?\d+$/.test(String(ctx.value).trim()),
511
- 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
512
1657
  },
513
- numeric: {
514
- validate: (ctx) => isNumeric(ctx.value),
515
- 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] ?? "") })
516
1667
  },
517
- boolean: { validate: (ctx) => BOOLEAN_VALUES.includes(ctx.value), messageKey: () => "boolean" },
518
- array: { validate: (ctx) => Array.isArray(ctx.value), messageKey: () => "array" }
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] ?? "") })
1672
+ },
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
+ }
519
1679
  };
520
1680
 
521
1681
  // rules/size.ts
1682
+ import { Arr as Arr10 } from "@orieljs/support";
522
1683
  function hasAny(ctx, names) {
523
1684
  return names.some((name) => ctx.ruleNames.includes(name));
524
1685
  }
525
- function isPlainObject(value) {
1686
+ var NUMERIC_RULES = ["numeric", "integer", "decimal"];
1687
+ function isPlainObject2(value) {
526
1688
  if (typeof value !== "object" || value === null) return false;
527
1689
  const proto = Object.getPrototypeOf(value);
528
1690
  return proto === Object.prototype || proto === null;
529
1691
  }
530
- function measure(ctx) {
531
- const { value } = ctx;
532
- if (hasAny(ctx, ["numeric", "integer"])) {
1692
+ function measure(ctx, value = ctx.value, numeric = hasAny(ctx, NUMERIC_RULES)) {
1693
+ if (numeric) {
533
1694
  return { type: "numeric", size: isNumeric(value) ? Number(value) : stringLength(value) };
534
1695
  }
535
1696
  if (Array.isArray(value)) return { type: "array", size: value.length };
536
1697
  if (isUploadedFileLike(value)) return { type: "file", size: value.size / 1024 };
537
1698
  if (hasAny(ctx, ["file", "image"])) return { type: "file", size: Number.NaN };
538
- if (isPlainObject(value)) return { type: "array", size: Object.keys(value).length };
1699
+ if (isPlainObject2(value)) return { type: "array", size: Object.keys(value).length };
539
1700
  if (hasAny(ctx, ["array"])) return { type: "array", size: stringLength(value) };
540
1701
  return { type: "string", size: stringLength(value) };
541
1702
  }
@@ -546,6 +1707,41 @@ function stringLength(value) {
546
1707
  function sizeOf(ctx) {
547
1708
  return measure(ctx).size;
548
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
+ });
549
1745
  function sizeType(ctx) {
550
1746
  return measure(ctx).type;
551
1747
  }
@@ -573,27 +1769,193 @@ var sizeRules = {
573
1769
  messageKey: (ctx) => `size.${sizeType(ctx)}`,
574
1770
  replacements: (ctx) => ({ size: ctx.parameters[0] ?? "" })
575
1771
  },
576
- in: { validate: (ctx) => ctx.parameters.includes(String(ctx.value)), messageKey: () => "in" },
577
- 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" }
578
1850
  };
579
1851
 
580
1852
  // registry.ts
581
1853
  var builtinRules = {
582
1854
  ...presenceRules,
1855
+ ...acceptanceRules,
1856
+ ...prohibitedRules,
1857
+ ...excludeRules,
583
1858
  ...typeRules,
1859
+ ...arrayRules,
584
1860
  ...formatRules,
1861
+ ...dateRules,
1862
+ ...networkRules,
1863
+ ...timezoneRules,
585
1864
  ...sizeRules,
1865
+ ...numericRules,
586
1866
  ...fileRules,
587
- ...databaseRules
1867
+ ...databaseRules,
1868
+ ...authRules
1869
+ };
1870
+
1871
+ // rules/password.ts
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
+ }
1876
+ var PasswordRule = class _PasswordRule {
1877
+ #min;
1878
+ #max;
1879
+ #letters = false;
1880
+ #mixedCase = false;
1881
+ #numbers = false;
1882
+ #symbols = false;
1883
+ constructor(min = 8) {
1884
+ this.#min = Math.max(1, min);
1885
+ }
1886
+ /** A password rule of at least `size` characters: PasswordRule.min(12).mixedCase(). */
1887
+ static min(size) {
1888
+ return new _PasswordRule(size);
1889
+ }
1890
+ static defaults(rule) {
1891
+ if (rule !== void 0) {
1892
+ defaultRule = rule ?? void 0;
1893
+ return;
1894
+ }
1895
+ const resolved = typeof defaultRule === "function" ? defaultRule() : defaultRule;
1896
+ return resolved === void 0 ? new _PasswordRule(8) : resolved.#copy();
1897
+ }
1898
+ min(size) {
1899
+ this.#min = Math.max(1, size);
1900
+ return this;
1901
+ }
1902
+ max(size) {
1903
+ this.#max = size;
1904
+ return this;
1905
+ }
1906
+ /** At least one letter. */
1907
+ letters() {
1908
+ this.#letters = true;
1909
+ return this;
1910
+ }
1911
+ /** At least one uppercase and one lowercase letter. */
1912
+ mixedCase() {
1913
+ this.#mixedCase = true;
1914
+ return this;
1915
+ }
1916
+ /** At least one number. */
1917
+ numbers() {
1918
+ this.#numbers = true;
1919
+ return this;
1920
+ }
1921
+ /** At least one symbol (punctuation, a symbol or a space). */
1922
+ symbols() {
1923
+ this.#symbols = true;
1924
+ return this;
1925
+ }
1926
+ validate(_attribute, value, fail) {
1927
+ const say = (key, replacements = {}) => fail(applyReplacements(DEFAULT_MESSAGES[key], replacements));
1928
+ if (typeof value !== "string") return say("string");
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");
1933
+ if (this.#letters && !/\p{L}/u.test(value)) say("password.letters");
1934
+ if (this.#symbols && !/\p{Z}|\p{S}|\p{P}/u.test(value)) say("password.symbols");
1935
+ if (this.#numbers && !/\p{N}/u.test(value)) say("password.numbers");
1936
+ }
1937
+ #copy() {
1938
+ const copy = new _PasswordRule(this.#min);
1939
+ copy.#max = this.#max;
1940
+ copy.#letters = this.#letters;
1941
+ copy.#mixedCase = this.#mixedCase;
1942
+ copy.#numbers = this.#numbers;
1943
+ copy.#symbols = this.#symbols;
1944
+ return copy;
1945
+ }
588
1946
  };
589
1947
 
590
1948
  // Rule.ts
591
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
+ }
592
1954
  // Declares all 4 contract parameters so a subclass's inferred type (from `Rule.in(...)` etc.) still accepts a
593
1955
  // call with the full 4-arg ValidationRule.validate signature, as Rule.test.ts makes.
594
1956
  async validate(attribute, value, fail, data, expandedFromWildcard = false) {
595
1957
  if (await this.passes(attribute, value, data, expandedFromWildcard)) return;
596
- fail(applyReplacements(DEFAULT_MESSAGES[this.ruleName], { attribute: defaultAttributeName(attribute) }));
1958
+ fail(applyReplacements(DEFAULT_MESSAGES[this.ruleName], { attribute: defaultAttributeName(attribute), values: this.messageParameters().join(", ") }));
597
1959
  }
598
1960
  };
599
1961
  var InRule = class extends BuiltinRuleObject {
@@ -603,8 +1965,145 @@ var InRule = class extends BuiltinRuleObject {
603
1965
  }
604
1966
  values;
605
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
+ }
606
2001
  passes(_attribute, value) {
607
- return this.values.map(String).includes(String(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;
2015
+ passes(_attribute, 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;
608
2107
  }
609
2108
  };
610
2109
  function isModelInstance(value) {
@@ -657,14 +2156,95 @@ var ExistsRule = class extends BuiltinRuleObject {
657
2156
  }
658
2157
  };
659
2158
  var Rule = {
2159
+ /** One of `values`, compared as text (as `in:a,b`); typed as those values: `Rule.in(['draft', 'publish'])`. */
660
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),
661
2183
  unique: (table, column) => new UniqueRule(table, column),
662
- exists: (table, column) => new ExistsRule(table, column)
2184
+ exists: (table, column) => new ExistsRule(table, column),
2185
+ /** Laravel's Password::defaults(): the application's password rule (at least 8 characters unless PasswordRule.defaults() changed it). */
2186
+ password: () => PasswordRule.defaults()
663
2187
  };
664
2188
 
665
2189
  // Validator.ts
666
- import { Arr as Arr4 } from "@orieljs/support";
2190
+ import { Arr as Arr11 } from "@orieljs/support";
667
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
+ }
668
2248
  function buildNested(flat) {
669
2249
  const root = {};
670
2250
  const created = /* @__PURE__ */ new Set([root]);
@@ -701,6 +2281,49 @@ function cloneContainer(existing) {
701
2281
  }
702
2282
  return container;
703
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
+ }
704
2327
  function arrayify(node, created) {
705
2328
  if (node === null || typeof node !== "object" || !created.has(node)) return node;
706
2329
  const entries = Object.entries(node).map(([k, v]) => [k, arrayify(v, created)]);
@@ -726,11 +2349,18 @@ var ValidatorImpl = class {
726
2349
  bag;
727
2350
  ranPromise;
728
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();
729
2359
  after(callback) {
730
2360
  this.afterCallbacks.push(callback);
731
2361
  return this;
732
2362
  }
733
- /** 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. */
734
2364
  fail(field, message) {
735
2365
  this.bag ??= new MessageBagImpl();
736
2366
  this.bag.add(field, message);
@@ -747,39 +2377,143 @@ var ValidatorImpl = class {
747
2377
  }
748
2378
  async validated() {
749
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);
750
2381
  const flat = {};
751
2382
  for (const pattern of Object.keys(this.rules)) {
752
2383
  for (const field of this.fieldsFor(pattern)) {
753
- 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);
2385
+ }
2386
+ }
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;
754
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;
755
2445
  }
756
- return buildNested(flat);
757
2446
  }
758
2447
  fieldsFor(pattern) {
759
2448
  return pattern.includes("*") ? expandWildcardKeys(pattern, this.data) : [pattern];
760
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
+ }
761
2486
  async run() {
762
2487
  this.bag = new MessageBagImpl();
763
- for (const [pattern, spec] of Object.entries(this.rules)) {
764
- const parsed = parseRuleSpec(spec);
2488
+ this.cache = /* @__PURE__ */ new Map();
2489
+ for (const [pattern, parsed] of this.parsed()) {
765
2490
  const names = parsed.filter((r) => r.kind === "named").map((r) => r.name);
766
2491
  const bail = names.includes("bail");
767
2492
  const nullable = names.includes("nullable");
768
2493
  const sometimes = names.includes("sometimes");
769
2494
  for (const field of this.fieldsFor(pattern)) {
770
- const present = Arr4.has(this.data, field);
2495
+ if (this.isExcluded(field)) continue;
2496
+ const present = Arr11.has(this.data, field);
771
2497
  if (sometimes && !present) continue;
772
- const value = Arr4.get(this.data, field);
2498
+ const value = Arr11.get(this.data, field);
773
2499
  const isNull = value === null || value === void 0;
774
2500
  const isBlank = typeof value === "string" && value.trim() === "";
775
2501
  for (const rule of parsed) {
776
2502
  if (rule.kind === "named" && SKIPPED_MODIFIERS.has(rule.name)) continue;
777
- 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;
778
2512
  if (!implicit && (!present || isBlank)) continue;
779
2513
  if (!implicit && nullable && isNull) continue;
780
- const message = await this.runRule(rule, field, pattern, value, names);
781
- if (message !== void 0) {
782
- this.bag.add(field, message);
2514
+ const messages = await this.runRule(rule, field, pattern, value, names);
2515
+ if (messages.length > 0) {
2516
+ for (const message of messages) this.bag.add(field, message);
783
2517
  if (bail || implicit) break;
784
2518
  }
785
2519
  }
@@ -788,30 +2522,33 @@ var ValidatorImpl = class {
788
2522
  for (const callback of this.afterCallbacks) await callback(this);
789
2523
  return this.bag.isEmpty();
790
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
+ }
791
2528
  async runRule(rule, field, pattern, value, ruleNames) {
792
2529
  const expanded = pattern.includes("*");
793
2530
  if (rule.kind === "object") {
794
2531
  if (rule.rule instanceof BuiltinRuleObject) {
795
- if (await rule.rule.passes(field, value, this.data, expanded)) return void 0;
796
- const ctx2 = { attribute: field, expandedFromWildcard: expanded, value, parameters: [], data: this.data, ruleNames };
797
- return this.failureMessage(rule.rule.ruleName, this.registry[rule.rule.ruleName], ctx2, pattern);
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);
2534
+ return [this.failureMessage(rule.rule.ruleName, this.registry[rule.rule.ruleName], ctx2, pattern)];
798
2535
  }
799
- let failure;
800
- await rule.rule.validate(field, value, (message) => {
801
- failure = message;
802
- }, this.data);
803
- return failure;
2536
+ const failures = [];
2537
+ const attribute = this.displayName(field);
2538
+ await rule.rule.validate(field, value, (message) => void failures.push(applyReplacements(message, { attribute })), this.data);
2539
+ return failures;
804
2540
  }
805
2541
  const builtin = this.registry[rule.name];
806
2542
  if (!builtin) throw new UnknownValidationRuleError(rule.name);
807
- const ctx = { attribute: field, expandedFromWildcard: expanded, value, parameters: rule.parameters, data: this.data, ruleNames };
808
- if (await builtin.validate(ctx)) return void 0;
809
- return this.failureMessage(rule.name, builtin, ctx, pattern);
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);
2545
+ if (await builtin.validate(ctx)) return [];
2546
+ return [this.failureMessage(rule.name, builtin, ctx, pattern)];
810
2547
  }
811
2548
  /** Custom message (concrete field, then wildcard pattern, then rule name) or the default, with :attribute filled. */
812
2549
  failureMessage(name, builtin, ctx, pattern) {
813
2550
  const field = ctx.attribute;
814
- const template = this.customMessages[`${field}.${name}`] ?? this.customMessages[`${pattern}.${name}`] ?? this.customMessages[name] ?? DEFAULT_MESSAGES[builtin?.messageKey(ctx) ?? name] ?? builtin?.defaultMessage ?? "The :attribute field is invalid.";
2551
+ const template = this.customMessages[`${field}.${name}`] ?? this.customMessages[`${pattern}.${name}`] ?? this.customMessages[name] ?? builtin?.message ?? DEFAULT_MESSAGES[builtin?.messageKey(ctx) ?? name] ?? builtin?.defaultMessage ?? "The :attribute field is invalid.";
815
2552
  const display = this.customAttributes[field] ?? this.customAttributes[pattern] ?? defaultAttributeName(field);
816
2553
  return applyReplacements(template, { attribute: display, ...builtin?.replacements?.(ctx) ?? {} });
817
2554
  }
@@ -823,10 +2560,15 @@ var ValidationFactoryImpl = class {
823
2560
  make(data, rules, messages = {}, attributes = {}) {
824
2561
  return new ValidatorImpl(data, rules, messages, attributes, { ...builtinRules, ...this.extended });
825
2562
  }
2563
+ /** Every rule this factory knows, built-in and extend()ed, sorted (for editors: `oriel ide:info`). */
2564
+ ruleNames() {
2565
+ return [.../* @__PURE__ */ new Set([...Object.keys(builtinRules), ...Object.keys(this.extended)])].sort();
2566
+ }
826
2567
  extend(name, callback, message) {
827
2568
  this.extended[name] = {
828
2569
  validate: (ctx) => callback(ctx.attribute, ctx.value, ctx.parameters, ctx.data),
829
2570
  messageKey: () => name,
2571
+ ...message === void 0 ? {} : { message },
830
2572
  defaultMessage: message ?? "The :attribute field is invalid."
831
2573
  };
832
2574
  }
@@ -852,9 +2594,11 @@ export {
852
2594
  FormRequest,
853
2595
  InvalidRuleParameterError,
854
2596
  MessageBagImpl,
2597
+ PasswordRule,
855
2598
  Rule,
856
2599
  UnknownValidationRuleError,
857
2600
  ValidationErrorImpl,
2601
+ ValidationException,
858
2602
  ValidationFactoryImpl,
859
2603
  ValidationServiceProvider,
860
2604
  Validator,