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