[fabric/core] convert AsyncResult to a class for improved usability and update Run functions to use AsyncResult
This commit is contained in:
parent
4950730d9e
commit
d56c4bd469
@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user