Skip to content

Commit 09b8aae

Browse files
committed
feat(validation): change default assert strategy
to fail fast
1 parent 228bdf4 commit 09b8aae

3 files changed

Lines changed: 296 additions & 300 deletions

File tree

README.md

Lines changed: 79 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,15 @@ Throws an error in the following cases:
180180

181181
## assert
182182

183+
Assert that the input passes validator.
184+
183185
```ts
184186
import {
185187
assert,
186188
number,
187189
object,
188190
string,
191+
ValidationError,
189192
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
190193
import {
191194
assertEquals,
@@ -197,94 +200,85 @@ const Profile = object({ name: string, age: number });
197200
try {
198201
assert(Profile, { name: null, age: null });
199202
} catch (e) {
200-
assertIsError(e, AggregateError);
201-
assertEquals(e.errors.length, 2);
203+
assertIsError(e, ValidationError, "<string validation message>");
202204
}
203205
```
204206

205-
### validation
207+
The default behavior of `assert` is fail fast. That is, it stops working as soon
208+
as it finds a validation failure.
206209

207-
Validation error configs.
210+
`assert` provides flexible options, allowing the following customizations.
208211

209-
#### error
212+
- Error instance
213+
- Fail slow mode
214+
- Limit number of failures
210215

211-
Error constructor.
216+
### Validation error
212217

213-
The default is `ValidationError`.
218+
There is a setting for validation error under the `validation` property.
214219

215-
The example of specify validation error as:
220+
The following elements of this instance can be customized:
216221

217-
```ts
218-
import { assert, between } from "https://deno.land/x/abstruct@$VERSION/mod.ts";
222+
- Constructor
223+
- Error message
224+
- Error by cause
219225

220-
assert(between(0, 255), 256, {
221-
failFast: true,
222-
validation: { error: RangeError },
223-
});
224-
```
226+
#### Validation error constructor
225227

226-
#### message
228+
The default constructor is `ValidationError`.
227229

228-
Error message.
230+
You may want to throw a web standard error. In this case, you can change the
231+
constructor in the `error` field.
229232

230233
```ts
234+
import { assert, between } from "https://deno.land/x/abstruct@$VERSION/mod.ts";
231235
import {
232-
assert,
233-
type Validator,
234-
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
235-
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
236-
237-
declare const validator: Validator<unknown>;
238-
declare const input: unknown;
239-
declare const message: string;
236+
assertEquals,
237+
assertIsError,
238+
} from "https://deno.land/std/testing/asserts.ts";
240239

241240
try {
242-
assert(validator, input, { failFast: true, validation: { message } });
241+
assert(between(0, 255), 256, { validation: { error: RangeError } });
243242
} catch (e) {
244-
assertEquals(e.message, message);
243+
assertIsError(e, RangeError);
245244
}
246245
```
247246

248-
#### cause
247+
This would be especially appropriate for libraries.
249248

250-
Original cause of the error.
249+
#### Validation default error message
250+
251+
A default error message can be specified, falling back to the default if the
252+
validator's failure message is empty.
253+
254+
This is usually not necessary since the validator normally reports failure
255+
messages.
251256

252257
```ts
253258
import {
254259
assert,
260+
ValidationError,
255261
type Validator,
256262
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
257-
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
263+
import {
264+
assertEquals,
265+
assertIsError,
266+
} from "https://deno.land/std/testing/asserts.ts";
258267

259-
declare const validator: Validator<unknown>;
268+
declare const emptyMsgValidator: Validator;
260269
declare const input: unknown;
261-
declare const cause: ErrorConstructor;
270+
declare const defaultMsg: string;
262271

263272
try {
264-
assert(validator, input, { failFast: true, validation: { cause } });
273+
assert(emptyMsgValidator, input, { validation: { message: defaultMsg } });
265274
} catch (e) {
266-
assertEquals(e.cause, cause);
275+
assertIsError(e, ValidationError, defaultMsg);
267276
}
268277
```
269278

