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

feat: Support .catch method for Result #23505

Merged
merged 5 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
feat: Support .catch method for Result
  • Loading branch information
zharinov committed Jul 21, 2023
commit 74c212c1679c0a6017259eb6f3a3dbc106879b27
78 changes: 78 additions & 0 deletions lib/util/result.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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));
});
});
});
});
81 changes: 81 additions & 0 deletions lib/util/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,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.
*
Expand Down Expand Up @@ -302,6 +313,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);
}
}
}

/**
Expand Down Expand Up @@ -402,6 +455,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.
*
Expand Down Expand Up @@ -485,4 +546,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);
}
}