Skip to content

Commit 5c15def

Browse files
committed
feat(validation): change assert options interface
1 parent 950fb27 commit 5c15def

4 files changed

Lines changed: 284 additions & 42 deletions

File tree

README.md

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,224 @@ Throws an error in the following cases:
178178

179179
- `RangeError`: If [maxErrors](#maxerrors) is not positive integer.
180180

181+
## assert
182+
183+
```ts
184+
import {
185+
assert,
186+
number,
187+
object,
188+
string,
189+
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
190+
import {
191+
assertEquals,
192+
assertIsError,
193+
} from "https://deno.land/std/testing/asserts.ts";
194+
195+
const Profile = object({ name: string, age: number });
196+
197+
try {
198+
assert(Profile, { name: null, age: null });
199+
} catch (e) {
200+
assertIsError(e, AggregateError);
201+
assertEquals(e.errors.length, 2);
202+
}
203+
```
204+
205+
### validation
206+
207+
Validation error configs.
208+
209+
#### error
210+
211+
Error constructor.
212+
213+
The default is `ValidationError`.
214+
215+
The example of specify validation error as:
216+
217+
```ts
218+
import { assert, between } from "https://deno.land/x/abstruct@$VERSION/mod.ts";
219+
220+
assert(between(0, 255), 256, {
221+
failFast: true,
222+
validation: { error: RangeError },
223+
});
224+
```
225+
226+
#### message
227+
228+
Error message.
229+
230+
```ts
231+
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;
240+
241+
try {
242+
assert(validator, input, { failFast: true, validation: { message } });
243+
} catch (e) {
244+
assertEquals(e.message, message);
245+
}
246+
```
247+
248+
#### cause
249+
250+
Original cause of the error.
251+
252+
```ts
253+
import {
254+
assert,
255+
type Validator,
256+
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
257+
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
258+
259+
declare const validator: Validator<unknown>;
260+
declare const input: unknown;
261+
declare const cause: ErrorConstructor;
262+
263+
try {
264+
assert(validator, input, { failFast: true, validation: { cause } });
265+
} catch (e) {
266+
assertEquals(e.cause, cause);
267+
}
268+
```
269+
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
286+
287+
If `failFast` is true, it works as lazy.
288+
289+
```ts
290+
import {
291+
assert,
292+
ValidationError,
293+
type Validator,
294+
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
295+
import {
296+
assertEquals,
297+
assertIsError,
298+
} from "https://deno.land/std/testing/asserts.ts";
299+
300+
declare const Profile: Validator<
301+
{ name: unknown; age: unknown },
302+
{ name: string; age: string }
303+
>;
304+
305+
try {
306+
assert(Profile, { name: null, age: null }, { failFast: true });
307+
} catch (e) {
308+
assertIsError(e, ValidationError);
309+
}
310+
```
311+
312+
The following fields can only be specified in greedy mode.
313+
314+
### maxErrors option
315+
316+
The number of validation errors can be changed with `maxErrors`. For details,
317+
see [maxErrors](#maxerrors)
318+
319+
### aggregation
320+
321+
Aggregation error configs.
322+
323+
#### error
324+
325+
Specify custom `AggregationErrorConstructor`.
326+
327+
The default is `AggregationError`.
328+
329+
```ts
330+
import {
331+
assert,
332+
type Validator,
333+
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
334+
import { assertIsError } from "https://deno.land/std/testing/asserts.ts";
335+
336+
declare const validator: Validator<unknown>;
337+
declare const input: unknown;
338+
declare const error: AggregateErrorConstructor;
339+
340+
try {
341+
assert(validator, input, { aggregation: { error } });
342+
} catch (e) {
343+
assertIsError(e, error);
344+
}
345+
```
346+
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";
357+
358+
declare const validator: Validator<unknown>;
359+
declare const input: unknown;
360+
declare const message: string;
361+
362+
try {
363+
assert(validator, input, { aggregation: { message } });
364+
} catch (e) {
365+
assertEquals(e.message, message);
366+
}
367+
```
368+
369+
#### cause
370+
371+
You can specify `cause` to express the cause of the `AggregationError`.
372+
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";
379+
380+
declare const validator: Validator<unknown>;
381+
declare const input: unknown;
382+
declare const cause: Error;
383+
384+
try {
385+
assert(validator, input, { aggregation: { cause } });
386+
} catch (e) {
387+
assertEquals(e.cause, cause);
388+
}
389+
```
390+
391+
### Throwing error
392+
393+
Throws an error in the following cases:
394+
395+
- `AggregateError`: If assertion is fail.
396+
- `ValidationError`: If assertion is fail and [failFast](#failfast) is true.
397+
- Same as [validate](#throwing-error).
398+
181399
## License
182400

183401
Copyright © 2023-present [Tomoki Miyauci](https://github.com/TomokiMiyauci).

mod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
export {
55
assert,
66
type AssertOptions,
7-
type EagerAssertOptions,
87
Err,
8+
type GreedyAssertOptions,
99
type LazyAssertOptions,
1010
Ok,
1111
type Result,

validation.ts

Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,24 @@ import { isNotEmpty } from "./deps.ts";
55
import { type ValidationFailure, Validator } from "./types.ts";
66
import { take } from "./iter_utils.ts";
77

8-
/** Assert options. */
9-
export interface AssertOptions extends ErrorOptions {
8+
interface ErrorConfigs extends ErrorOptions {
109
/** Error constructor. */
1110
error?: NewableFunction;
1211

13-
/** Error message. */
12+
/** Error message. */
1413
message?: string;
15-
16-
/** Whether to perform the assertion fail fast or not.
17-
* @default false
18-
*/
19-
failFast?: boolean;
2014
}
2115

22-
/** Eager assert options. */
23-
export interface EagerAssertOptions extends AssertOptions {
24-
/**
16+
/** Validation error configs. */
17+
interface ValidationConfigs extends ErrorConfigs {
18+
/** Validation error constructor.
2519
* @default ValidationError
2620
*/
2721
error?: { new (message?: string, options?: ErrorOptions): Error };
28-
failFast: true;
2922
}
3023

31-
/** Lazy assert options. */
32-
export interface LazyAssertOptions extends AssertOptions, ValidateOptions {
33-
/**
24+
interface AggregationConfigs extends ErrorConfigs {
25+
/** Aggregation error constructor.
3426
* @default AggregateError
3527
*/
3628
error?: {
@@ -41,6 +33,31 @@ export interface LazyAssertOptions extends AssertOptions, ValidateOptions {
4133
options?: ErrorOptions,
4234
): Error;
4335
};
36+
}
37+
38+
/** Assert options. */
39+
export interface AssertOptions {
40+
/** Validation error configs. */
41+
validation?: ValidationConfigs;
42+
43+
/** Whether to perform the assertion fail fast or not.
44+
* @default false
45+
*/
46+
failFast?: boolean;
47+
}
48+
49+
/** Lazy assert options. */
50+
export interface LazyAssertOptions extends AssertOptions {
51+
failFast: true;
52+
}
53+
54+
/** Aggregation error configs. */
55+
56+
/** Greedy assert options. */
57+
export interface GreedyAssertOptions extends AssertOptions, ValidateOptions {
58+
/** Aggregation error configs. */
59+
aggregation?: AggregationConfigs;
60+
4461
failFast?: false;
4562
}
4663

@@ -52,40 +69,44 @@ export interface LazyAssertOptions extends AssertOptions, ValidateOptions {
5269
*/
5370
export function assert<In = unknown, A extends In = In>(
5471
validator: Readonly<Validator<In, A>>,
55-
input: Readonly<In>,
56-
options: Readonly<EagerAssertOptions | LazyAssertOptions> = {},
72+
input: In,
73+
options: Readonly<LazyAssertOptions | GreedyAssertOptions> = {},
5774
): asserts input is A {
5875
const {
59-
message,
60-
cause,
6176
failFast,
62-
error,
77+
validation = {},
6378
} = options;
6479
const maxErrors = failFast ? 1 : options.maxErrors;
6580
const result = validate(validator, input, { maxErrors });
6681

6782
if (result.isOk()) return;
6883

84+
const ErrorCtor = validation.error ?? ValidationError;
85+
6986
if (failFast) {
7087
const failure = result.value[0];
71-
const ErrorCtor = error ?? ValidationError;
72-
const e = new ErrorCtor(message ?? makeMsg(failure), {
73-
cause,
74-
instancePath: failure.instancePath,
75-
});
88+
const e = failure2Error(failure);
7689

7790
throw captured(e);
7891
}
7992

80-
const errors = result.value
81-
.map((failure) =>
82-
new ValidationError(makeMsg(failure), {
83-
instancePath: failure.instancePath,
84-
})
85-
).map(captured);
86-
const ErrorCtor = error ?? AggregateError;
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+
});
8799

88-
throw captured(new ErrorCtor(errors, message, { cause }));
100+
throw captured(e);
101+
102+
function failure2Error(
103+
{ message, instancePath }: Readonly<ValidationFailure>,
104+
): Error {
105+
message = message || (validation.message ?? "");
106+
const msg = makeMsg({ message, instancePath });
107+
108+
return new ErrorCtor(msg, { cause: validation.cause, instancePath });
109+
}
89110

90111
// deno-lint-ignore ban-types
91112
function captured<T extends Object>(error: T): T {
@@ -181,7 +202,7 @@ export interface ValidateOptions {
181202
*/
182203
export function validate<In = unknown, A extends In = In>(
183204
validator: Readonly<Validator<In, A>>,
184-
input: Readonly<In>,
205+
input: In,
185206
options: Readonly<ValidateOptions> = {},
186207
): Result<A, [ValidationFailure, ...ValidationFailure[]]> {
187208
const failures = [...take(validator.validate(input), options.maxErrors)];

0 commit comments

Comments
 (0)