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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE CHANGED
@@ -7,3 +7,10 @@ This product includes software developed by the Oriel project
7
7
  Oriel is licensed under the Apache License, Version 2.0 (see LICENSE).
8
8
  The Oriel name and logo are not covered by that license; see TRADEMARKS.md
9
9
  (https://github.com/orieljs/framework/blob/main/TRADEMARKS.md).
10
+
11
+ @orieljs/support (and @orieljs/framework, which bundles it) includes transliteration data from
12
+ voku/portable-ascii (https://github.com/voku/portable-ascii), Copyright (C) 2019 Lars Moelleken, MIT License.
13
+
14
+ @orieljs/notifications (and @orieljs/framework, which bundles it) includes the HTML and CSS of
15
+ Laravel's default notification mail (laravel/framework, https://github.com/laravel/framework),
16
+ Copyright (c) Taylor Otwell, MIT License.
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @orieljs/validation
2
+
3
+ Validation for Oriel: rules for incoming data, form requests and error messages, as Laravel's validation. It is part of `@orieljs/framework`, which every Oriel app already installs, and is re-exported from it. In an app, import it from `@orieljs/framework`; `@orieljs/validation` is also published on its own.
4
+
5
+ ```bash
6
+ npm install @orieljs/framework
7
+ ```
8
+
9
+ ```ts title="app/Http/Requests/StorePostRequest.ts"
10
+ import { FormRequest, Rule } from '@orieljs/framework'
11
+
12
+ export class StorePostRequest extends FormRequest {
13
+ rules() {
14
+ return {
15
+ title: 'required|string|max:255',
16
+ slug: ['required', 'alpha_dash', Rule.unique('posts', 'slug')],
17
+ body: 'required|string',
18
+ published_at: 'nullable|date',
19
+ } as const
20
+ }
21
+ }
22
+ ```
23
+
24
+ In a controller, `await StorePostRequest.validate()` runs `authorize()` and `rules()` and returns the validated data, typed from the rules: write `rules()` with `as const` and no return type (it may be `async`). A failure sends the user back with the errors and the old input, or answers 422 with JSON. `await request().validate(rules)` and `Validator.make(data, rules)` validate without a class, and `Rule.unique()`, `Rule.exists()`, `Rule.in()` and `Rule.password()` build rule objects.
25
+
26
+ Documentation: https://orieljs.vercel.app/docs/validation
27
+
28
+ Status: early alpha. APIs will change.
29
+
30
+ ## License
31
+
32
+ Oriel is open-source software licensed under the Apache License 2.0 (Apache-2.0; see `LICENSE` and `NOTICE`). The Oriel name and logo are trademarks; see `TRADEMARKS.md`.
33
+
34
+ Website: https://orieljs.vercel.app
package/TRADEMARKS.md ADDED
@@ -0,0 +1,33 @@
1
+ # Oriel trademark policy
2
+
3
+ The Oriel code is open source under the Apache License 2.0. The **Oriel name and logo** are not: the license covers
4
+ the code only (section 6 of the license says so), and the name and logo stay with the project. This page explains
5
+ what you can and can't do with them. The goal is simple: people should always be able to tell what is the real Oriel
6
+ and what isn't.
7
+
8
+ ## You can, without asking
9
+
10
+ - Use, change, ship and sell the code, as the license allows.
11
+ - Say that your app or package is "built with Oriel", "for Oriel" or "compatible with Oriel".
12
+ - Name a package `oriel-<something>` or `<something>-for-oriel` when it works with Oriel, as long as it is clear that
13
+ it is not official (for example, don't publish it under the `@orieljs` npm scope).
14
+ - Use the name and logo to write about Oriel, teach it, or link to it, in articles, talks, videos and courses.
15
+ - Use the unmodified logo to point to the project, as it is (no recolouring, cropping or
16
+ distortion).
17
+
18
+ ## You can't, without written permission
19
+
20
+ - Call a fork, a modified copy or a competing product "Oriel", or anything likely to be confused with it.
21
+ - Use the name or logo as the name, logo or domain of your own product, company or service.
22
+ - Suggest that the Oriel project made, endorses or supports your work when it doesn't.
23
+ - Sell merchandise with the name or logo.
24
+
25
+ ## Forks
26
+
27
+ Forks are welcome. Please give yours a different name and logo, keep the `LICENSE` and `NOTICE` files, and say that
28
+ it is based on Oriel.
29
+
30
+ ## Questions
31
+
32
+ Open an issue at https://github.com/orieljs/framework or email meeeet.dev@gmail.com. If you're unsure whether a use
33
+ is fine, ask; the answer is usually yes.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { OrielError, ValidationError, Rules, MessageBag, ValidationRule, Validator as Validator$1, ValidationFactory, RuleCallback } from '@orieljs/contracts';
1
+ import { OrielError, ValidationError, Rules, InferValidatedType, MessageBag, ValidationRule, TypedValidationRule, RuleTyping, InValue, UploadedFile, Validator as Validator$1, ValidationFactory, RuleCallback } from '@orieljs/contracts';
2
+ export { InferRuleType, InferValidatedType } from '@orieljs/contracts';
2
3
  import * as _orieljs_support from '@orieljs/support';
3
4
  import { ServiceProvider } from '@orieljs/support';
4
5
 
@@ -41,34 +42,38 @@ declare class InvalidRuleParameterError extends Error implements OrielError {
41
42
  constructor(rule: string, parameter: string, reason: string);
42
43
  }
43
44
 
45
+ /** The data a FormRequest's validate() returns, typed from its rules(). */
46
+ type ValidatedData<F extends FormRequest> = InferValidatedType<Awaited<ReturnType<F['rules']>>>;
44
47
  /**
45
48
  * Mirrors Illuminate\Foundation\Http\FormRequest, minus dependency injection by type (Oriel resolves the current
46
49
  * request and the ValidationFactory through inject(), never through constructor parameters — see spec §3 rule 5).
47
50
  * A subclass only has to implement rules(); authorize()/messages()/attributes() default to Laravel's own defaults.
48
51
  */
49
52
  declare abstract class FormRequest {
50
- abstract rules(): Rules;
53
+ /** May be async, to await something first: `async rules() { const user = await request().user<User>() … }`. */
54
+ abstract rules(): Rules | Promise<Rules>;
51
55
  authorize(): boolean | Promise<boolean>;
52
56
  messages(): Record<string, string>;
53
57
  attributes(): Record<string, string>;
54
58
  /**
55
59
  * `const data = await StorePostRequest.validate()` (spec §4.3). Resolves the current request and the
56
60
  * 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()).
61
+ * returns false), then validates request.all() against rules()/messages()/attributes() and returns the validated
62
+ * data (or throws the 422 ValidationError — see ValidatorImpl.validated()). The data is typed from rules(): write
63
+ * it without a return type and with `as const` (`return { title: 'required|string' } as const`), so its rule
64
+ * strings keep their text; a rules() typed `Rules` gives `Record<string, unknown>`.
59
65
  */
60
- static validate<T = Record<string, unknown>>(this: new () => FormRequest): Promise<T>;
66
+ static validate<F extends FormRequest>(this: new () => F): Promise<ValidatedData<F>>;
61
67
  }
62
68
 
63
- /** Humanizes a (possibly dotted) attribute key, e.g. "items.0.product_name" -> "items.0.product name". */
69
+ /** Humanizes a (possibly dotted) attribute key, e.g. "items.0.product_name" -> "items.0.product name" ("colors.*" stays). */
64
70
  declare function defaultAttributeName(key: string): string;
65
71
  declare function applyReplacements(template: string, replacements: Record<string, string>): string;
66
- /** Laravel's English lang/en/validation.php, reproduced as closely as possible (see plan Notes/assumptions). */
72
+ /** Laravel's English lang/en/validation.php, reproduced as closely as possible. */
67
73
  declare const DEFAULT_MESSAGES: Record<string, string>;
68
74
 
69
75
  declare class MessageBagImpl implements MessageBag {
70
76
  private readonly items;
71
- /** Not part of the MessageBag contract — used internally by ValidatorImpl and by after() hooks. */
72
77
  add(key: string, message: string): void;
73
78
  has(key: string): boolean;
74
79
  first(key?: string): string | undefined;
@@ -90,6 +95,17 @@ interface RuleContext {
90
95
  data: Record<string, unknown>;
91
96
  /** Every named rule attached to this field (for size-type resolution: numeric/array/file/string). */
92
97
  ruleNames: string[];
98
+ /** The rule key as written, e.g. "items.*.email" (distinct reads every row of it). */
99
+ pattern?: string;
100
+ /**
101
+ * What the rules keep for one validation pass, such as every row a wildcard names: each pass starts with an empty
102
+ * one, so nothing outlives the data it was read from. Absent when a rule runs on its own.
103
+ */
104
+ cache?: Map<string, unknown>;
105
+ /** The parameters of rule `name` on `field` (a concrete path), or undefined when that field has no such rule. */
106
+ getRule?(field: string, name: string): string[] | undefined;
107
+ /** How a message names `field`: its custom attribute, else the humanized key. */
108
+ displayName?(field: string): string;
93
109
  }
94
110
  interface BuiltinRule {
95
111
  validate(ctx: RuleContext): boolean | Promise<boolean>;
@@ -103,6 +119,11 @@ interface BuiltinRule {
103
119
  message?: string;
104
120
  /** Runs even when the value is missing, blank, or null under nullable (the required* family, and accepted); a failure stops the field. */
105
121
  implicit?: boolean;
122
+ /**
123
+ * An exclude rule (`exclude`, `exclude_if`, …): it always runs, and when validate() returns false the field and its
124
+ * children are skipped from then on and left out of the validated data, with no message.
125
+ */
126
+ excludes?: boolean;
106
127
  }
107
128
  type ParsedRule = {
108
129
  kind: 'named';
@@ -112,8 +133,18 @@ type ParsedRule = {
112
133
  kind: 'object';
113
134
  rule: ValidationRule;
114
135
  };
115
- declare function parseRuleSpec(spec: string | ReadonlyArray<string | ValidationRule>): ParsedRule[];
116
- /** Laravel's definition of "empty" for the required family: null, undefined, '' and []. false and 0 are NOT empty. */
136
+ /**
137
+ * A rule object that stands for string rules (Rule.requiredIf(), Rule.when(), Rule.array(), …), as Laravel's
138
+ * ValidationRuleParser reads them: the rules expandRules() returns, given the data, take its place.
139
+ */
140
+ interface ExpandingRule extends ValidationRule {
141
+ expandRules(data: Record<string, unknown>): string | ReadonlyArray<string | ValidationRule>;
142
+ }
143
+ declare function parseRuleSpec(spec: string | ReadonlyArray<string | ValidationRule>, data?: Record<string, unknown>): ParsedRule[];
144
+ /**
145
+ * Laravel's definition of "empty" for the required family (validateRequired()): null, undefined, text that is blank
146
+ * once trimmed, an empty array and an empty plain object (a JSON object is a PHP array). false and 0 are NOT empty.
147
+ */
117
148
  declare function isEmptyValue(value: unknown): boolean;
118
149
  /** Duck-types Contracts/Http's UploadedFile without importing @orieljs/http. */
119
150
  declare function isUploadedFileLike(value: unknown): value is {
@@ -126,8 +157,8 @@ declare function isUploadedFileLike(value: unknown): value is {
126
157
  /**
127
158
  * Expands a `*`-wildcard rule pattern (e.g. "items.*.name") into every concrete path present in `data`
128
159
  * (e.g. ["items.0.name", "items.1.name"]). A pattern with no `*` is returned unchanged. A `*` segment whose
129
- * container is missing, not an array and not a plain object expands to nothing for that branch (Review Focus #4:
130
- * every reported error is keyed by a concrete path, never the literal pattern). Object keys in UNSAFE_KEYS are
160
+ * container is missing, not an array and not a plain object expands to nothing for that branch, so every reported
161
+ * error is keyed by a concrete path, never the literal pattern. Object keys in UNSAFE_KEYS are
131
162
  * treated as absent, so a JSON body like {"items":{"__proto__":{...}}} never yields a path through the prototype.
132
163
  */
133
164
  declare function expandWildcardKeys(pattern: string, data: Record<string, unknown>): string[];
@@ -141,8 +172,14 @@ declare const builtinRules: Record<string, BuiltinRule>;
141
172
  * Rule.password() returns the application default (PasswordRule.defaults()). (Laravel's uncompromised() is not
142
173
  * ported: it calls an outside service.)
143
174
  */
144
- declare class PasswordRule implements ValidationRule {
175
+ declare class PasswordRule implements TypedValidationRule<{
176
+ value: string;
177
+ }> {
145
178
  #private;
179
+ /** A passing value is a string (for the validated data's type); never set at run time. */
180
+ readonly __typing?: {
181
+ value: string;
182
+ };
146
183
  constructor(min?: number);
147
184
  /** A password rule of at least `size` characters: PasswordRule.min(12).mixedCase(). */
148
185
  static min(size: number): PasswordRule;
@@ -162,7 +199,7 @@ declare class PasswordRule implements ValidationRule {
162
199
  numbers(): this;
163
200
  /** At least one symbol (punctuation, a symbol or a space). */
164
201
  symbols(): this;
165
- validate(attribute: string, value: unknown, fail: (message: string) => void): void;
202
+ validate(_attribute: string, value: unknown, fail: (message: string) => void): void;
166
203
  }
167
204
 
168
205
  /**
@@ -171,16 +208,120 @@ declare class PasswordRule implements ValidationRule {
171
208
  * ("email.unique", "items.*.email.unique", "unique"), custom attributes, then the default text. Called directly
172
209
  * as a plain ValidationRule, validate() fails with the default English message.
173
210
  */
174
- declare abstract class BuiltinRuleObject implements ValidationRule {
211
+ declare abstract class BuiltinRuleObject<T extends RuleTyping = Record<never, never>> implements TypedValidationRule<T> {
175
212
  abstract readonly ruleName: string;
176
- abstract passes(attribute: string, value: unknown, data: Record<string, unknown>, expandedFromWildcard?: boolean): boolean | Promise<boolean>;
213
+ /** What a passing value is, for the validated data's type (`T`); never set at run time. */
214
+ readonly __typing?: T;
215
+ /** `ruleNames`: the field's named rules, when it runs inside a Validator (`Rule.in()` reads `array`). */
216
+ abstract passes(attribute: string, value: unknown, data: Record<string, unknown>, expandedFromWildcard?: boolean, ruleNames?: readonly string[]): boolean | Promise<boolean>;
217
+ /** The parameters its message shows (`:values` of Rule.doesntContain()), as the string rule's would be. */
218
+ messageParameters(): string[];
177
219
  validate(attribute: string, value: unknown, fail: (message: string) => void, data: Record<string, unknown>, expandedFromWildcard?: boolean): Promise<void>;
178
220
  }
179
- declare class InRule extends BuiltinRuleObject {
221
+ /**
222
+ * What `Rule.in(values)` lets through. A list the compiler only knows as `string[]` says nothing: any value whose text
223
+ * is listed passes (the number 1 for '1'), so `string` would be too narrow. A rule string known only as `string` is
224
+ * treated the same way.
225
+ */
226
+ type InTyping<V> = string extends V ? Record<never, never> : {
227
+ value: InValue<V>;
228
+ each: InValue<V>;
229
+ };
230
+ declare class InRule<V> extends BuiltinRuleObject<InTyping<V>> {
180
231
  private readonly values;
181
232
  readonly ruleName = "in";
233
+ constructor(values: readonly V[]);
234
+ passes(_attribute: string, value: unknown, _data?: Record<string, unknown>, _expanded?: boolean, ruleNames?: readonly string[]): boolean;
235
+ }
236
+ declare class NotInRule extends BuiltinRuleObject {
237
+ private readonly values;
238
+ readonly ruleName = "not_in";
182
239
  constructor(values: readonly unknown[]);
240
+ passes(_attribute: string, value: unknown, _data?: Record<string, unknown>, _expanded?: boolean, ruleNames?: readonly string[]): boolean;
241
+ }
242
+ /** A TypeScript enum, or an `as const` object of string and number values. */
243
+ type EnumLike = Record<string, string | number>;
244
+ /**
245
+ * Laravel's Rule::enum(): one of the enum's values; for a numeric enum, its number or exactly the text of it (a form
246
+ * sends text: `'1'`, not `'1.0'` or `'01'`). only() and except() narrow the values.
247
+ */
248
+ declare class EnumRule<E extends EnumLike> extends BuiltinRuleObject<{
249
+ value: InValue<E[keyof E]>;
250
+ }> {
251
+ #private;
252
+ private readonly type;
253
+ readonly ruleName = "enum";
254
+ constructor(type: E);
255
+ only(values: E[keyof E] | ReadonlyArray<E[keyof E]>): this;
256
+ except(values: E[keyof E] | ReadonlyArray<E[keyof E]>): this;
257
+ passes(_attribute: string, value: unknown): boolean;
258
+ }
259
+ /** Laravel's Rule::contains() and Rule::doesntContain(): every value is an item of the array, or none is. */
260
+ declare class ContainsRule extends BuiltinRuleObject<{
261
+ value: unknown[];
262
+ }> {
263
+ readonly ruleName: 'contains' | 'doesnt_contain';
264
+ private readonly values;
265
+ constructor(ruleName: 'contains' | 'doesnt_contain', values: readonly unknown[]);
183
266
  passes(_attribute: string, value: unknown): boolean;
267
+ messageParameters(): string[];
268
+ }
269
+ /** Rules for the field: a rule string, a list, or a callback given the data that returns one. */
270
+ type RuleSet = string | ReadonlyArray<string | ValidationRule> | ((data: Record<string, unknown>) => string | ReadonlyArray<string | ValidationRule>);
271
+ /**
272
+ * Base for the builders that stand for string rules (Laravel's RequiredIf, ConditionalRules, ArrayRule, Dimensions):
273
+ * the Validator puts the rules expandRules() returns in their place, so they run only inside a Validator.
274
+ */
275
+ declare abstract class ExpandingRuleObject<T extends RuleTyping = Record<never, never>> implements ExpandingRule, TypedValidationRule<T> {
276
+ readonly __typing?: T;
277
+ abstract expandRules(data: Record<string, unknown>): string | ReadonlyArray<string | ValidationRule>;
278
+ validate(): never;
279
+ }
280
+ /** Rule.requiredIf(), Rule.excludeIf(), Rule.prohibitedIf(): the rule when the condition holds, else nothing. */
281
+ declare class ConditionalRule<T extends RuleTyping = Record<never, never>> extends ExpandingRuleObject<T> {
282
+ private readonly rule;
283
+ private readonly condition;
284
+ constructor(rule: 'required' | 'exclude' | 'prohibited', condition: boolean | (() => boolean));
285
+ expandRules(): string;
286
+ }
287
+ /** Laravel's ConditionalRules (Rule.when() and Rule.unless()): one set of rules or the other, chosen from the data. */
288
+ declare class ConditionalRules extends ExpandingRuleObject {
289
+ private readonly condition;
290
+ private readonly rules;
291
+ private readonly defaultRules;
292
+ private readonly negate;
293
+ constructor(condition: boolean | ((data: Record<string, unknown>) => boolean), rules: RuleSet, defaultRules: RuleSet, negate?: boolean);
294
+ expandRules(data: Record<string, unknown>): string | ReadonlyArray<string | ValidationRule>;
295
+ }
296
+ /** Laravel's Rule::array(): `array`, or `array:a,b` (an object whose keys are among those named). */
297
+ declare class ArrayRule<K extends string> extends ExpandingRuleObject<{
298
+ value: [K] extends [never] ? unknown[] : {
299
+ [P in K]?: unknown;
300
+ };
301
+ }> {
302
+ private readonly keys;
303
+ constructor(keys: readonly K[]);
304
+ expandRules(): string;
305
+ }
306
+ type DimensionConstraints = Partial<Record<'width' | 'height' | 'min_width' | 'min_height' | 'max_width' | 'max_height' | 'ratio', number>>;
307
+ /** Laravel's Rule::dimensions(): the dimensions rule, from an object or fluently (`.maxWidth(1000).ratio(3 / 2)`). */
308
+ declare class DimensionsRule extends ExpandingRuleObject<{
309
+ value: UploadedFile;
310
+ }> {
311
+ #private;
312
+ constructor(constraints: DimensionConstraints);
313
+ width(pixels: number): this;
314
+ height(pixels: number): this;
315
+ minWidth(pixels: number): this;
316
+ minHeight(pixels: number): this;
317
+ maxWidth(pixels: number): this;
318
+ maxHeight(pixels: number): this;
319
+ /** Width over height: `ratio(3 / 2)`. */
320
+ ratio(ratio: number): this;
321
+ /** `dimensions:max_width=1000,ratio=1.5`, Laravel's __toString(). */
322
+ toString(): string;
323
+ expandRules(): string;
324
+ private set;
184
325
  }
185
326
  declare class UniqueRule extends BuiltinRuleObject {
186
327
  private readonly table;
@@ -208,34 +349,38 @@ declare class ExistsRule extends BuiltinRuleObject {
208
349
  }
209
350
  /** Fluent rule-object builders, for when a plain string rule can't express what's needed (e.g. .ignore()). */
210
351
  declare const Rule: {
211
- in: (values: readonly unknown[]) => InRule;
352
+ /** One of `values`, compared as text (as `in:a,b`); typed as those values: `Rule.in(['draft', 'publish'])`. */
353
+ in: <const V extends readonly unknown[]>(values: V) => InRule<V[number]>;
354
+ /** None of `values`, compared as text (as `not_in:a,b`). */
355
+ notIn: (values: readonly unknown[]) => NotInRule;
356
+ /** One of a TypeScript enum's values (or an `as const` object's): `Rule.enum(Status)`, `.only([…])`, `.except([…])`. */
357
+ enum: <E extends EnumLike>(type: E) => EnumRule<E>;
358
+ /** An array holding every one of `values`. */
359
+ contains: (values: readonly unknown[]) => ContainsRule;
360
+ /** An array holding none of `values`. */
361
+ doesntContain: (values: readonly unknown[]) => ContainsRule;
362
+ /** `required` when the condition (or the callback) holds. */
363
+ requiredIf: (condition: boolean | (() => boolean)) => ConditionalRule;
364
+ /** `exclude` when the condition (or the callback) holds; the key is optional, as `exclude_if` makes it. */
365
+ excludeIf: (condition: boolean | (() => boolean)) => ConditionalRule<{
366
+ presence: "optional";
367
+ }>;
368
+ /** `prohibited` when the condition (or the callback) holds. */
369
+ prohibitedIf: (condition: boolean | (() => boolean)) => ConditionalRule;
370
+ /** `rules` when the condition (or the callback, given the data) holds, else `defaultRules`. */
371
+ when: (condition: boolean | ((data: Record<string, unknown>) => boolean), rules: RuleSet, defaultRules?: RuleSet) => ConditionalRules;
372
+ /** `rules` unless the condition (or the callback, given the data) holds, else `defaultRules`. */
373
+ unless: (condition: boolean | ((data: Record<string, unknown>) => boolean), rules: RuleSet, defaultRules?: RuleSet) => ConditionalRules;
374
+ /** `array`, or with keys `array:name,email` (an object whose keys are among those named). */
375
+ array: <const K extends string = never>(keys?: readonly K[]) => ArrayRule<K>;
376
+ /** The dimensions rule: `Rule.dimensions().maxWidth(1000).ratio(3 / 2)`, or `Rule.dimensions({ min_width: 100 })`. */
377
+ dimensions: (constraints?: DimensionConstraints) => DimensionsRule;
212
378
  unique: (table: string, column?: string) => UniqueRule;
213
379
  exists: (table: string, column?: string) => ExistsRule;
214
380
  /** Laravel's Password::defaults(): the application's password rule (at least 8 characters unless PasswordRule.defaults() changed it). */
215
381
  password: () => PasswordRule;
216
382
  };
217
383
 
218
- /** See Task 9's writeup in the plan for exactly what this does and doesn't infer. */
219
- type SplitPipe<S extends string> = S extends `${infer Head}|${infer Tail}` ? Head | SplitPipe<Tail> : S;
220
- type RuleName<T extends string> = T extends `${infer Name}:${string}` ? Name : T;
221
- type Names<S extends string> = RuleName<SplitPipe<S>>;
222
- type BaseType<S extends string> = 'array' extends Names<S> ? unknown[] : 'boolean' extends Names<S> ? boolean : 'integer' extends Names<S> ? number : 'numeric' extends Names<S> ? number : 'email' extends Names<S> ? string : 'url' extends Names<S> ? string : 'uuid' extends Names<S> ? string : 'date' extends Names<S> ? string : 'string' extends Names<S> ? string : unknown;
223
- /**
224
- * The inferred type for one rule string, e.g. 'nullable|integer' -> number | null. A non-literal `string` (a rules
225
- * object declared without `as const`) carries no rule names to read, so it infers unknown: without this guard every
226
- * `'x' extends string` check would match and the result would wrongly be `unknown[] | null`.
227
- */
228
- type InferRuleType<S extends string> = string extends S ? unknown : 'nullable' extends Names<S> ? BaseType<S> | null : BaseType<S>;
229
- /**
230
- * Best-effort: applies InferRuleType to every string-valued rule; array-form (mixed string/object) rules -> unknown.
231
- * Strips `readonly` (via `-readonly`) so a `Rules` object declared `as const` (the common way to get literal rule
232
- * strings for inference — see the Task 9 type test) still produces a plain, mutable validated-data shape, not a
233
- * readonly one; a mapped type otherwise carries the source's readonly modifier over by default.
234
- */
235
- type InferValidatedType<R> = {
236
- -readonly [K in keyof R]: R[K] extends string ? InferRuleType<R[K]> : unknown;
237
- };
238
-
239
384
  /**
240
385
  * Runs each field's rules the way Laravel's Validator does:
241
386
  * - a missing key, or a string that is blank after trimming, only runs implicit rules (the required* family and
@@ -245,7 +390,7 @@ type InferValidatedType<R> = {
245
390
  * - `sometimes` skips a missing field entirely;
246
391
  * - a field stops validating after its first failure under `bail`, or after any implicit rule fails.
247
392
  */
248
- declare class ValidatorImpl implements Validator$1 {
393
+ declare class ValidatorImpl<R extends Rules = Rules> implements Validator$1<R> {
249
394
  private readonly data;
250
395
  private readonly rules;
251
396
  private readonly customMessages;
@@ -255,16 +400,40 @@ declare class ValidatorImpl implements Validator$1 {
255
400
  private bag;
256
401
  private ranPromise;
257
402
  private readonly afterCallbacks;
258
- constructor(data: Record<string, unknown>, rules: Rules, customMessages: Record<string, string>, customAttributes: Record<string, string>, registry: Record<string, BuiltinRule>, errorBagName?: string);
403
+ private parsedRules;
404
+ /** The fields an exclude rule took out (with their children). */
405
+ private readonly excluded;
406
+ /** The same fields as a tree of segment runs from the root, so a lookup reads the field once. */
407
+ private readonly excludedRuns;
408
+ /** What the rules keep while one pass runs (RuleContext.cache); a new pass starts with a new one. */
409
+ private cache;
410
+ constructor(data: Record<string, unknown>, rules: R, customMessages: Record<string, string>, customAttributes: Record<string, string>, registry: Record<string, BuiltinRule>, errorBagName?: string);
259
411
  after(callback: (validator: Validator$1) => void | Promise<void>): this;
260
- /** Not part of the Validator contract — lets an after() hook add a failure (MessageBag itself is read-only). */
412
+ /** Lets an after() hook add a failure. */
261
413
  fail(field: string, message: string): void;
262
414
  passes(): Promise<boolean>;
263
415
  fails(): Promise<boolean>;
264
416
  errors(): MessageBagImpl;
265
- validated<T = Record<string, unknown>>(): Promise<T>;
417
+ validated<T = InferValidatedType<R>>(): Promise<T>;
418
+ /** Takes the field (with its children) out of validation and of the validated data. */
419
+ private exclude;
420
+ /**
421
+ * Laravel's shouldBeExcluded(): the field, or one of its parents, was taken out by an exclude rule. The field's
422
+ * segments are followed from the root (so `user` takes out `user.name`, not `username`) and each is read once,
423
+ * however many fields were excluded and however many dots the field holds.
424
+ */
425
+ private isExcluded;
266
426
  private fieldsFor;
427
+ private parsed;
428
+ /** The rule key a concrete field falls under: itself, else the first wildcard key that matches it. */
429
+ private patternOf;
430
+ /** Laravel's getRule(): the parameters of rule `name` on `field`, or undefined when the field has no such rule. */
431
+ private ruleParameters;
432
+ /** Laravel's getDisplayableAttribute(): a custom attribute for the field or its rule key, else the humanized key. */
433
+ private displayName;
434
+ private context;
267
435
  private run;
436
+ private passesExcludeRule;
268
437
  private runRule;
269
438
  /** Custom message (concrete field, then wildcard pattern, then rule name) or the default, with :attribute filled. */
270
439
  private failureMessage;
@@ -272,7 +441,7 @@ declare class ValidatorImpl implements Validator$1 {
272
441
 
273
442
  declare class ValidationFactoryImpl implements ValidationFactory {
274
443
  private readonly extended;
275
- make(data: Record<string, unknown>, rules: Rules, messages?: Record<string, string>, attributes?: Record<string, string>): ValidatorImpl;
444
+ make<const R extends Rules>(data: Record<string, unknown>, rules: R, messages?: Record<string, string>, attributes?: Record<string, string>): ValidatorImpl<R>;
276
445
  extend(name: string, callback: RuleCallback, message?: string): void;
277
446
  }
278
447
 
@@ -283,4 +452,4 @@ declare class ValidationServiceProvider extends ServiceProvider {
283
452
  register(): void;
284
453
  }
285
454
 
286
- export { AuthorizationErrorImpl, type BuiltinRule, BuiltinRuleObject, DEFAULT_MESSAGES, FormRequest, type InferRuleType, type InferValidatedType, InvalidRuleParameterError, MessageBagImpl, type ParsedRule, PasswordRule, Rule, type RuleContext, UnknownValidationRuleError, ValidationErrorImpl, ValidationException, ValidationFactoryImpl, ValidationServiceProvider, Validator, ValidatorImpl, applyReplacements, builtinRules, defaultAttributeName, expandWildcardKeys, isEmptyValue, isUploadedFileLike, parseRuleSpec };
455
+ export { AuthorizationErrorImpl, type BuiltinRule, BuiltinRuleObject, DEFAULT_MESSAGES, type ExpandingRule, FormRequest, InvalidRuleParameterError, MessageBagImpl, type ParsedRule, PasswordRule, Rule, type RuleContext, UnknownValidationRuleError, ValidationErrorImpl, ValidationException, ValidationFactoryImpl, ValidationServiceProvider, Validator, ValidatorImpl, applyReplacements, builtinRules, defaultAttributeName, expandWildcardKeys, isEmptyValue, isUploadedFileLike, parseRuleSpec };