[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 type { TaggedError } from "../error/tagged-error.ts";
import { UnexpectedError } from "../error/unexpected-error.ts";
import type { MaybePromise } from "../types/maybe-promise.ts"; import type { MaybePromise } from "../types/maybe-promise.ts";
import { Result } from "./result.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 * An AsyncResult represents the result of an asynchronous operation that can
* resolve to a value of type `TValue` or an error of type `TError`. * resolve to a value of type `TValue` or an error of type `TError`.
*/ */
export type AsyncResult< export class AsyncResult<
TValue = any, TValue = any,
TError extends TaggedError = never, TError extends TaggedError = never,
> = Promise<Result<TValue, TError>>; > {
static tryFrom<T, TError extends TaggedError>(
export namespace AsyncResult {
export async function tryFrom<T, TError extends TaggedError>(
fn: () => MaybePromise<T>, fn: () => MaybePromise<T>,
errorMapper: (error: any) => TError, errorMapper: (error: any) => TError,
): AsyncResult<T, TError> { ): AsyncResult<T, TError> {
try { return new AsyncResult(
return Result.succeedWith(await fn()); new Promise<Result<T, TError>>(async (resolve) => {
} catch (error) { try {
return Result.failWith(errorMapper(error)); 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> { static from<T>(fn: () => MaybePromise<T>): AsyncResult<T, never> {
return tryFrom(fn, (error) => new UnexpectedError(error) as 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 { describe, expect, test } from "@fabric/testing";
import { UnexpectedError } from "../error/unexpected-error.ts"; import { UnexpectedError } from "../error/unexpected-error.ts";
import { Result } from "../result/result.ts";
import { Run } from "./run.ts"; import { Run } from "./run.ts";
describe("Run", () => { describe("Run", () => {
describe("In Sequence", () => { describe("In Sequence", () => {
test("should pipe the results of multiple async functions", async () => { test("should pipe the results of multiple async functions", async () => {
const result = await Run.seq( const result = Run.seq(
async () => Result.succeedWith(1), () => AsyncResult.succeedWith(1),
async (x) => Result.succeedWith(x + 1), (x) => AsyncResult.succeedWith(x + 1),
async (x) => Result.succeedWith(x * 2), (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 () => { test("should return the first error if one of the functions fails", async () => {
const result = await Run.seq( const result = await Run.seq(
async () => Result.succeedWith(1), () => AsyncResult.succeedWith(1),
async () => Result.failWith(new UnexpectedError()), () => AsyncResult.failWith(new UnexpectedError()),
async (x) => Result.succeedWith(x * 2), (x) => AsyncResult.succeedWith(x * 2),
); ).promise();
expect(result.isError()).toBe(true); expect(result.isError()).toBe(true);
}); });

View File

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