summaryrefslogtreecommitdiff
path: root/packages/taler-util/src/operation.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-util/src/operation.ts')
-rw-r--r--packages/taler-util/src/operation.ts198
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 }