Skip to content

Commit

Permalink
✨ feat: Add Safunc#onValidationError for alternative error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Snowflyt committed Apr 27, 2024
1 parent e4f497c commit 0635c2e
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 3 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ const range = def(sig1, sig2, (startOrStop, stop?, step = 1) => {
const unwrappedRange = range.unwrap();
// ^?: ((stop: number) => number[]) & ((start: number, stop: number, step?: number) => number[])

// Use `onValidationError` to provide alternative error handling instead of just throwing a `TypeError`
const range2 = range.onValidationError(console.error);
range2(1, 3.5); // => [1, 2, 3] - The function is returned as the errors are handled by a custom handler
// This time, the error message is printed to the console and no error is thrown
// If you still want to throw an error instead of returning the function, you can rethrow it in the custom handler

// Use `Safunc#matchArguments` to get the matched `Sig` for the given arguments
range.matchArguments(3); // => sig1
range.matchArguments(1, 5); // => sig2
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "safunc",
"version": "0.1.2",
"version": "0.1.3",
"private": true,
"description": "Create runtime-validated functions for both synchronous and asynchronous ones with ease, supporting optional parameters and overloaded signatures with smart type inference in TypeScript",
"keywords": [
Expand Down
28 changes: 28 additions & 0 deletions src/safunc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,34 @@ it("should support asynchronous functions", async () => {
),
);

let errorMessage = "";
const getTodosWrong2 = getTodosWrong.onValidationError((e) => {
errorMessage = e.message;
});
expect((await getTodosWrong2()).slice(0, 3)).toEqual([
{
userId: 1,
id: 1,
title: "delectus aut autem",
completed: false,
},
{
userId: 1,
id: 2,
title: "quis ut nam facilis et officia qui",
completed: false,
},
{
userId: 1,
id: 3,
title: "fugiat veniam minus",
completed: false,
},
]);
expect(errorMessage).toBe(
"Property '0/id' of the return value of 'function(): Promise<Array<{ userId: integer>0; id: string>0; title: string; completed: boolean }>>' must be a string (was number)",
);

const getTodo = defAsync(
// ^?
sig("integer>0", "=>", todo),
Expand Down
41 changes: 39 additions & 2 deletions src/safunc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ export interface Safunc<F extends Fn> extends F {
*/
unwrap: () => F;

/**
* Provides an error handler for validation errors instead of throwing them.
* @param handler The error handler.
* @returns
*/
onValidationError: (handler: (e: TypeError) => void) => Safunc<F>;

/**
* Get the matched `Sig` for given arguments.
* @param args Arguments to match.
Expand All @@ -311,7 +318,13 @@ export interface Safunc<F extends Fn> extends F {
}

const _defBuilder =
({ async }: { async: boolean }) =>
({
async,
validationErrorHandler,
}: {
async: boolean;
validationErrorHandler?: (e: TypeError) => void;
}) =>
(...args: unknown[]) => {
const sigs = args.slice(0, -1) as Sig<any>[];
const fn = args[args.length - 1] as Fn;
Expand All @@ -323,6 +336,11 @@ const _defBuilder =
].sort();
if (!availableArgumentLengths.includes(args.length)) {
const message = `Expected ${humanizeNaturalNumbers(availableArgumentLengths)} arguments, but got ${args.length}`;
if (validationErrorHandler) {
validationErrorHandler(new TypeError(message));
$matchedMorphedArguments = args;
return sigs[0]!;
}
throw new TypeError(message);
}

Expand Down Expand Up @@ -374,7 +392,14 @@ const _defBuilder =
.map(([sig, message], i) => ({ i, sig, message }))
.filter(({ message: m }) => m !== "ARG_LENGTH_NOT_MATCH");

if (errors.length === 1) throw new TypeError(errors[0]!.message);
if (errors.length === 1) {
if (validationErrorHandler) {
validationErrorHandler(new TypeError(errors[0]!.message));
$matchedMorphedArguments = args;
return errors[0]!.sig;
}
throw new TypeError(errors[0]!.message);
}

let message = "No overload ";
if (fn.name) message += `of function '${fn.name}' `;
Expand All @@ -387,6 +412,11 @@ const _defBuilder =
"\n";
}
message = message.trimEnd();
if (validationErrorHandler) {
validationErrorHandler(new TypeError(message));
$matchedMorphedArguments = args;
return errors[0]!.sig;
}
throw new TypeError(message);
};

Expand All @@ -410,6 +440,10 @@ const _defBuilder =
if (sigs.length > 1) message += `(overload ${sigs.indexOf(sig) + 1} of ${sigs.length}) `;
message += reason;
message = capitalize(message);
if (validationErrorHandler) {
validationErrorHandler(new TypeError(message));
return r;
}
throw new TypeError(message);
};

Expand Down Expand Up @@ -439,6 +473,9 @@ const _defBuilder =

unwrap: () => f,

onValidationError: (handler: (e: TypeError) => void) =>
_defBuilder({ async, validationErrorHandler: handler })(...args) as unknown as Safunc<any>,

matchArguments: (...args: unknown[]) => {
try {
return matchArguments(...args);
Expand Down
9 changes: 9 additions & 0 deletions test/README.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,15 @@ test("helper methods", () => {
return Array.from({ length: Math.ceil((stop - start) / step) }, (_, i) => start + i * step);
});

let errorMessage = "";
const range2 = range.onValidationError((e) => {
errorMessage = e.message;
});
expect(range2(1, 3.5)).toEqual([1, 2, 3]);
expect(errorMessage).toBe(
"The 2nd argument of 'function(integer, integer, ?integer>0): Array<integer>' (overload 2 of 2) must be an integer (was 3.5)",
);

expect(range.matchArguments(3)).toBe(sig1);
expect(range.matchArguments(1, 5)).toBe(sig2);
expect(range.matchArguments("foo")).toBe(null);
Expand Down

0 comments on commit 0635c2e

Please sign in to comment.