[fabric/core] convert AsyncResult to a class for improved usability and update Run functions to use AsyncResult

This commit is contained in:
Pablo Baleztena 2024-10-16 16:15:57 -03:00
parent 4950730d9e
commit d56c4bd469
3 changed files with 133 additions and 49 deletions

View File

@ -1,6 +1,6 @@
// deno-lint-ignore-file no-namespace no-explicit-any
// deno-lint-ignore-file no-namespace no-explicit-any no-async-promise-executor
import { UnexpectedError } from "@fabric/core";
import type { TaggedError } from "../error/tagged-error.ts";
import { UnexpectedError } from "../error/unexpected-error.ts";
import type { MaybePromise } from "../types/maybe-promise.ts";
import { Result } from "./result.ts";
@ -8,24 +8,123 @@ import { Result } from "./result.ts";
* An AsyncResult represents the result of an asynchronous operation that can
* resolve to a value of type `TValue` or an error of type `TError`.
*/
export type AsyncResult<
export class AsyncResult<
TValue = any,
TError extends TaggedError = never,
> = Promise<Result<TValue, TError>>;
export namespace AsyncResult {
export async function tryFrom<T, TError extends TaggedError>(
> {
static tryFrom<T, TError extends TaggedError>(
fn: () => MaybePromise<T>,
errorMapper: (error: any) => TError,
): AsyncResult<T, TError> {
try {
return Result.succeedWith(await fn());
} catch (error) {
return Result.failWith(errorMapper(error));
}
return new AsyncResult(
new Promise<Result<T, TError>>(async (resolve) => {
try {
const value = await fn();
resolve(Result.ok(value));
} catch (error) {
resolve(Result.failWith(errorMapper(error)));
}
}),
);
}
export function from<T>(fn: () => MaybePromise<T>): AsyncResult<T, never> {
return tryFrom(fn, (error) => new UnexpectedError(error) as never);
static from<T>(fn: () => MaybePromise<T>): AsyncResult<T, never> {
return AsyncResult.tryFrom(
fn,
(error) => new UnexpectedError(error) as never,
);
}
static ok<T>(value: T): AsyncResult<T, never> {
return new AsyncResult(Promise.resolve(Result.ok(value)));
}
static succeedWith = AsyncResult.ok;
static failWith<TError extends TaggedError>(
error: TError,
): AsyncResult<never, TError> {
return new AsyncResult(Promise.resolve(Result.failWith(error)));
}
private constructor(private r: Promise<Result<TValue, TError>>) {
}
promise(): Promise<Result<TValue, TError>> {
return this.r;
}
async unwrapOrThrow(): Promise<TValue> {
return (await this.r).unwrapOrThrow();
}
async orThrow(): Promise<void> {
return (await this.r).orThrow();
}
async unwrapErrorOrThrow(): Promise<TError> {
return (await this.r).unwrapErrorOrThrow();
}
/**
* Map a function over the value of the result.
*/
map<TMappedValue>(
fn: (value: TValue) => TMappedValue,
): AsyncResult<TMappedValue, TError> {
return new AsyncResult(
this.r.then((result) => result.map(fn)),
);
}
/**
* Maps a function over the value of the result and flattens the result.
*/
flatMap<TMappedValue, TMappedError extends TaggedError>(
fn: (value: TValue) => AsyncResult<TMappedValue, TMappedError>,
): AsyncResult<TMappedValue, TError | TMappedError> {
return new AsyncResult(
this.r.then((result) => {
if (result.isError()) {
return result as any;
}
return (fn(result.unwrapOrThrow())).promise();
}),
);
}
/**
* Try to map a function over the value of the result.
* If the function throws an error, the result will be a failure.
*/
tryMap<TMappedValue>(
fn: (value: TValue) => TMappedValue,
errMapper: (error: any) => TError,
): AsyncResult<TMappedValue, TError> {
return new AsyncResult(
this.r.then((result) => result.tryMap(fn, errMapper)),
);
}
/**
* Map a function over the error of the result.
*/
mapError<TMappedError extends TaggedError>(
fn: (error: TError) => TMappedError,
): AsyncResult<TValue, TMappedError> {
return new AsyncResult(
this.r.then((result) => result.mapError(fn)),
);
}
/**
* Taps a function if the result is a success.
* This is useful for side effects that do not modify the result.
*/
tap(fn: (value: TValue) => void): AsyncResult<TValue, TError> {
return new AsyncResult(
this.r.then((result) => result.tap(fn)),
);
}
}

View File

@ -1,27 +1,26 @@
// deno-lint-ignore-file require-await
import { AsyncResult } from "@fabric/core";
import { describe, expect, test } from "@fabric/testing";
import { UnexpectedError } from "../error/unexpected-error.ts";
import { Result } from "../result/result.ts";
import { Run } from "./run.ts";
describe("Run", () => {
describe("In Sequence", () => {
test("should pipe the results of multiple async functions", async () => {
const result = await Run.seq(
async () => Result.succeedWith(1),
async (x) => Result.succeedWith(x + 1),
async (x) => Result.succeedWith(x * 2),
const result = Run.seq(
() => AsyncResult.succeedWith(1),
(x) => AsyncResult.succeedWith(x + 1),
(x) => AsyncResult.succeedWith(x * 2),
);
expect(result.unwrapOrThrow()).toEqual(4);
expect(await result.unwrapOrThrow()).toEqual(4);
});
test("should return the first error if one of the functions fails", async () => {
const result = await Run.seq(
async () => Result.succeedWith(1),
async () => Result.failWith(new UnexpectedError()),
async (x) => Result.succeedWith(x * 2),
);
() => AsyncResult.succeedWith(1),
() => AsyncResult.failWith(new UnexpectedError()),
(x) => AsyncResult.succeedWith(x * 2),
).promise();
expect(result.isError()).toBe(true);
});

View File

@ -4,7 +4,7 @@ import type { AsyncResult } from "../result/async-result.ts";
export namespace Run {
// prettier-ignore
export async function seq<
export function seq<
T1,
TE1 extends TaggedError,
T2,
@ -14,7 +14,7 @@ export namespace Run {
fn2: (value: T1) => AsyncResult<T2, TE2>,
): AsyncResult<T2, TE1 | TE2>;
// prettier-ignore
export async function seq<
export function seq<
T1,
TE1 extends TaggedError,
T2,
@ -27,7 +27,7 @@ export namespace Run {
fn3: (value: T2) => AsyncResult<T3, TE3>,
): AsyncResult<T3, TE1 | TE2 | TE3>;
// prettier-ignore
export async function seq<
export function seq<
T1,
TE1 extends TaggedError,
T2,
@ -42,24 +42,20 @@ export namespace Run {
fn3: (value: T2) => AsyncResult<T3, TE3>,
fn4: (value: T3) => AsyncResult<T4, TE4>,
): AsyncResult<T4, TE1 | TE2 | TE3 | TE4>;
export async function seq(
export function seq(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): AsyncResult<any, any> {
let result = await fns[0]!();
let result = fns[0]!();
for (let i = 1; i < fns.length; i++) {
if (result.isError()) {
return result;
}
result = await fns[i]!(result.unwrapOrThrow());
result = result.flatMap((value) => fns[i]!(value));
}
return result;
}
// prettier-ignore
export async function seqUNSAFE<
export function seqOrThrow<
T1,
TE1 extends TaggedError,
T2,
@ -69,7 +65,7 @@ export namespace Run {
fn2: (value: T1) => AsyncResult<T2, TE2>,
): Promise<T2>;
// prettier-ignore
export async function seqUNSAFE<
export function seqOrThrow<
T1,
TE1 extends TaggedError,
T2,
@ -81,21 +77,11 @@ export namespace Run {
fn2: (value: T1) => AsyncResult<T2, TE2>,
fn3: (value: T2) => AsyncResult<T3, TE3>,
): Promise<T2>;
export async function seqUNSAFE(
export function seqOrThrow(
...fns: ((...args: any[]) => AsyncResult<any, any>)[]
): Promise<any> {
const result = await (seq as any)(...fns);
if (result.isError()) {
throw result.unwrapOrThrow();
}
const result = (seq as any)(...fns);
return result.unwrapOrThrow();
}
export async function UNSAFE<T, TError extends TaggedError>(
fn: () => AsyncResult<T, TError>,
): Promise<T> {
return (await fn()).unwrapOrThrow();
}
}