1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

feat: Introduce utils to handle if failing calls should throw

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>
Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
Iku-turso 2023-05-24 16:47:52 +03:00
parent 6a408aae77
commit 34f7d00877
5 changed files with 371 additions and 0 deletions

View File

@ -42,3 +42,5 @@ export * from "./src/types";
export * from "./src/union-env-path";
export * from "./src/wait";
export * from "./src/with-concurrency-limit";
export * from "./src/fatality-of-call/call-result/call-result";
export * from "./src/fatality-of-call/with-thrown-failures/with-thrown-failures";

View File

@ -0,0 +1,50 @@
import {
CallResult,
callWasFailure,
callWasSuccessful,
getFailure,
getSuccess,
} from "./call-result";
describe("call-result", () => {
it("given successful call, narrows type of response", () => {
const someSuccess: CallResult<string> = getSuccess("some-success");
if (callWasSuccessful(someSuccess)) {
expect(someSuccess.response).toBe("some-success");
}
expect.assertions(1);
});
it("given successful call, call is not failure", () => {
const actual = callWasFailure(getSuccess("some-success"));
expect(actual).toBe(false);
});
it("given unsuccessful call, narrows type of error", () => {
const someFailure: CallResult<string> = getFailure(
"some-error-code",
"some-cause"
);
if (callWasFailure(someFailure)) {
expect(someFailure.error).toEqual({
code: "some-error-code",
cause: "some-cause",
message: "some-cause",
});
}
expect.assertions(1);
});
it("given unsuccessful call, call is not successful", () => {
const actual = callWasSuccessful(
getFailure("some-error-code", "some-cause")
);
expect(actual).toBe(false);
});
});

View File

@ -0,0 +1,52 @@
import { isString } from "lodash/fp";
export type CallFailure = {
callWasSuccessful: false;
error: { code: string; message: string; cause: unknown };
};
export type CallSuccess<Response> = Response extends void
? { callWasSuccessful: true; response?: undefined }
: { callWasSuccessful: true; response: Response };
export type CallResult<Response> = CallSuccess<Response> | CallFailure;
export type AsyncCallResult<Response> = Promise<CallResult<Response>>;
export type AsyncCallSuccess<Response> = Promise<CallSuccess<Response>>;
export const getSuccess = <T>(response: T) => ({
callWasSuccessful: true as const,
response,
});
const getErrorMessage = (cause: unknown) => {
if (isString(cause)) {
return cause;
}
const causeObject = cause as any;
if (causeObject.message) {
return causeObject.message;
}
return undefined;
};
export const getFailure = (errorCode: string, errorCause: unknown) => ({
callWasSuccessful: false as const,
error: {
code: errorCode,
cause: errorCause,
message: getErrorMessage(errorCause),
},
});
export const callWasSuccessful = <Result>(
callResult: CallResult<Result>
): callResult is CallSuccess<Result> => callResult.callWasSuccessful;
export const callWasFailure = <Result>(
callResult: CallResult<Result>
): callResult is CallFailure => !callResult.callWasSuccessful;

View File

