Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: Remove custom error from assert and ensure #130

Open
wants to merge 2 commits into
base: v5-pre
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,22 @@ const a: unknown = "Hello";
// `assert` does nothing or throws an `AssertError`
assert(a, is.String);
// a is now narrowed to string
```

Use [`@core/errorutil/alter`](https://jsr.io/@core/errorutil/doc/alter/~/alter)
to throw a custom error:

```typescript
import { alter } from "@core/errorutil/alter";
import { assert, is } from "@core/unknownutil";

const a: unknown = 0;

// With custom message
assert(a, is.String, { message: "a must be a string" });
// The following throws an Error("a is not a string")
alter(
() => assert(a, is.String),
new Error("a is not a string"),
);
```

### ensure
Expand All @@ -211,9 +224,22 @@ const a: unknown = "Hello";

// `ensure` returns `string` or throws an `AssertError`
const _: string = ensure(a, is.String);
```

Use [`@core/errorutil/alter`](https://jsr.io/@core/errorutil/doc/alter/~/alter)
to throw a custom error:

```typescript
import { alter } from "@core/errorutil/alter";
import { ensure, is } from "@core/unknownutil";

const a: unknown = 0;

// With custom message
const __: string = ensure(a, is.String, { message: "a must be a string" });
// The following throws an Error("a is not a string")
const _: string = alter(
() => ensure(a, is.String),
new Error("a is not a string"),
);
```

### maybe
Expand Down
95 changes: 33 additions & 62 deletions assert.ts
Original file line number Diff line number Diff line change
@@ -1,81 +1,49 @@
import type { Predicate } from "./type.ts";

/**
* A factory function that generates assertion error messages.
*/
export type AssertMessageFactory = (
x: unknown,
pred: Predicate<unknown>,
name?: string,
) => string;

/**
* The default factory function used to generate assertion error messages.
*/
export const defaultAssertMessageFactory: AssertMessageFactory = (
x,
pred,
name,
) => {
const assertMessageFactory = (x: unknown, pred: Predicate<unknown>) => {
const p = pred.name || "anonymous predicate";
const t = typeof x;
const v = JSON.stringify(x, null, 2);
return `Expected ${
name ?? "a value"
} that satisfies the predicate ${p}, got ${t}: ${v}`;
return `Expected a value that satisfies the predicate ${p}, got ${t}: ${v}`;
};

let assertMessageFactory = defaultAssertMessageFactory;

/**
* Represents an error that occurs when an assertion fails.
*/
export class AssertError extends Error {
/**
* Constructs a new instance.
* @param message The error message.
* The value that failed the assertion.
*/
constructor(message?: string) {
super(message);
readonly x: unknown;

if (Error.captureStackTrace) {
Error.captureStackTrace(this, AssertError);
}
/**
* The predicate that the value failed to satisfy.
*/
readonly pred: Predicate<unknown>;

/**
* Constructs a new instance.
*
* @param x The value that failed the assertion.
* @param pred The predicate that the value failed to satisfy.
*/
constructor(x: unknown, pred: Predicate<unknown>) {
super(assertMessageFactory(x, pred));
this.name = this.constructor.name;
this.x = x;
this.pred = pred;
}
}

/**
* Sets the factory function used to generate assertion error messages.
*
* ```ts
* import { is, setAssertMessageFactory } from "@core/unknownutil";
*
* setAssertMessageFactory((x, pred) => {
* if (pred === is.String) {
* return `Expected a string, got ${typeof x}`;
* } else if (pred === is.Number) {
* return `Expected a number, got ${typeof x}`;
* } else if (pred === is.Boolean) {
* return `Expected a boolean, got ${typeof x}`;
* } else {
* return `Expected a value that satisfies the predicate, got ${typeof x}`;
* }
* });
* ```
*
* @param factory The factory function.
*/
export function setAssertMessageFactory(factory: AssertMessageFactory): void {
assertMessageFactory = factory;
}

/**
* Asserts that the given value satisfies the provided predicate.
*
* It throws {@linkcode AssertError} if the value does not satisfy the predicate.
*
* @param x The value to be asserted.
* @param pred The predicate function to test the value against.
* @returns The function has a return type of `asserts x is T` to help TypeScript narrow down the type of `x` after the assertion.
*
* ```ts
* import { assert, is } from "@core/unknownutil";
*
Expand All @@ -84,19 +52,22 @@ export function setAssertMessageFactory(factory: AssertMessageFactory): void {
* // a is now narrowed to string
* ```
*
* @param x The value to be asserted.
* @param pred The predicate function to test the value against.
* @param options Optional configuration for the assertion.
* @returns The function has a return type of `asserts x is T` to help TypeScript narrow down the type of `x` after the assertion.
* Use {@linkcode https://jsr.io/@core/errorutil/doc/alter/~/alter|@core/errorutil/alter.alter} to alter error.
*
* ```ts
* import { alter } from "@core/errorutil/alter";
* import { assert, is } from "@core/unknownutil";
*
* const a: unknown = 42;
* alter(() => assert(a, is.String), new Error("a is not a string"));
* // Error: a is not a string
* ```
*/
export function assert<T>(
x: unknown,
pred: Predicate<T>,
options: { message?: string; name?: string } = {},
): asserts x is T {
if (!pred(x)) {
throw new AssertError(
options.message ?? assertMessageFactory(x, pred, options.name),
);
throw new AssertError(x, pred);
}
}
43 changes: 1 addition & 42 deletions assert_test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { assertThrows } from "@std/assert";
import {
assert,
AssertError,
defaultAssertMessageFactory,
setAssertMessageFactory,
} from "./assert.ts";
import { assert, AssertError } from "./assert.ts";

const x: unknown = Symbol("x");

Expand Down Expand Up @@ -39,40 +34,4 @@ Deno.test("assert", async (t) => {
);
},
);

await t.step(
"throws an `AssertError` on false predicate with a custom name",
() => {
assertThrows(
() => assert(x, falsePredicate, { name: "hello world" }),
AssertError,
`Expected hello world that satisfies the predicate falsePredicate, got symbol: undefined`,
);
},
);

await t.step(
"throws an `AssertError` with a custom message on false predicate",
() => {
assertThrows(
() => assert(x, falsePredicate, { message: "Hello" }),
AssertError,
"Hello",
);
},
);
});

