Skip to content

Commit

Permalink
feat(validation): change assert options interface
Browse files Browse the repository at this point in the history
  • Loading branch information
TomokiMiyauci committed May 27, 2023
1 parent 950fb27 commit 5c15def
Show file tree
Hide file tree
Showing 4 changed files with 284 additions and 42 deletions.
218 changes: 218 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,224 @@ Throws an error in the following cases:

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

## assert

```ts
import {
assert,
number,
object,
string,
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
import {
assertEquals,
assertIsError,
} from "https://deno.land/std/testing/asserts.ts";

const Profile = object({ name: string, age: number });

try {
assert(Profile, { name: null, age: null });
} catch (e) {
assertIsError(e, AggregateError);
assertEquals(e.errors.length, 2);
}
```

### validation

Validation error configs.

#### error

Error constructor.

The default is `ValidationError`.

The example of specify validation error as:

```ts
import { assert, between } from "https://deno.land/x/abstruct@$VERSION/mod.ts";

assert(between(0, 255), 256, {
failFast: true,
validation: { error: RangeError },
});
```

#### message

Error message.

```ts
import {
assert,
type Validator,
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

declare const validator: Validator<unknown>;
declare const input: unknown;
declare const message: string;

try {
assert(validator, input, { failFast: true, validation: { message } });
} catch (e) {
assertEquals(e.message, message);
}
```

#### cause

Original cause of the error.

```ts
import {
assert,
type Validator,
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

declare const validator: Validator<unknown>;
declare const input: unknown;
declare const cause: ErrorConstructor;

try {
assert(validator, input, { failFast: true, validation: { cause } });
} catch (e) {
assertEquals(e.cause, cause);
}
```

### Lazy vs Greedy

Validation by assert works with lazy or greedy.

Lazy terminates the evaluation as soon as it finds a validation error and
reports only one validation error.

In contrast, greedy continues validation until the specified number of
validation errors is reached or all validations are completed.

Also, validator has a lazy evaluation mechanism, so only as many validations are
performed as needed.

By default, it operates as greedy.

### failFast

If `failFast` is true, it works as lazy.

```ts
import {
assert,
ValidationError,
type Validator,
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
import {
assertEquals,
assertIsError,
} from "https://deno.land/std/testing/asserts.ts";

declare const Profile: Validator<
{ name: unknown; age: unknown },
{ name: string; age: string }
>;

try {
assert(Profile, { name: null, age: null }, { failFast: true });
} catch (e) {
assertIsError(e, ValidationError);
}
```

The following fields can only be specified in greedy mode.

### maxErrors option