@ -0,0 +1,191 @@
import { AsyncCallResult, getFailure, getSuccess } from "../call-result/call-result";
import asyncFn, { AsyncFnMock } from "@async-fn/jest";
import {
withThrownFailures,
withThrownFailuresUnless,
} from "./with-thrown-failures";
type TestCallResult = AsyncCallResult<string>;
describe("with-thrown-failures", () => {
let toBeDecoratedMock: AsyncFnMock<() => TestCallResult>;
let actualPromise: TestCallResult;
let toBeDecorated: (arg1: string, arg2: string) => TestCallResult;
beforeEach(() => {
toBeDecoratedMock = asyncFn();
toBeDecorated = toBeDecoratedMock;
});
describe("given a function with general error handling, when called", () => {
beforeEach(() => {
const decorated = withThrownFailures(toBeDecorated);
actualPromise = decorated("some-arg", "some-other-arg");
});
it("calls the underlying function", () => {
expect(toBeDecoratedMock).toHaveBeenCalledWith(
"some-arg",
"some-other-arg"
);
});
it("when call resolves as success, resolves as so", async () => {
await toBeDecoratedMock.resolve(getSuccess("some-success"));
const actual = await actualPromise;
expect(actual).toEqual(getSuccess("some-success"));
});
it("when call resolves with failed call result with string as cause, throws", () => {
toBeDecoratedMock.resolve(getFailure("some-error-code", "some-cause"));
return expect(actualPromise).rejects.toThrow(
"Error(some-error-code): some-cause"
);
});
it("when call resolves with failed call result containing message for cause of error, throws", () => {
const someCause = { message: "some-cause" };
toBeDecoratedMock.resolve(getFailure("some-error-code", someCause));
return expect(actualPromise).rejects.toThrow(
"Error(some-error-code): some-cause"
);
});
it("when call resolves with failed call result with error object as cause, throws original error", () => {
const someError = new Error("some-error");
toBeDecoratedMock.resolve(getFailure("irrelevant", someError));
return expect(actualPromise).rejects.toBe(someError);
});
it("when call resolves with failed call result not containing message for cause of error, throws", () => {
const someCause = { some: "value" };
toBeDecoratedMock.resolve(getFailure("some-error-code", someCause));
return expect(actualPromise).rejects.toThrow("Error(some-error-code)");
});
it("when call rejects, throws original error", async () => {
const someError = new Error("some-unrelated-error");
toBeDecoratedMock.reject(someError);
return expect(actualPromise).rejects.toBe(someError);
});
});
describe("given a function with error handling unless error is specific, when called", () => {
beforeEach(() => {
const errorIsSpecific = (error: { code: string }) =>
error.code === "some-specific-failure";
const decorated =
withThrownFailuresUnless(errorIsSpecific)(toBeDecorated);
actualPromise = decorated("some-arg", "some-other-arg");
});
it("calls the underlying function", () => {
expect(toBeDecoratedMock).toHaveBeenCalledWith(
"some-arg",
"some-other-arg"
);
});
it("when call resolves as success, resolves as so", async () => {
await toBeDecoratedMock.resolve(getSuccess("some-success"));
const actual = await actualPromise;
expect(actual).toEqual(getSuccess("some-success"));
});
it("when call resolves as unrelated failure, throws", () => {
toBeDecoratedMock.resolve(
getFailure("some-unrelated-failure", "some-cause")
);
return expect(actualPromise).rejects.toThrow("some-cause");
});
it("when call resolves as the specific failure, resolves as failure", async () => {
await toBeDecoratedMock.resolve(
getFailure("some-specific-failure", "some-cause")
);
const actual = await actualPromise;
expect(actual).toEqual(getFailure("some-specific-failure", "some-cause"));
});
it("when call resolves with failed call result with string as cause, throws", () => {
toBeDecoratedMock.resolve(getFailure("some-error-code", "some-cause"));
return expect(actualPromise).rejects.toThrow(
"Error(some-error-code): some-cause"
);
});
it("when call resolves with failed call result containing message for cause of error, throws", () => {
const someCause = { message: "some-cause" };
toBeDecoratedMock.resolve(getFailure("some-error-code", someCause));
return expect(actualPromise).rejects.toThrow(
"Error(some-error-code): some-cause"
);
});
it("when call resolves with failed call result with error object as cause, throws original error", () => {
const someError = new Error("some-error");
toBeDecoratedMock.resolve(getFailure("irrelevant", someError));
return expect(actualPromise).rejects.toBe(someError);
});
it("when call resolves with failed call result not containing message for cause of error, throws", () => {
const someCause = { some: "value" };
toBeDecoratedMock.resolve(getFailure("some-error-code", someCause));
return expect(actualPromise).rejects.toThrow("Error(some-error-code)");
});
});
describe("given thrown failures unless specific error is thrown, when called", () => {
beforeEach(() => {
const decorated = withThrownFailuresUnless(
(error) => error.message === "some-specific-error"
)(toBeDecorated);
actualPromise = decorated("some-arg", "some-other-arg");
});
it("when call resolves as success, resolves as so", async () => {
await toBeDecoratedMock.resolve(getSuccess("some-success"));
const actual = await actualPromise;
expect(actual).toEqual(getSuccess("some-success"));
});
it("when call rejects as the specific error, resolves as specific failure", async () => {
const error = new Error("some-specific-error");
await toBeDecoratedMock.reject(error);
const actual = await actualPromise;
expect(actual).toEqual(getFailure("unknown", error));
});
it("when call rejects as some unrelated error, throws", async () => {
toBeDecoratedMock.reject(new Error("some-unrelated-error"));
return expect(actualPromise).rejects.toThrow("some-unrelated-error");
});
});
});

View File

@ -0,0 +1,76 @@
import {
AsyncCallResult,
CallFailure,
CallResult,
callWasFailure,
getFailure,
} from "../call-result/call-result";
type ToBeDecorated<T, T2 extends unknown[]> = (
...args: T2
) => AsyncCallResult<T>;
export const withThrownFailures =
<T, T2 extends unknown[]>(toBeDecorated: ToBeDecorated<T, T2>) =>
async (...args: T2) => {
const result = await toBeDecorated(...args);
if (callWasFailure(result)) {
throw getError(result);
}
return result;
};
export const withThrownFailuresUnless =
(...reasonsToNotThrow: ErrorShouldNotThrow[]) =>
<TValue, TArgs extends unknown[]>(
toBeDecorated: ToBeDecorated<TValue, TArgs>
) =>
async (...args: TArgs) => {
let result: CallResult<TValue>;
try {
result = await toBeDecorated(...args);
} catch (error) {
const reasonsForNotThrowing = reasonsToNotThrow.filter((reason) =>
reason(error)
);
if (reasonsForNotThrowing.length) {
return getFailure("unknown", error);
}
throw error;
}
const notThrownCall = result;
if (callWasFailure(notThrownCall)) {
const reasonsForNotThrowing = reasonsToNotThrow.filter((reason) =>
reason(notThrownCall.error)
);
if (reasonsForNotThrowing.length) {
return notThrownCall;
}
throw getError(notThrownCall);
}
return notThrownCall;
};
type ErrorShouldNotThrow = (error: any) => boolean;
const getError = (call: CallFailure) => {
if (call.error.cause instanceof Error) {
return call.error.cause;
}
if (call.error.message) {
return new Error(`Error(${call.error.code}): ${call.error.message}`);
}
return new Error(`Error(${call.error.code})`);
};