From f28fc24201ebe9e0e254da6069a8e64ef1a19b9f Mon Sep 17 00:00:00 2001 From: Sergei Zharinov <zharinov@users.noreply.github.com> Date: Sat, 22 Jul 2023 16:16:10 +0300 Subject: [PATCH] feat: Support `.catch` method for `Result` (#23505) --- lib/util/result.spec.ts | 78 +++++++++++++++++++++++++++++++++++++++ lib/util/result.ts | 81 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/lib/util/result.spec.ts b/lib/util/result.spec.ts index 71cc995519..1d213025d3 100644 --- a/lib/util/result.spec.ts +++ b/lib/util/result.spec.ts @@ -107,6 +107,16 @@ describe('util/result', () => { .unwrap() ).toThrow('oops'); }); + + it('returns ok-value for unwrapOrThrow', () => { + const res = Result.ok(42); + expect(res.unwrapOrThrow()).toBe(42); + }); + + it('throws error for unwrapOrThrow on error result', () => { + const res = Result.err('oops'); + expect(() => res.unwrapOrThrow()).toThrow('oops'); + }); }); describe('Transforming', () => { @@ -140,6 +150,36 @@ describe('util/result', () => { ); }); }); + + describe('Catch', () => { + it('bypasses ok result', () => { + const res = Result.ok(42); + expect(res.catch(() => Result.ok(0))).toEqual(Result.ok(42)); + expect(res.catch(() => Result.ok(0))).toBe(res); + }); + + it('bypasses uncaught transform errors', () => { + const res = Result.ok(42).transform(() => { + throw 'oops'; + }); + expect(res.catch(() => Result.ok(0))).toEqual(Result._uncaught('oops')); + expect(res.catch(() => Result.ok(0))).toBe(res); + }); + + it('converts error to Result', () => { + const result = Result.err<string>('oops').catch(() => + Result.ok<number>(42) + ); + expect(result).toEqual(Result.ok(42)); + }); + + it('handles error thrown in catch function', () => { + const result = Result.err<string>('oops').catch(() => { + throw 'oops'; + }); + expect(result).toEqual(Result._uncaught('oops')); + }); + }); }); describe('AsyncResult', () => { @@ -222,6 +262,16 @@ describe('util/result', () => { const res = Result.wrap(Promise.reject('oops')); await expect(res.unwrap(42)).resolves.toBe(42); }); + + it('returns ok-value for unwrapOrThrow', async () => { + const res = Result.wrap(Promise.resolve(42)); + await expect(res.unwrapOrThrow()).resolves.toBe(42); + }); + + it('rejects for error for unwrapOrThrow', async () => { + const res = Result.wrap(Promise.reject('oops')); + await expect(res.unwrapOrThrow()).rejects.toBe('oops'); + }); }); describe('Transforming', () => { @@ -367,5 +417,33 @@ describe('util/result', () => { expect(res).toEqual(Result.ok('F-O-O')); }); }); + + describe('Catch', () => { + it('converts error to AsyncResult', async () => { + const result = await Result.err<string>('oops').catch(() => + AsyncResult.ok(42) + ); + expect(result).toEqual(Result.ok(42)); + }); + + it('converts error to Promise', async () => { + const fallback = Promise.resolve(Result.ok(42)); + const result = await Result.err<string>('oops').catch(() => fallback); + expect(result).toEqual(Result.ok(42)); + }); + + it('handles error thrown in Promise result', async () => { + const fallback = Promise.reject('oops'); + const result = await Result.err<string>('oops').catch(() => fallback); + expect(result).toEqual(Result._uncaught('oops')); + }); + + it('converts AsyncResult error to Result', async () => { + const result = await AsyncResult.err<string>('oops').catch(() => + AsyncResult.ok<number>(42) + ); + expect(result).toEqual(Result.ok(42)); + }); + }); }); }); diff --git a/lib/util/result.ts b/lib/util/result.ts index 1069dc7858..19f31e1704 100644 --- a/lib/util/result.ts +++ b/lib/util/result.ts @@ -226,6 +226,17 @@ export class Result<T, E = Error> { return this.res; } + /** + * Returns the ok-value or throw the error. + */ + unwrapOrThrow(): NonNullable<T> { + if (this.res.ok) { + return this.res.val; + } + + throw this.res.err; + } + /** * Transforms the ok-value, sync or async way. * @@ -301,6 +312,48 @@ export class Result<T, E = Error> { return Result._uncaught(err); } } + + catch<U = T, EE = E>( + fn: (err: NonNullable<E>) => Result<U, E | EE> + ): Result<T | U, E | EE>; + catch<U = T, EE = E>( + fn: (err: NonNullable<E>) => AsyncResult<U, E | EE> + ): AsyncResult<T | U, E | EE>; + catch<U = T, EE = E>( + fn: (err: NonNullable<E>) => Promise<Result<U, E | EE>> + ): AsyncResult<T | U, E | EE>; + catch<U = T, EE = E>( + fn: ( + err: NonNullable<E> + ) => Result<U, E | EE> | AsyncResult<U, E | EE> | Promise<Result<U, E | EE>> + ): Result<T | U, E | EE> | AsyncResult<T | U, E | EE> { + if (this.res.ok) { + return this; + } + + if (this.res._uncaught) { + return this; + } + + try { + const result = fn(this.res.err); + + if (result instanceof Promise) { + return AsyncResult.wrap(result, (err) => { + logger.warn( + { err }, + 'Result: unexpected error in async catch handler' + ); + return Result._uncaught(err); + }); + } + + return result; + } catch (err) { + logger.warn({ err }, 'Result: unexpected error in catch handler'); + return Result._uncaught(err); + } + } } /** @@ -401,6 +454,14 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { : this.asyncResult.then<NonNullable<T>>((res) => res.unwrap(fallback)); } + /** + * Returns the ok-value or throw the error. + */ + async unwrapOrThrow(): Promise<NonNullable<T>> { + const result = await this.asyncResult; + return result.unwrapOrThrow(); + } + /** * Transforms the ok-value, sync or async way. * @@ -484,4 +545,24 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> { }) ); } + + catch<U = T, EE = E>( + fn: (err: NonNullable<E>) => Result<U, E | EE> + ): AsyncResult<T | U, E | EE>; + catch<U = T, EE = E>( + fn: (err: NonNullable<E>) => AsyncResult<U, E | EE> + ): AsyncResult<T | U, E | EE>; + catch<U = T, EE = E>( + fn: (err: NonNullable<E>) => Promise<Result<U, E | EE>> + ): AsyncResult<T | U, E | EE>; + catch<U = T, EE = E>( + fn: ( + err: NonNullable<E> + ) => Result<U, E | EE> | AsyncResult<U, E | EE> | Promise<Result<U, E | EE>> + ): AsyncResult<T | U, E | EE> { + const caughtAsyncResult = this.asyncResult.then((result) => + result.catch(fn as never) + ); + return AsyncResult.wrap(caughtAsyncResult); + } } -- GitLab