Deno.test("setAssertMessageFactory", async (t) => {
setAssertMessageFactory((x, pred) => `Hello ${typeof x} ${pred.name}`);

await t.step("change `AssertError` message on `assert` failure", () => {
assertThrows(
() => assert(x, falsePredicate),
AssertError,
"Hello symbol falsePredicate",
);
});

setAssertMessageFactory(defaultAssertMessageFactory);
});
1 change: 1 addition & 0 deletions deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
]
},
"imports": {
"@core/errorutil": "jsr:@core/errorutil@^1.2.0",
"@core/iterutil": "jsr:@core/iterutil@^0.3.0",
"@core/unknownutil": "./mod.ts",
"@deno/dnt": "jsr:@deno/dnt@^0.41.1",
Expand Down
21 changes: 15 additions & 6 deletions ensure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,32 @@ import { assert } from "./assert.ts";
*
* It throws {@linkcode [assert].AssertError|AssertError} if the value does not satisfy the predicate.
*
* @param x The value to be ensured.
* @param pred The predicate function to test the value against.
* @returns The input value `x`.
*
* ```ts
* import { ensure, is } from "@core/unknownutil";
*
* const a: unknown = "hello";
* const _: string = ensure(a, is.String);
* ```
*
* @param x The value to be ensured.
* @param pred The predicate function to test the value against.
* @param options Optional configuration for the assertion.
* @returns The input value `x`.
* Use {@linkcode https://jsr.io/@core/errorutil/doc/alter/~/alter|@core/errorutil/alter.alter} to alter error.
*
* ```ts
* import { alter } from "@core/errorutil/alter";
* import { ensure, is } from "@core/unknownutil";
*
* const a: unknown = 42;
* const _: string = alter(() => ensure(a, is.String), new Error("a is not a string"));
* // Error: a is not a string
* ```
*/
export function ensure<T>(
x: unknown,
pred: Predicate<T>,
options: { message?: string; name?: string } = {},
): T {
assert(x, pred, options);
assert(x, pred);
return x;
}
42 changes: 1 addition & 41 deletions ensure_test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import { assertStrictEquals, assertThrows } from "@std/assert";
import {
AssertError,
defaultAssertMessageFactory,
setAssertMessageFactory,
} from "./assert.ts";
import { AssertError } from "./assert.ts";
import { ensure } from "./ensure.ts";

const x: unknown = Symbol("x");
Expand All @@ -28,40 +24,4 @@ Deno.test("ensure", async (t) => {
`Expected a value that satisfies the predicate falsePredicate, got symbol: undefined`,
);
});

await t.step(
"throws an `AssertError` on false predicate with a custom name",
() => {
assertThrows(
() => ensure(x, falsePredicate, { name: "hello world" }),
AssertError,
`Expected hello world that satisfies the predicate falsePredicate, got symbol: undefined`,
);
},
);

await t.step(
"throws an `AssertError` with a custom message on false predicate",
() => {
assertThrows(
() => ensure(x, falsePredicate, { message: "Hello" }),
AssertError,
"Hello",
);
},
);
});

Deno.test("setAssertMessageFactory", async (t) => {
setAssertMessageFactory((x, pred) => `Hello ${typeof x} ${pred.name}`);

await t.step("change `AssertError` message on `ensure` failure", () => {
assertThrows(
() => ensure(x, falsePredicate),
AssertError,
"Hello symbol falsePredicate",
);
});

setAssertMessageFactory(defaultAssertMessageFactory);
});