The number of validation errors can be changed with `maxErrors`. For details,
see [maxErrors](#maxerrors)

### aggregation

Aggregation error configs.

#### error

Specify custom `AggregationErrorConstructor`.

The default is `AggregationError`.

```ts
import {
assert,
type Validator,
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
import { assertIsError } from "https://deno.land/std/testing/asserts.ts";

declare const validator: Validator<unknown>;
declare const input: unknown;
declare const error: AggregateErrorConstructor;

try {
assert(validator, input, { aggregation: { error } });
} catch (e) {
assertIsError(e, error);
}
```

#### message

Customize `AggregationError` message.

```ts
import {
assert,
type Validator,
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

declare const validator: Validator<unknown>;
declare const input: unknown;
declare const message: string;

try {
assert(validator, input, { aggregation: { message } });
} catch (e) {
assertEquals(e.message, message);
}
```

#### cause

You can specify `cause` to express the cause of the `AggregationError`.

```ts
import {
assert,
type Validator,
} from "https://deno.land/x/abstruct@$VERSION/mod.ts";
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

declare const validator: Validator<unknown>;
declare const input: unknown;
declare const cause: Error;

try {
assert(validator, input, { aggregation: { cause } });
} catch (e) {
assertEquals(e.cause, cause);
}
```

### Throwing error

Throws an error in the following cases:

- `AggregateError`: If assertion is fail.
- `ValidationError`: If assertion is fail and [failFast](#failfast) is true.
- Same as [validate](#throwing-error).

## License

Copyright © 2023-present [Tomoki Miyauci](https://github.com/TomokiMiyauci).
Expand Down
2 changes: 1 addition & 1 deletion mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
export {
assert,
type AssertOptions,
type EagerAssertOptions,
Err,
type GreedyAssertOptions,
type LazyAssertOptions,
Ok,
type Result,
Expand Down
89 changes: 55 additions & 34 deletions validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,24 @@ import { isNotEmpty } from "./deps.ts";
import { type ValidationFailure, Validator } from "./types.ts";
import { take } from "./iter_utils.ts";

/** Assert options. */
export interface AssertOptions extends ErrorOptions {
interface ErrorConfigs extends ErrorOptions {
/** Error constructor. */
error?: NewableFunction;

/** Error message. */
/** Error message. */
message?: string;

/** Whether to perform the assertion fail fast or not.
* @default false
*/
failFast?: boolean;
}

/** Eager assert options. */
export interface EagerAssertOptions extends AssertOptions {
/**
/** Validation error configs. */
interface ValidationConfigs extends ErrorConfigs {
/** Validation error constructor.
* @default ValidationError
*/
error?: { new (message?: string, options?: ErrorOptions): Error };
failFast: true;
}

/** Lazy assert options. */
export interface LazyAssertOptions extends AssertOptions, ValidateOptions {
/**
interface AggregationConfigs extends ErrorConfigs {
/** Aggregation error constructor.
* @default AggregateError
*/
error?: {
Expand All @@ -41,6 +33,31 @@ export interface LazyAssertOptions extends AssertOptions, ValidateOptions {
options?: ErrorOptions,
): Error;
};
}

/** Assert options. */
export interface AssertOptions {
/** Validation error configs. */
validation?: ValidationConfigs;

/** Whether to perform the assertion fail fast or not.
* @default false
*/
failFast?: boolean;
}

/** Lazy assert options. */
export interface LazyAssertOptions extends AssertOptions {
failFast: true;
}

/** Aggregation error configs. */

/** Greedy assert options. */
export interface GreedyAssertOptions extends AssertOptions, ValidateOptions {
/** Aggregation error configs. */
aggregation?: AggregationConfigs;

failFast?: false;
}

Expand All @@ -52,40 +69,44 @@ export interface LazyAssertOptions extends AssertOptions, ValidateOptions {
*/
export function assert<In = unknown, A extends In = In>(
validator: Readonly<Validator<In, A>>,
input: Readonly<In>,
options: Readonly<EagerAssertOptions | LazyAssertOptions> = {},
input: In,
options: Readonly<LazyAssertOptions | GreedyAssertOptions> = {},
): asserts input is A {
const {
message,
cause,
failFast,
error,
validation = {},
} = options;
const maxErrors = failFast ? 1 : options.maxErrors;
const result = validate(validator, input, { maxErrors });

if (result.isOk()) return;

const ErrorCtor = validation.error ?? ValidationError;

if (failFast) {
const failure = result.value[0];
const ErrorCtor = error ?? ValidationError;
const e = new ErrorCtor(message ?? makeMsg(failure), {
cause,
instancePath: failure.instancePath,
});
const e = failure2Error(failure);

throw captured(e);
}

const errors = result.value
.map((failure) =>
new ValidationError(makeMsg(failure), {
instancePath: failure.instancePath,
})
).map(captured);
const ErrorCtor = error ?? AggregateError;
const errors = result.value.map(failure2Error).map(captured);
const { aggregation = {} } = options;
const ErrorsCtor = aggregation.error ?? AggregateError;
const e = new ErrorsCtor(errors, aggregation.message, {
cause: aggregation.cause,
});

throw captured(new ErrorCtor(errors, message, { cause }));
throw captured(e);

function failure2Error(
{ message, instancePath }: Readonly<ValidationFailure>,
): Error {
message = message || (validation.message ?? "");
const msg = makeMsg({ message, instancePath });

return new ErrorCtor(msg, { cause: validation.cause, instancePath });
}

// deno-lint-ignore ban-types
function captured<T extends Object>(error: T): T {
Expand Down Expand Up @@ -181,7 +202,7 @@ export interface ValidateOptions {
*/
export function validate<In = unknown, A extends In = In>(
validator: Readonly<Validator<In, A>>,
input: Readonly<In>,
input: In,
options: Readonly<ValidateOptions> = {},
): Result<A, [ValidationFailure, ...ValidationFailure[]]> {
const failures = [...take(validator.validate(input), options.maxErrors)];
Expand Down
Loading

0 comments on commit 5c15def

Please sign in to comment.