270-
### Lazy vs Greedy
271-
272-
Validation by assert works with lazy or greedy.
273-
274-
Lazy terminates the evaluation as soon as it finds a validation error and
275-
reports only one validation error.
276-
277-
In contrast, greedy continues validation until the specified number of
278-
validation errors is reached or all validations are completed.
279-
280-
Also, validator has a lazy evaluation mechanism, so only as many validations are
281-
performed as needed.
282-
283-
By default, it operates as greedy.
284-
285-
### failFast
279+
#### Error by cause
286280

287-
If `failFast` is true, it works as lazy.
281+
Original cause of the error.
288282

289283
```ts
290284
import {
@@ -297,103 +291,69 @@ import {
297291
assertIsError,
298292
} from "https://deno.land/std/testing/asserts.ts";
299293

300-
declare const Profile: Validator<
301-
{ name: unknown; age: unknown },
302-
{ name: string; age: string }
303-
>;
294+
declare const validator: Validator;
295+
declare const input: unknown;
296+
declare const cause: unknown;
304297

305-
try {
306-
assert(Profile, { name: null, age: null }, { failFast: true });
307-
} catch (e) {
308-
assertIsError(e, ValidationError);
309-
}
298+
assert(validator, input, { validation: { cause } });
310299
```
311300

312-
The following fields can only be specified in greedy mode.
313-
314-
### maxFailures option
301+
### Fail slow
315302

