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:
parent
6a408aae77
commit
34f7d00877
@ -42,3 +42,5 @@ export * from "./src/types";
|
|||||||
export * from "./src/union-env-path";
|
export * from "./src/union-env-path";
|
||||||
export * from "./src/wait";
|
export * from "./src/wait";
|
||||||
export * from "./src/with-concurrency-limit";
|
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";
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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;
|
||||||
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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})`);
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue
Block a user