[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 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> {
|
||||||
|
return new AsyncResult(
|
||||||
|
new Promise<Result<T, TError>>(async (resolve) => {
|
||||||
try {
|
try {
|
||||||
return Result.succeedWith(await fn());
|
const value = await fn();
|
||||||
|
resolve(Result.ok(value));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return Result.failWith(errorMapper(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)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user