316-
The number of validation failures can be changed with `maxFailures`. For
317-
details, see [maxFailures](#maxfailures)
303+
To execute multiple validations, specify `failSlow`. This delays throwing errors
304+
until the specified number of failures is reached.
318305

319-
### aggregation
320-
321-
Aggregation error configs.
322-
323-
#### error
324-
325-
Specify custom `AggregationErrorConstructor`.
326-
327-
The default is `AggregationError`.
306+
The error instance will then be `AggregateError` since multiple failures may be
307+
found.
328308

329309
```ts
330310
import {
331311
assert,
332-
type Validator,
312+
number,
313+
object,
314+
string,
333315
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
334-
import { assertIsError } from "https://deno.land/std/testing/asserts.ts";
316+
import {
317+
assertEquals,
318+
assertIsError,
319+
} from "https://deno.land/std/testing/asserts.ts";
335320

336-
declare const validator: Validator<unknown>;
337-
declare const input: unknown;
338-
declare const error: AggregateErrorConstructor;
321+
const Profile = object({ name: string, age: number });
339322

340323
try {
341-
assert(validator, input, { aggregation: { error } });
324+
assert(Profile, { name: null, age: null }, { failSlow: true });
342325
} catch (e) {
343-
assertIsError(e, error);
326+
assertIsError(e, AggregateError);
327+
assertEquals(e.errors.length, 2);
344328
}
345329
```
346330

347-
#### message
348-
349-
Customize `AggregationError` message.
350-
351-
```ts
352-
import {
353-
assert,
354-
type Validator,
355-
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
356-
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
331+
#### maxFailures option
357332

358-
declare const validator: Validator<unknown>;
359-
declare const input: unknown;
360-
declare const message: string;
333+
The number of validation failures can be changed with `maxFailures`. For
334+
details, see [maxFailures](#maxfailures)
361335

362-
try {
363-
assert(validator, input, { aggregation: { message } });
364-
} catch (e) {
365-
assertEquals(e.message, message);
366-
}
367-
```
336+
#### aggregation
368337

369-
#### cause
338+
There is a setting for `AggregateError` under the `aggregation` property.
370339

371-
You can specify `cause` to express the cause of the `AggregationError`.
340+
The following element can be customized as
341+
[Validation error](#validation-error).
372342

373-
```ts
374-
import {
375-
assert,
376-
type Validator,
377-
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
378-
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
343+
- [error](#validation-error-constructor)
344+
- [message](#validation-default-error-message)
345+
- [cause](#error-by-cause)
379346

380-
declare const validator: Validator<unknown>;
381-
declare const input: unknown;
382-
declare const cause: Error;
347+
The `message` in `AggregateError` is empty by default.
383348

384-
try {
385-
assert(validator, input, { aggregation: { cause } });
386-
} catch (e) {
387-
assertEquals(e.cause, cause);
388-
}
389-
```
349+
In fail slow mode, it is recommended to provide it.
390350

391351
### Throwing error
392352

393353
Throws an error in the following cases:
394354

395-
- `AggregateError`: If assertion is fail.
396-
- `ValidationError`: If assertion is fail and [failFast](#failfast) is true.
355+
- `ValidationError`: If assertion is fail.
356+
- `AggregateError`: If assertion is fail and [failSlow](#fail-slow) is `true`.
397357
- Same as [validate](#throwing-error).
398358

399359
## Factories

validation.ts

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,17 @@ export interface AssertOptions {
4040
/** Validation error configs. */
4141
validation?: ValidationConfigs;
4242

43-
/** Whether to perform the assertion fail fast or not.
43+
/** Whether to perform the assertion fail slow or not.
44+
* - `true`: fail fast
45+
* - `false`: fail slow
4446
* @default false
4547
*/
46-
failFast?: boolean;
48+
failSlow?: boolean;
4749
}
4850

4951
/** Lazy assert options. */
5052
export interface LazyAssertOptions extends AssertOptions {
51-
failFast: true;
53+
failSlow?: false;
5254
}
5355

5456
/** Aggregation error configs. */
@@ -58,10 +60,33 @@ export interface GreedyAssertOptions extends AssertOptions, ValidateOptions {
5860
/** Aggregation error configs. */
5961
aggregation?: AggregationConfigs;
6062

61-
failFast?: false;
63+
failSlow: true;
6264
}
6365

64-
/** Assert with validator.
66+
/** Assert that the input passes validator.
67+
*
68+
* @example
69+
* ```ts
70+
* import {
71+
* assert,
72+
* number,
73+
* object,
74+
* string,
75+
* ValidationError,
76+
* } from "https://deno.land/x/abstruct@$VERSION/mod.ts";
77+
* import {
78+
* assertEquals,
79+
* assertIsError,
80+
* } from "https://deno.land/std/testing/asserts.ts";
81+
*
82+
* const Profile = object({ name: string, age: number });
83+
*
84+
* try {
85+
* assert(Profile, { name: null, age: null });
86+
* } catch (e) {
87+
* assertIsError(e, ValidationError, "<string validation message>");
88+
* }
89+
* ```
6590
*
6691
* @throws {AggregateError} If assertion is fail.
6792
* @throws {ValidationError} If assertion is fail and {@link options.failFast} is true.
@@ -73,29 +98,29 @@ export function assert<In = unknown, A extends In = In>(
7398
options: Readonly<LazyAssertOptions | GreedyAssertOptions> = {},
7499
): asserts input is A {
75100
const {
76-
failFast,
101+
failSlow,
77102
validation = {},
78103
} = options;
79-
const maxFailures = failFast ? 1 : options.maxFailures;
104+
const maxFailures = failSlow ? options.maxFailures : 1;
80105
const result = validate(validator, input, { maxFailures });
81106

82107
if (result.isOk()) return;
83108

84-
const ErrorCtor = validation.error ?? ValidationError;
109+
const { error: VError = ValidationError } = validation;
85110

86-
if (failFast) {
87-
const failure = result.value[0];
88-
const e = failure2Error(failure);
111+
if (failSlow) {
112+
const errors = result.value.map(failure2Error).map(captured);
113+
const { aggregation = {} } = options;
114+
const ErrorsCtor = aggregation.error ?? AggregateError;
115+
const e = new ErrorsCtor(errors, aggregation.message, {
116+
cause: aggregation.cause,
117+
});
89118

90119
throw captured(e);
91120
}
92121

93-
const errors = result.value.map(failure2Error).map(captured);
94-
const { aggregation = {} } = options;
95-
const ErrorsCtor = aggregation.error ?? AggregateError;
96-
const e = new ErrorsCtor(errors, aggregation.message, {
97-
cause: aggregation.cause,
98-
});
122+
const failure = result.value[0];
123+
const e = failure2Error(failure);
99124

100125
throw captured(e);
101126

@@ -105,7 +130,7 @@ export function assert<In = unknown, A extends In = In>(
105130
message = message || (validation.message ?? "");
106131
const msg = makeMsg({ message, instancePath });
107132

108-
return new ErrorCtor(msg, { cause: validation.cause, instancePath });
133+
return new VError(msg, { cause: validation.cause, instancePath });
109134
}
110135

111136
// deno-lint-ignore ban-types

0 commit comments

Comments
 (0)