@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/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
 
@@ -11,6 +12,16 @@ declare class ValidationErrorImpl extends Error implements ValidationError {
11
12
  readonly name = "ValidationError";
12
13
  constructor(errors: Record<string, string[]>, errorBag?: string, redirectTo?: string | undefined);
13
14
  }
15
+ /**
16
+ * Laravel's ValidationException, for failing a request by hand:
17
+ * `throw ValidationException.withMessages({ email: __('auth.failed') })`. Rendered like any validation error: 422 JSON,
18
+ * or a redirect back with the errors.
19
+ */
20
+ declare class ValidationException extends ValidationErrorImpl {
21
+ /** Messages per field, each a string or a list. */
22
+ static withMessages(messages: Record<string, string | readonly string[]>): ValidationException;
23
+ constructor(errors: Record<string, string[]>, errorBag?: string, redirectTo?: string);
24
+ }
14
25
  declare class AuthorizationErrorImpl extends Error implements OrielError {
15
26
  readonly kind: "Authorization";
16
27
  readonly status = 403;
@@ -31,34 +42,38 @@ declare class InvalidRuleParameterError extends Error implements OrielError {
31
42
  constructor(rule: string, parameter: string, reason: string);
32
43
  }
33
44
 
45
+ /** The data a FormRequest's validate() returns, typed from its rules(). */
46
+ type ValidatedData<F extends FormRequest> = InferValidatedType<Awaited<ReturnType<F['rules']>>>;
34
47
  /**
35
48
  * Mirrors Illuminate\Foundation\Http\FormRequest, minus dependency injection by type (Oriel resolves the current
36
49
  * request and the ValidationFactory through inject(), never through constructor parameters — see spec §3 rule 5).
37
50
  * A subclass only has to implement rules(); authorize()/messages()/attributes() default to Laravel's own defaults.
38
51
  */
39
52
  declare abstract class FormRequest {
40
- 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>;
41
55
  authorize(): boolean | Promise<boolean>;
42
56
  messages(): Record<string, string>;
43
57
  attributes(): Record<string, string>;
44
58
  /**
45
59
  * `const data = await StorePostRequest.validate()` (spec §4.3). Resolves the current request and the
46
60
  * ValidationFactory from the active container, runs authorize() first (throwing AuthorizationErrorImpl if it
47
- * returns false), then validates request.all() against rules()/messages()/attributes() and returns the typed
48
- * 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>`.
49
65
  */
50
- static validate<T = Record<string, unknown>>(this: new () => FormRequest): Promise<T>;
66
+ static validate<F extends FormRequest>(this: new () => F): Promise<ValidatedData<F>>;
51
67
  }
52
68
 
53
- /** 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). */
54
70
  declare function defaultAttributeName(key: string): string;
55
71
  declare function applyReplacements(template: string, replacements: Record<string, string>): string;
56
- /** 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. */
57
73
  declare const DEFAULT_MESSAGES: Record<string, string>;
58
74
 
59
75
  declare class MessageBagImpl implements MessageBag {
60
76
  private readonly items;
61
- /** Not part of the MessageBag contract — used internally by ValidatorImpl and by after() hooks. */
62
77
  add(key: string, message: string): void;
63
78
  has(key: string): boolean;
64
79
  first(key?: string): string | undefined;
@@ -80,6 +95,17 @@ interface RuleContext {
80
95
  data: Record<string, unknown>;
81
96
  /** Every named rule attached to this field (for size-type resolution: numeric/array/file/string). */
82
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;
83
109
  }
84
110
  interface BuiltinRule {
85
111
  validate(ctx: RuleContext): boolean | Promise<boolean>;
@@ -89,8 +115,15 @@ interface BuiltinRule {
89
115
  replacements?(ctx: RuleContext): Record<string, string>;
90
116
  /** Used when messageKey() isn't in DEFAULT_MESSAGES (e.g. an extend()ed rule with a custom message). */
91
117
  defaultMessage?: string;
118
+ /** An extend()ed rule's own message: it wins over the default lines, as Laravel's fallback messages do. */
119
+ message?: string;
92
120
  /** Runs even when the value is missing, blank, or null under nullable (the required* family, and accepted); a failure stops the field. */
93
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;
94
127
  }
95
128
  type ParsedRule = {
96
129
  kind: 'named';
@@ -100,8 +133,18 @@ type ParsedRule = {
100
133
  kind: 'object';
101
134
  rule: ValidationRule;
102
135
  };
103
- declare function parseRuleSpec(spec: string | ReadonlyArray<string | ValidationRule>): ParsedRule[];
104
- /** 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
+ */
105
148
  declare function isEmptyValue(value: unknown): boolean;
106
149
  /** Duck-types Contracts/Http's UploadedFile without importing @orieljs/http. */
107
150
  declare function isUploadedFileLike(value: unknown): value is {
@@ -114,8 +157,8 @@ declare function isUploadedFileLike(value: unknown): value is {
114
157
  /**
115
158
  * Expands a `*`-wildcard rule pattern (e.g. "items.*.name") into every concrete path present in `data`
116
159
  * (e.g. ["items.0.name", "items.1.name"]). A pattern with no `*` is returned unchanged. A `*` segment whose
117
- * container is missing, not an array and not a plain object expands to nothing for that branch (Review Focus #4:
118
- * 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
119
162
  * treated as absent, so a JSON body like {"items":{"__proto__":{...}}} never yields a path through the prototype.
120
163
  */
121
164
  declare function expandWildcardKeys(pattern: string, data: Record<string, unknown>): string[];
@@ -123,22 +166,162 @@ declare function expandWildcardKeys(pattern: string, data: Record<string, unknow
123
166
  /** Every built-in rule, merged. Later tasks (database rules) add their own spread here. */
124
167
  declare const builtinRules: Record<string, BuiltinRule>;
125
168
 
169
+ /**
170
+ * Laravel's Illuminate\Validation\Rules\Password: a string of at least `min` characters (8 by default), plus the
171
+ * checks you add: letters(), mixedCase(), numbers(), symbols(), max(). Every failing check adds its own message.
172
+ * Rule.password() returns the application default (PasswordRule.defaults()). (Laravel's uncompromised() is not
173
+ * ported: it calls an outside service.)
174
+ */
175
+ declare class PasswordRule implements TypedValidationRule<{
176
+ value: string;
177
+ }> {
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
+ };
183
+ constructor(min?: number);
184
+ /** A password rule of at least `size` characters: PasswordRule.min(12).mixedCase(). */
185
+ static min(size: number): PasswordRule;
186
+ /**
187
+ * With an argument, sets the default (Laravel's Password::defaults(fn () => ...), in a provider's boot());
188
+ * `null` resets it. Without one, returns a copy of the default: PasswordRule.min(8) unless set.
189
+ */
190
+ static defaults(): PasswordRule;
191
+ static defaults(rule: PasswordRule | (() => PasswordRule) | null): void;
192
+ min(size: number): this;
193
+ max(size: number): this;
194
+ /** At least one letter. */
195
+ letters(): this;
196
+ /** At least one uppercase and one lowercase letter. */
197
+ mixedCase(): this;
198
+ /** At least one number. */
199
+ numbers(): this;
200
+ /** At least one symbol (punctuation, a symbol or a space). */
201
+ symbols(): this;
202
+ validate(_attribute: string, value: unknown, fail: (message: string) => void): void;
203
+ }
204
+
126
205
  /**
127
206
  * Base for the fluent builders. Each one reports under a built-in rule name (`in`, `unique`, `exists`), so when it
128
207
  * runs inside a Validator its failure goes through exactly the same lookup as the string rule: custom messages
129
208
  * ("email.unique", "items.*.email.unique", "unique"), custom attributes, then the default text. Called directly
130
209
  * as a plain ValidationRule, validate() fails with the default English message.
131
210
  */
132
- declare abstract class BuiltinRuleObject implements ValidationRule {
211
+ declare abstract class BuiltinRuleObject<T extends RuleTyping = Record<never, never>> implements TypedValidationRule<T> {
133
212
  abstract readonly ruleName: string;
134
- 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[];
135
219
  validate(attribute: string, value: unknown, fail: (message: string) => void, data: Record<string, unknown>, expandedFromWildcard?: boolean): Promise<void>;
136
220
  }
137
- 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>> {
138
231
  private readonly values;
139
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";
140
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[]);
141
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;
142
325
  }
143
326
  declare class UniqueRule extends BuiltinRuleObject {
144
327
  private readonly table;
@@ -166,30 +349,36 @@ declare class ExistsRule extends BuiltinRuleObject {
166
349
  }
167
350
  /** Fluent rule-object builders, for when a plain string rule can't express what's needed (e.g. .ignore()). */
168
351
  declare const Rule: {
169
- 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;
170
378
  unique: (table: string, column?: string) => UniqueRule;
171
379
  exists: (table: string, column?: string) => ExistsRule;
172
- };
173
-
174
- /** See Task 9's writeup in the plan for exactly what this does and doesn't infer. */
175
- type SplitPipe<S extends string> = S extends `${infer Head}|${infer Tail}` ? Head | SplitPipe<Tail> : S;
176
- type RuleName<T extends string> = T extends `${infer Name}:${string}` ? Name : T;
177
- type Names<S extends string> = RuleName<SplitPipe<S>>;
178
- 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;
179
- /**
180
- * The inferred type for one rule string, e.g. 'nullable|integer' -> number | null. A non-literal `string` (a rules
181
- * object declared without `as const`) carries no rule names to read, so it infers unknown: without this guard every
182
- * `'x' extends string` check would match and the result would wrongly be `unknown[] | null`.
183
- */
184
- type InferRuleType<S extends string> = string extends S ? unknown : 'nullable' extends Names<S> ? BaseType<S> | null : BaseType<S>;
185
- /**
186
- * Best-effort: applies InferRuleType to every string-valued rule; array-form (mixed string/object) rules -> unknown.
187
- * Strips `readonly` (via `-readonly`) so a `Rules` object declared `as const` (the common way to get literal rule
188
- * strings for inference — see the Task 9 type test) still produces a plain, mutable validated-data shape, not a
189
- * readonly one; a mapped type otherwise carries the source's readonly modifier over by default.
190
- */
191
- type InferValidatedType<R> = {
192
- -readonly [K in keyof R]: R[K] extends string ? InferRuleType<R[K]> : unknown;
380
+ /** Laravel's Password::defaults(): the application's password rule (at least 8 characters unless PasswordRule.defaults() changed it). */
381
+ password: () => PasswordRule;
193
382
  };
194
383
 
195
384
  /**
@@ -201,7 +390,7 @@ type InferValidatedType<R> = {
201
390
  * - `sometimes` skips a missing field entirely;
202
391
  * - a field stops validating after its first failure under `bail`, or after any implicit rule fails.
203
392
  */
204
- declare class ValidatorImpl implements Validator$1 {
393
+ declare class ValidatorImpl<R extends Rules = Rules> implements Validator$1<R> {
205
394
  private readonly data;
206
395
  private readonly rules;
207
396
  private readonly customMessages;
@@ -211,16 +400,40 @@ declare class ValidatorImpl implements Validator$1 {
211
400
  private bag;
212
401
  private ranPromise;
213
402
  private readonly afterCallbacks;
214
- 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);
215
411
  after(callback: (validator: Validator$1) => void | Promise<void>): this;
216
- /** 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. */
217
413
  fail(field: string, message: string): void;
218
414
  passes(): Promise<boolean>;
219
415
  fails(): Promise<boolean>;
220
416
  errors(): MessageBagImpl;
221
- 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;
222
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;
223
435
  private run;
436
+ private passesExcludeRule;
224
437
  private runRule;
225
438
  /** Custom message (concrete field, then wildcard pattern, then rule name) or the default, with :attribute filled. */
226
439
  private failureMessage;
@@ -228,7 +441,9 @@ declare class ValidatorImpl implements Validator$1 {
228
441
 
229
442
  declare class ValidationFactoryImpl implements ValidationFactory {
230
443
  private readonly extended;
231
- 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>;
445
+ /** Every rule this factory knows, built-in and extend()ed, sorted (for editors: `oriel ide:info`). */
446
+ ruleNames(): string[];
232
447
  extend(name: string, callback: RuleCallback, message?: string): void;
233
448
  }
234
449
 
@@ -239,4 +454,4 @@ declare class ValidationServiceProvider extends ServiceProvider {
239
454
  register(): void;
240
455
  }
241
456
 
242
- export { AuthorizationErrorImpl, type BuiltinRule, BuiltinRuleObject, DEFAULT_MESSAGES, FormRequest, type InferRuleType, type InferValidatedType, InvalidRuleParameterError, MessageBagImpl, type ParsedRule, Rule, type RuleContext, UnknownValidationRuleError, ValidationErrorImpl, ValidationFactoryImpl, ValidationServiceProvider, Validator, ValidatorImpl, applyReplacements, builtinRules, defaultAttributeName, expandWildcardKeys, isEmptyValue, isUploadedFileLike, parseRuleSpec };
457
+ 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 };