Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make EitherAsync interface more versatile #192

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/EitherAsync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ 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));

const newEitherAsync2 = EitherAsync(() => Promise.reject('nope'));
expect(await newEitherAsync2).toEqual(Left('nope'));
});

test('map', async () => {
const newEitherAsync = EitherAsync(() => Promise.resolve(5)).map(
(_) => 'val'
Expand All @@ -54,6 +62,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<number, never>(() =>
Promise.reject(0)
Expand All @@ -67,6 +87,19 @@ describe('EitherAsync', () => {
expect(await newEitherAsync2.run()).toEqual(Right(0))
})

test('mapLeft async', async () => {
const newEitherAsync = EitherAsync<number, never>(() =>
Promise.reject(0)
).mapLeft(async (x) => x + 1)

const newEitherAsync2 = EitherAsync<never, number>(() =>
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'))
Expand All @@ -79,6 +112,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 Promsie<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)
Expand All @@ -91,6 +148,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<number, number>(() =>
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<Either>', async () => {
const newEitherAsync = EitherAsync(() =>
Promise.resolve(5)
).chainLeft(async _ => Right(0));
const newEitherAsync2 = EitherAsync<number, number>(() =>
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')))

Expand Down
45 changes: 32 additions & 13 deletions src/EitherAsync.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Either, Left, Right } from './Either'
import { MaybeAsync } from './MaybeAsync'

export interface EitherAsync<L, R> {
type EitherAsyncLike<L, R> = Either<L, R> | Promise<Either<L, R>>;

export interface EitherAsync<L, R> extends Promise<Either<L, R>> {
/**
* It's important to remember how `run` will behave because in an
* async context there are other ways for a function to fail other
Expand All @@ -17,13 +19,13 @@ export interface EitherAsync<L, R> {
*/
run(): Promise<Either<L, R>>
/** 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<R2>(f: (value: R) => R2): EitherAsync<L, R2>
map<R2>(f: (value: R) => R2 | PromiseLike<R2>): EitherAsync<L, R2>
/** Maps the `Left` value of `this`, acts like an identity if `this` is `Right` */
mapLeft<L2>(f: (value: L) => L2): EitherAsync<L2, R>
mapLeft<L2>(f: (value: L) => L2 | PromiseLike<L2>): EitherAsync<L2, R>
/** Transforms `this` with a function that returns a `EitherAsync`. Behaviour is the same as the regular Either#chain */
chain<R2>(f: (value: R) => EitherAsync<L, R2>): EitherAsync<L, R2>
chain<R2>(f: (value: R) => EitherAsyncLike<L, R2>): EitherAsync<L, R2>
/** The same as EitherAsync#chain but executes the transformation function only if the value is Left. Useful for recovering from errors */
chainLeft<L2>(f: (value: L) => EitherAsync<L2, R>): EitherAsync<L2, R>
chainLeft<L2>(f: (value: L) => EitherAsyncLike<L2, R>): EitherAsync<L2, R>
/** Converts `this` to a MaybeAsync, discarding any error values */
toMaybeAsync(): MaybeAsync<R>
/** Returns `Right` if `this` is `Left` and vice versa */
Expand Down Expand Up @@ -75,33 +77,33 @@ class EitherAsyncImpl<L, R> implements EitherAsync<L, R> {
}
}

map<R2>(f: (value: R) => R2): EitherAsync<L, R2> {
return EitherAsync((helpers) => this.runPromise(helpers).then(f))
map<R2>(f: (value: R) => R2 | PromiseLike<R2>): EitherAsync<L, R2> {
return EitherAsync((helpers) => this.runPromise(helpers).then(f));
}

mapLeft<L2>(f: (value: L) => L2): EitherAsync<L2, R> {
mapLeft<L2>(f: (value: L) => L2 | PromiseLike<L2>): EitherAsync<L2, R> {
return EitherAsync(async (helpers) => {
try {
return await this.runPromise((helpers as any) as EitherAsyncHelpers<L>)
} catch (e) {
throw f(e)
throw await f(e);
}
})
}

chain<R2>(f: (value: R) => EitherAsync<L, R2>): EitherAsync<L, R2> {
chain<R2>(f: (value: R) => EitherAsyncLike<L, R2>): EitherAsync<L, R2> {
return EitherAsync(async (helpers) => {
const value = await this.runPromise(helpers)
return helpers.fromPromise(f(value).run())
return helpers.fromPromise(Promise.resolve(f(value)))
})
}

chainLeft<L2>(f: (value: L) => EitherAsync<L2, R>): EitherAsync<L2, R> {
chainLeft<L2>(f: (value: L) => EitherAsyncLike<L2, R>): EitherAsync<L2, R> {
return EitherAsync(async (helpers) => {
try {
return await this.runPromise((helpers as any) as EitherAsyncHelpers<L>)
} catch (e) {
return helpers.fromPromise(f(e).run())
return helpers.fromPromise(Promise.resolve(f(e)));
}
})
}
Expand Down Expand Up @@ -130,6 +132,23 @@ class EitherAsyncImpl<L, R> implements EitherAsync<L, R> {
): EitherAsync<L, R2> {
return this.chain(f)
}

async then<TResult1 = Either<L, R>, TResult2 = never>(
onfulfilled?: ((value: Either<L, R>) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.run().then(onfulfilled, onrejected);
}

async catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<Either<L,R> | TResult> {
return this.run().catch(onrejected);
}

async finally(onfinally?: (() => void) | undefined | null): Promise<Either<L,R>> {
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 */
Expand Down