From 463912215e2f8cba7174dfbf082527bceb13bd2c Mon Sep 17 00:00:00 2001 From: Dan Rumney Date: Sat, 4 May 2024 13:48:21 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20make=20Result=20more=20u?= =?UTF-8?q?ser=20friendly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Result is nicer to use when we stick to the language of succcess and error, so this commit provides that BREAKING CHANGE: 🧨 y ✅ Closes: 195 --- src/Either.spec.ts | 13 ++++++++++ src/Either.ts | 28 +++++++------------- src/Result.spec.ts | 65 ++++++++++++++++++++++++++++++++++++++++++++++ src/Result.ts | 44 ++++++++++++++++++++++++++++++- 4 files changed, 131 insertions(+), 19 deletions(-) create mode 100644 src/Result.spec.ts diff --git a/src/Either.spec.ts b/src/Either.spec.ts index 324c6223..2337214d 100644 --- a/src/Either.spec.ts +++ b/src/Either.spec.ts @@ -62,4 +62,17 @@ describe('Either', () => { expect(e.isLeft()).toBe(false); }); }); + describe('.proceedLeft', () => { + test('handles regular functions', () => { + const e = Either.left('test'); + expect(e.proceedLeft((x) => Either.left(x + 'ing')).getLeft()).toBe('testing'); + }); + test('handles async functions', async () => { + const e = Either.left('test'); + const proceeded = await e.proceedLeft((x) => + Promise.resolve(Either.left(x + 'ing')) + ); + expect(proceeded.getLeft()).toBe('testing'); + }); + }); }); diff --git a/src/Either.ts b/src/Either.ts index adab3e08..11d329fe 100644 --- a/src/Either.ts +++ b/src/Either.ts @@ -127,7 +127,11 @@ export class Either { * * @param lFunc */ - public proceedLeft(lFunc: (val: L) => Either): Either { + public proceedLeft(lFunc: (val: L) => Promise>): Promise>; + public proceedLeft(lFunc: (val: L) => Either): Either; + public proceedLeft( + lFunc: ((val: L) => Either) | ((val: L) => Promise>) + ): Either | Promise> { if (this.left.isPresent()) { return lFunc(this.left.get()); } @@ -144,23 +148,11 @@ export class Either { * * @param rFunc */ - public proceedRight(rFunc: (val: R) => Either): Either { - if (this.right.isPresent()) { - return rFunc(this.right.get()); - } - - return Either.left(this.left.get()); - } - - /** - * This provides an implementation of {@link proceedRightAsync} that can handle a mapping function - * that returns a `Promise`. - * - * @param rFunc - */ - public async proceedRightAsync( - rFunc: (val: R) => Promise> - ): Promise> { + public proceedRight(rFunc: (val: R) => Promise>): Promise>; + public proceedRight(rFunc: (val: R) => Either): Either; + public proceedRight( + rFunc: ((val: R) => Either) | ((val: R) => Promise>) + ): Either | Promise> { if (this.right.isPresent()) { return rFunc(this.right.get()); } diff --git a/src/Result.spec.ts b/src/Result.spec.ts new file mode 100644 index 00000000..f0e250fb --- /dev/null +++ b/src/Result.spec.ts @@ -0,0 +1,65 @@ +import {expect, describe, test} from 'vitest'; + +import {Result} from '../src/Result'; +import {Optional} from '../src/Optional'; + +describe('Result', () => { + test('maps Optionals correctly (right)', async () => { + const e = Result.error(new Error('Test')); + e.mapRight((d) => Optional.of(d)).apply( + () => { + throw new Error('Wrong function called'); + }, + (f) => { + expect(f).toBeInstanceOf(Error); + } + ); + }); + test('maps Optionals correctly (left)', async () => { + const e = Result.success(1); + e.mapLeft((d) => Optional.of(d)).apply( + (f) => { + expect(f).toBe(1); + }, + () => { + throw new Error('Wrong function called'); + } + ); + }); + test('maps Optionals correctly (both)', async () => { + expect( + Result.success(1).map( + (d) => Optional.of((d as number) * 2), + (d) => Optional.of(0) + ) + ).toBe(2); + expect( + Result.error(new Error('Test')).map( + (d) => Optional.of((d as number) * 2), + (d) => Optional.of(0) + ) + ).toBe(0); + }); + + describe('.isError', () => { + test('returns true for right values', () => { + const e = Result.error(new Error('Test')); + expect(e.isError()).toBe(true); + }); + test('returns false for left values', () => { + const e = Result.success('test'); + expect(e.isError()).toBe(false); + }); + }); + + describe('.isSuccess', () => { + test('returns true for left values', () => { + const e = Result.success('test'); + expect(e.isSuccess()).toBe(true); + }); + test('returns false for right values', () => { + const e = Result.error(new Error('Test')); + expect(e.isSuccess()).toBe(false); + }); + }); +}); diff --git a/src/Result.ts b/src/Result.ts index 76303ef1..e80f8b8a 100644 --- a/src/Result.ts +++ b/src/Result.ts @@ -1,6 +1,48 @@ import {Either} from './Either'; +import {Optional} from './Optional'; /** * A Result is a value that represents either success or failure. It's a special case of an Either. */ -export class Result extends Either {} +export class Result extends Either { + private static fromEither(either: Either) { + const left: Optional = Optional.of(either.isLeft() ? either.getLeft() : null); + const right: Optional = Optional.of(either.isRight() ? either.getRight() : null); + + return new Result(left, right); + } + + public static success( + value: NT | Optional + ): Result { + return Result.fromEither(Either.left(value)); + } + + public static error(value: NE): Result { + return Result.fromEither(Either.right(value)); + } + + public isSuccess() { + return this.isLeft(); + } + + public isError() { + return this.isRight(); + } + + public mapSuccess(...args: Parameters['mapLeft']>) { + return this.mapLeft(...args); + } + + public proceedWithSuccess(...args: Parameters['proceedLeft']>) { + return this.proceedLeft(...args); + } + + public mapError(...args: Parameters['mapRight']>) { + return this.mapRight(...args); + } + + public proceedsWithError(...args: Parameters['proceedRight']>) { + return this.proceedRight(...args); + } +}