diff --git a/lib/util/result.spec.ts b/lib/util/result.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..74d79451747e3da94338651e6397e6f4dafb5661 --- /dev/null +++ b/lib/util/result.spec.ts @@ -0,0 +1,119 @@ +import { Result } from './result'; + +describe('util/result', () => { + describe('ok', () => { + it('constructs successful result from value', () => { + expect(Result.ok(42).value()).toBe(42); + }); + }); + + describe('err', () => { + it('constructs error result', () => { + const res = Result.err(); + expect(res.error()).toEqual(new Error()); + }); + + it('constructs error result from string', () => { + const res = Result.err('oops'); + expect(res.error()?.message).toBe('oops'); + }); + + it('constructs error result from Error instance', () => { + const err = new Error('oops'); + const res = Result.err(err); + expect(res.error()).toBe(err); + }); + }); + + describe('wrap', () => { + it('wraps function returning successful result', () => { + const res = Result.wrap(() => 42); + expect(res.value()).toBe(42); + }); + + it('wraps function that throws an error', () => { + const res = Result.wrap(() => { + throw new Error('oops'); + }); + expect(res.error()?.message).toBe('oops'); + }); + + it('wraps promise resolving to value', async () => { + const res = await Result.wrap(Promise.resolve(42)); + expect(res.value()).toBe(42); + }); + + it('wraps promise rejecting with error', async () => { + const err = new Error('oops'); + const res = await Result.wrap(Promise.reject(err)); + expect(res.error()?.message).toBe('oops'); + }); + }); + + describe('transform', () => { + const fn = (x: string) => x.toUpperCase(); + + it('transforms successful result', () => { + const res = Result.ok('foo').transform(fn); + expect(res).toEqual(Result.ok('FOO')); + }); + + it('no-op for error result', () => { + const err = new Error('bar'); + const res = Result.err(err).transform(fn); + expect(res.value()).toBeUndefined(); + expect(res.error()).toBe(err); + }); + }); + + describe('unwrap', () => { + it('unwraps successful result', () => { + const res = Result.ok(42); + expect(res.unwrap()).toEqual({ ok: true, value: 42 }); + }); + + it('unwraps error result with fallback value', () => { + const err = new Error('oops'); + const res = Result.err(err); + expect(res.unwrap(42)).toEqual({ ok: true, value: 42 }); + }); + + it('unwraps error result', () => { + const err = new Error('oops'); + const res = Result.err(err); + expect(res.unwrap()).toEqual({ ok: false, error: err }); + }); + }); + + describe('value', () => { + it('returns successful value', () => { + const res = Result.ok(42); + expect(res.value()).toBe(42); + }); + + it('returns fallback value for error result', () => { + const err = new Error('oops'); + const res = Result.err(err); + expect(res.value(42)).toBe(42); + }); + + it('returns undefined value for error result', () => { + const err = new Error('oops'); + const res = Result.err(err); + expect(res.value()).toBeUndefined(); + }); + }); + + describe('error', () => { + it('returns undefined error for successful result', () => { + const res = Result.ok(42); + expect(res.error()).toBeUndefined(); + }); + + it('returns error for non-successful result', () => { + const err = new Error('oops'); + const res = Result.err(err); + expect(res.error()).toEqual(err); + }); + }); +}); diff --git a/lib/util/result.ts b/lib/util/result.ts new file mode 100644 index 0000000000000000000000000000000000000000..ae6dcf917a0c7ca86500a3b8504d7ea350fb0bd5 --- /dev/null +++ b/lib/util/result.ts @@ -0,0 +1,90 @@ +interface Ok<T> { + ok: true; + value: T; +} + +interface Err { + ok: false; + error: Error; +} + +type Res<T> = Ok<T> | Err; + +export class Result<T> { + static ok<T>(value: T): Result<T> { + return new Result({ ok: true, value }); + } + + static err(): Result<never>; + static err(error: Error): Result<never>; + static err(message: string): Result<never>; + static err(error?: Error | string): Result<never> { + if (typeof error === 'undefined') { + return new Result({ ok: false, error: new Error() }); + } + + if (typeof error === 'string') { + return new Result({ ok: false, error: new Error(error) }); + } + + return new Result({ ok: false, error }); + } + + private static wrapCallback<T>(callback: () => T): Result<T> { + try { + return Result.ok(callback()); + } catch (error) { + return Result.err(error); + } + } + + private static wrapPromise<T>(promise: Promise<T>): Promise<Result<T>> { + return promise.then( + (value) => Result.ok(value), + (error) => Result.err(error) + ); + } + + static wrap<T>(callback: () => T): Result<T>; + static wrap<T>(promise: Promise<T>): Promise<Result<T>>; + static wrap<T>( + input: (() => T) | Promise<T> + ): Result<T> | Promise<Result<T>> { + return input instanceof Promise + ? Result.wrapPromise(input) + : Result.wrapCallback(input); + } + + private constructor(private res: Res<T>) {} + + transform<U>(fn: (value: T) => U): Result<U> { + return this.res.ok + ? Result.ok(fn(this.res.value)) + : Result.err(this.res.error); + } + + unwrap(): Res<T>; + unwrap<U>(fallback: U): Res<T | U>; + unwrap<U>(fallback?: U): Res<T | U> { + if (this.res.ok) { + return this.res; + } + + if (arguments.length) { + return { ok: true, value: fallback as U }; + } + + return this.res; + } + + value(): T | undefined; + value<U>(fallback: U): T | U; + value<U>(fallback?: U): T | U | undefined { + const res = arguments.length ? this.unwrap(fallback as U) : this.unwrap(); + return res.ok ? res.value : undefined; + } + + error(): Error | undefined { + return this.res.ok ? undefined : this.res.error; + } +}