diff options
Diffstat (limited to 'packages/taler-util/src/operation.ts')
-rw-r--r-- | packages/taler-util/src/operation.ts | 198 |
1 files changed, 198 insertions, 0 deletions
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts new file mode 100644 index 000000000..e2ab9d4e4 --- /dev/null +++ b/packages/taler-util/src/operation.ts @@ -0,0 +1,198 @@ +/* + This file is part of GNU Taler + (C) 2023-2024 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Imports. + */ +import { + HttpResponse, + readResponseJsonOrErrorCode, + readSuccessResponseJsonOrThrow, + readTalerErrorResponse, +} from "./http-common.js"; +import { + Codec, + HttpStatusCode, + TalerError, + TalerErrorCode, + TalerErrorDetail, +} from "./index.js"; + +type OperationFailWithBodyOrNever<ErrorEnum, ErrorMap> = + ErrorEnum extends keyof ErrorMap ? OperationFailWithBody<ErrorMap> : never; + +export type OperationResult<Body, ErrorEnum, K = never> = + | OperationOk<Body> + | OperationAlternative<ErrorEnum, any> + | OperationFail<ErrorEnum> + | OperationFailWithBodyOrNever<ErrorEnum, K>; + +export function isOperationOk<T, E>( + c: OperationResult<T, E>, +): c is OperationOk<T> { + return c.type === "ok"; +} + +export function isOperationFail<T, E>( + c: OperationResult<T, E>, +): c is OperationFail<E> { + return c.type === "fail"; +} + +/** + * successful operation + */ +export interface OperationOk<BodyT> { + type: "ok"; + + /** + * Parsed response body. + */ + body: BodyT; +} + +/** + * unsuccessful operation, see details + */ +export interface OperationFail<T> { + type: "fail"; + + /** + * Error case (either HTTP status code or TalerErrorCode) + */ + case: T; + + detail: TalerErrorDetail; +} + +/** + * unsuccessful operation, see body + */ +export interface OperationAlternative<T, B> { + type: "fail"; + + case: T; + body: B; +} + +export interface OperationFailWithBody<B> { + type: "fail"; + + case: keyof B; + body: B[OperationFailWithBody<B>["case"]]; +} + +export async function opSuccessFromHttp<T>( + resp: HttpResponse, + codec: Codec<T>, +): Promise<OperationOk<T>> { + const body = await readSuccessResponseJsonOrThrow(resp, codec); + return { type: "ok" as const, body }; +} + +/** + * Success case, but instead of the body we're returning a fixed response + * to the client. + */ +export function opFixedSuccess<T>(body: T): OperationOk<T> { + return { type: "ok" as const, body }; +} + +export function opEmptySuccess(resp: HttpResponse): OperationOk<void> { + return { type: "ok" as const, body: void 0 }; +} + +export async function opKnownFailureWithBody<B>( + case_: keyof B, + body: B[typeof case_], +): Promise<OperationFailWithBody<B>> { + return { type: "fail", case: case_, body }; +} + +export async function opKnownAlternativeFailure<T extends HttpStatusCode, B>( + resp: HttpResponse, + s: T, + codec: Codec<B>, +): Promise<OperationAlternative<T, B>> { + const body = (await readResponseJsonOrErrorCode(resp, codec)).response; + return { type: "fail", case: s, body }; +} + +export async function opKnownHttpFailure<T extends HttpStatusCode>( + s: T, + resp: HttpResponse, +): Promise<OperationFail<T>> { + const detail = await readTalerErrorResponse(resp); + return { type: "fail", case: s, detail }; +} + +export function opKnownTalerFailure<T extends TalerErrorCode>( + s: T, + detail: TalerErrorDetail, +): OperationFail<T> { + return { type: "fail", case: s, detail }; +} + +export function opUnknownFailure(resp: HttpResponse, error: TalerErrorDetail): never { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR, + { + requestUrl: resp.requestUrl, + requestMethod: resp.requestMethod, + httpStatusCode: resp.status, + errorResponse: error, + }, + `Unexpected HTTP status ${resp.status} in response`, + ); +} + +/** + * Convenience function to throw an error if the operation is not a success. + */ +export function narrowOpSuccessOrThrow<Body, ErrorEnum>( + opName: string, + opRes: OperationResult<Body, ErrorEnum>, +): asserts opRes is OperationOk<Body> { + if (opRes.type !== "ok") { + throw TalerError.fromDetail( + TalerErrorCode.GENERIC_CLIENT_INTERNAL_ERROR, + { + operation: opName, + error: String(opRes.case), + detail: "detail" in opRes ? opRes.detail : undefined, + }, + `Operation ${opName} failed: ${String(opRes.case)}`, + ); + } +} + +export type ResultByMethod< + TT extends object, + p extends keyof TT, +> = TT[p] extends (...args: any[]) => infer Ret + ? Ret extends Promise<infer Result> + ? Result extends OperationResult<any, any> + ? Result + : never + : never //api always use Promises + : never; //error cases just for functions + +export type FailCasesByMethod<TT extends object, p extends keyof TT> = Exclude< + ResultByMethod<TT, p>, + OperationOk<any> +>; + +export type RedirectResult = { redirectURL: URL } |