From 5326044b1ee347e7ba73c565a23021e4f865a548 Mon Sep 17 00:00:00 2001 From: Chen Guo Date: Thu, 4 Jun 2020 12:40:42 -0700 Subject: [PATCH 1/5] EitherAsync implements Promise --- src/EitherAsync.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/EitherAsync.ts b/src/EitherAsync.ts index f4d46c41..5860c169 100644 --- a/src/EitherAsync.ts +++ b/src/EitherAsync.ts @@ -1,7 +1,7 @@ import { Either, Left, Right } from './Either' import { MaybeAsync } from './MaybeAsync' -export interface EitherAsync { +export interface EitherAsync extends Promise> { /** * It's important to remember how `run` will behave because in an * async context there are other ways for a function to fail other @@ -130,6 +130,23 @@ class EitherAsyncImpl implements EitherAsync { ): EitherAsync { return this.chain(f) } + + async then, TResult2 = never>( + onfulfilled?: ((value: Either) => TResult1 | PromiseLike) | undefined | null, + onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null + ): Promise { + return this.run().then(onfulfilled, onrejected); + } + + async catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise | TResult> { + return this.run().catch(onrejected); + } + + async finally(onfinally?: (() => void) | undefined | null): Promise> { + return this.run().finally(onfinally); + } + + readonly [Symbol.toStringTag]: string; } /** Constructs an EitherAsync object from a function that takes an object full of helpers that let you lift things into the EitherAsync context and returns a Promise */ From c30617f079e55eec032f53df4b6f3544f9f55430 Mon Sep 17 00:00:00 2001 From: Chen Guo Date: Thu, 4 Jun 2020 17:55:24 -0700 Subject: [PATCH 2/5] Make EitherAsync's map and chain more versatile --- src/EitherAsync.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/EitherAsync.ts b/src/EitherAsync.ts index 5860c169..1d2753e4 100644 --- a/src/EitherAsync.ts +++ b/src/EitherAsync.ts @@ -1,6 +1,8 @@ import { Either, Left, Right } from './Either' import { MaybeAsync } from './MaybeAsync' +export type EitherAsyncable = Either | EitherAsync | Promise>; + export interface EitherAsync extends Promise> { /** * It's important to remember how `run` will behave because in an @@ -17,11 +19,11 @@ export interface EitherAsync extends Promise> { */ run(): Promise> /** Transforms the `Right` value of `this` with a given function. If the EitherAsync that is being mapped resolves to a Left then the mapping function won't be called and `run` will resolve the whole thing to that Left, just like the regular Either#map */ - map(f: (value: R) => R2): EitherAsync + map(f: (value: R) => R2 | PromiseLike): EitherAsync /** Maps the `Left` value of `this`, acts like an identity if `this` is `Right` */ mapLeft(f: (value: L) => L2): EitherAsync /** Transforms `this` with a function that returns a `EitherAsync`. Behaviour is the same as the regular Either#chain */ - chain(f: (value: R) => EitherAsync): EitherAsync + chain(f: (value: R) => EitherAsyncable): EitherAsync /** The same as EitherAsync#chain but executes the transformation function only if the value is Left. Useful for recovering from errors */ chainLeft(f: (value: L) => EitherAsync): EitherAsync /** Converts `this` to a MaybeAsync, discarding any error values */ @@ -75,8 +77,8 @@ class EitherAsyncImpl implements EitherAsync { } } - map(f: (value: R) => R2): EitherAsync { - return EitherAsync((helpers) => this.runPromise(helpers).then(f)) + map(f: (value: R) => R2 | PromiseLike): EitherAsync { + return EitherAsync((helpers) => this.runPromise(helpers).then(f)); } mapLeft(f: (value: L) => L2): EitherAsync { @@ -89,10 +91,10 @@ class EitherAsyncImpl implements EitherAsync { }) } - chain(f: (value: R) => EitherAsync): EitherAsync { + chain(f: (value: R) => EitherAsyncable): EitherAsync { return EitherAsync(async (helpers) => { const value = await this.runPromise(helpers) - return helpers.fromPromise(f(value).run()) + return helpers.fromPromise(Promise.resolve(f(value))) }) } From 65b15a531d3f34dd5d86206ebf1c2b5824f58b0a Mon Sep 17 00:00:00 2001 From: Chen Guo Date: Thu, 4 Jun 2020 18:13:45 -0700 Subject: [PATCH 3/5] add tests for new EitherAsync capabilities --- src/EitherAsync.test.ts | 41 +++++++++++++++++++++++++++++++++++++++++ src/EitherAsync.ts | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/EitherAsync.test.ts b/src/EitherAsync.test.ts index 80d493b7..b3b50096 100644 --- a/src/EitherAsync.test.ts +++ b/src/EitherAsync.test.ts @@ -42,6 +42,11 @@ describe('EitherAsync', () => { expect(await ea.run()).toEqual(Left('should show')) }) + test('promise interface', async () => { + const newEitherAsync = EitherAsync(() => Promise.resolve(5)); + expect(await newEitherAsync).toEqual(Right(5)); + }); + test('map', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5)).map( (_) => 'val' @@ -54,6 +59,18 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right('val')) }) + test('map async', async () => { + const newEitherAsync = EitherAsync(() => Promise.resolve(5)).map( + async (_) => 'val' + ) + const newEitherAsync2 = EitherAsync(() => Promise.resolve(5))[ + 'fantasy-land/map' + ](async (_) => 'val') + + expect(await newEitherAsync.run()).toEqual(Right('val')) + expect(await newEitherAsync2.run()).toEqual(Right('val')) + }); + test('mapLeft', async () => { const newEitherAsync = EitherAsync(() => Promise.reject(0) @@ -79,6 +96,30 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right('val')) }) + test('chain either', async () => { + const chainedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + v => Right(v + 1) + ); + expect(await chainedEitherAsync).toEqual(Right(6)); + + const failedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + _ => Left('failed') + ); + expect(await failedEitherAsync).toEqual(Left('failed')); + }) + + test('chain promise either', async () => { + const chainedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + async (v) => Right(v + 1) + ); + expect(await chainedEitherAsync).toEqual(Right(6)); + + const failedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( + async (_) => Left('failed') + ); + expect(await failedEitherAsync).toEqual(Left('failed')); + }) + test('chainLeft', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5) diff --git a/src/EitherAsync.ts b/src/EitherAsync.ts index 1d2753e4..f74769c3 100644 --- a/src/EitherAsync.ts +++ b/src/EitherAsync.ts @@ -1,7 +1,7 @@ import { Either, Left, Right } from './Either' import { MaybeAsync } from './MaybeAsync' -export type EitherAsyncable = Either | EitherAsync | Promise>; +export type EitherAsyncable = Either | Promise>; export interface EitherAsync extends Promise> { /** From 6b51c0b3f0113e317b4c9947696160dda3fff8b1 Mon Sep 17 00:00:00 2001 From: Chen Guo Date: Thu, 4 Jun 2020 18:27:00 -0700 Subject: [PATCH 4/5] apply more versatile interface to EitherAsync mapLeft and chainLeft --- src/EitherAsync.test.ts | 41 +++++++++++++++++++++++++++++++++++++++-- src/EitherAsync.ts | 18 +++++++++--------- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src/EitherAsync.test.ts b/src/EitherAsync.test.ts index b3b50096..101a4290 100644 --- a/src/EitherAsync.test.ts +++ b/src/EitherAsync.test.ts @@ -84,6 +84,19 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right(0)) }) + test('mapLeft async', async () => { + const newEitherAsync = EitherAsync(() => + Promise.reject(0) + ).mapLeft(async (x) => x + 1) + + const newEitherAsync2 = EitherAsync(() => + Promise.resolve(0) + ).mapLeft(async (x) => x + 1) + + expect(await newEitherAsync).toEqual(Left(1)) + expect(await newEitherAsync2).toEqual(Right(0)) + }) + test('chain', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5)).chain((_) => EitherAsync(() => Promise.resolve('val')) @@ -96,7 +109,7 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right('val')) }) - test('chain either', async () => { + test('chain Either', async () => { const chainedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( v => Right(v + 1) ); @@ -108,7 +121,7 @@ describe('EitherAsync', () => { expect(await failedEitherAsync).toEqual(Left('failed')); }) - test('chain promise either', async () => { + test('chain Promsie', async () => { const chainedEitherAsync = EitherAsync(() => Promise.resolve(5)).chain( async (v) => Right(v + 1) ); @@ -132,6 +145,30 @@ describe('EitherAsync', () => { expect(await newEitherAsync2.run()).toEqual(Right(6)) }) + test('chainLeft Either', async () => { + const newEitherAsync = EitherAsync(() => + Promise.resolve(5) + ).chainLeft(_ => Right(0)); + const newEitherAsync2 = EitherAsync(() => + Promise.reject(5) + ).chainLeft(v => Right(v + 1)); + + expect(await newEitherAsync.run()).toEqual(Right(5)) + expect(await newEitherAsync2.run()).toEqual(Right(6)) + }) + + test('chainLeft Promise', async () => { + const newEitherAsync = EitherAsync(() => + Promise.resolve(5) + ).chainLeft(async _ => Right(0)); + const newEitherAsync2 = EitherAsync(() => + Promise.reject(5) + ).chainLeft(async v => Right(v + 1)); + + expect(await newEitherAsync.run()).toEqual(Right(5)) + expect(await newEitherAsync2.run()).toEqual(Right(6)) + }) + test('toMaybeAsync', async () => { const ma = EitherAsync(({ liftEither }) => liftEither(Left('123'))) diff --git a/src/EitherAsync.ts b/src/EitherAsync.ts index f74769c3..06a245eb 100644 --- a/src/EitherAsync.ts +++ b/src/EitherAsync.ts @@ -1,7 +1,7 @@ import { Either, Left, Right } from './Either' import { MaybeAsync } from './MaybeAsync' -export type EitherAsyncable = Either | Promise>; +type EitherAsyncLike = Either | Promise>; export interface EitherAsync extends Promise> { /** @@ -21,11 +21,11 @@ export interface EitherAsync extends Promise> { /** Transforms the `Right` value of `this` with a given function. If the EitherAsync that is being mapped resolves to a Left then the mapping function won't be called and `run` will resolve the whole thing to that Left, just like the regular Either#map */ map(f: (value: R) => R2 | PromiseLike): EitherAsync /** Maps the `Left` value of `this`, acts like an identity if `this` is `Right` */ - mapLeft(f: (value: L) => L2): EitherAsync + mapLeft(f: (value: L) => L2 | PromiseLike): EitherAsync /** Transforms `this` with a function that returns a `EitherAsync`. Behaviour is the same as the regular Either#chain */ - chain(f: (value: R) => EitherAsyncable): EitherAsync + chain(f: (value: R) => EitherAsyncLike): EitherAsync /** The same as EitherAsync#chain but executes the transformation function only if the value is Left. Useful for recovering from errors */ - chainLeft(f: (value: L) => EitherAsync): EitherAsync + chainLeft(f: (value: L) => EitherAsyncLike): EitherAsync /** Converts `this` to a MaybeAsync, discarding any error values */ toMaybeAsync(): MaybeAsync /** Returns `Right` if `this` is `Left` and vice versa */ @@ -81,29 +81,29 @@ class EitherAsyncImpl implements EitherAsync { return EitherAsync((helpers) => this.runPromise(helpers).then(f)); } - mapLeft(f: (value: L) => L2): EitherAsync { + mapLeft(f: (value: L) => L2 | PromiseLike): EitherAsync { return EitherAsync(async (helpers) => { try { return await this.runPromise((helpers as any) as EitherAsyncHelpers) } catch (e) { - throw f(e) + throw await f(e); } }) } - chain(f: (value: R) => EitherAsyncable): EitherAsync { + chain(f: (value: R) => EitherAsyncLike): EitherAsync { return EitherAsync(async (helpers) => { const value = await this.runPromise(helpers) return helpers.fromPromise(Promise.resolve(f(value))) }) } - chainLeft(f: (value: L) => EitherAsync): EitherAsync { + chainLeft(f: (value: L) => EitherAsyncLike): EitherAsync { return EitherAsync(async (helpers) => { try { return await this.runPromise((helpers as any) as EitherAsyncHelpers) } catch (e) { - return helpers.fromPromise(f(e).run()) + return helpers.fromPromise(Promise.resolve(f(e))); } }) } From ce80c939741abfc971fc29a1d2488e7b8ea40c70 Mon Sep 17 00:00:00 2001 From: Chen Guo Date: Thu, 4 Jun 2020 18:28:52 -0700 Subject: [PATCH 5/5] add EitherAsync promise reject test --- src/EitherAsync.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/EitherAsync.test.ts b/src/EitherAsync.test.ts index 101a4290..f55a867c 100644 --- a/src/EitherAsync.test.ts +++ b/src/EitherAsync.test.ts @@ -45,7 +45,10 @@ describe('EitherAsync', () => { test('promise interface', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5)); expect(await newEitherAsync).toEqual(Right(5)); - }); + + const newEitherAsync2 = EitherAsync(() => Promise.reject('nope')); + expect(await newEitherAsync2).toEqual(Left('nope')); +}); test('map', async () => { const newEitherAsync = EitherAsync(() => Promise.resolve(5)).map(