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/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";
|
||||
|
||||
@ -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