diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..4a3b7e8 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +export * as result from './src/result' diff --git a/src/result.ts b/src/result.ts new file mode 100644 index 0000000..0dc798d --- /dev/null +++ b/src/result.ts @@ -0,0 +1,202 @@ +import logger from 'log' +import {Option} from './option' + +const log = logger('result') + +export class Result { + public constructor( + protected readonly _data: T | E, + private readonly _state: State, + ) {} + + // Constructors + static ok(data: T) : Result { + log.trace('New Ok') + return new Result(data, State.OK) + } + static error(err: E) : Result { + log.trace('New Error') + log.debug(`Error : ${err}`) + return new Result(err, State.ERROR) + } + static fromObject(res: Result) : Result { + // @ts-expect-error + return new Result(res._data, res._state) + } + public as_error() : Result { + return Result.error(this.unwrap_err()) + } + + // Getters + public state() : State { + return this._state + } + + // TODO : implements Iterable<...> + + // ***************************************************************************************************************** + // Ok + public is_ok() : boolean { + return this._state === State.OK + } + public is_ok_and(f: (data: T) => boolean) : boolean { + return this._state === State.OK && f(this._data as T) + } + public expect(msg: string) : T { + log.trace('expect') + + if (this._state === State.ERROR) { + log.error('unwrapped an Error') + log.error(msg) + log.debug(`Error : ${this._data}`) + throw msg + } + + return this._data as T + } + public unwrap() : T { + log.trace('unwrap') + + if (this._state === State.ERROR) { + log.error("Unwrapped an Error") + log.debug(`Error : ${this._data}`) + throw "Unwrapped an Error" + } + + return this._data as T + } + public ok() : Option { + if (this._state === State.OK) { + return Option.some(this._data as T) + } else { + return Option.none() + } + } + public map(f: (data: T) => U) : Result { + if (this._state === State.OK) { + return Result.ok(f(this._data as T)) + } else { + return Result.error(this._data as E) + } + } + public map_or(default_: U, f: (data: T) => U) : U { + if (this._state === State.OK) { + return f(this._data as T) + } else { + return default_ + } + } + public map_or_else(fe: (err: E) => U, f: (data: T) => U) : U { + if (this._state === State.OK) { + return f(this._data as T) + } else { + return fe(this._data as E) + } + } + public inspect(f: (data: T) => void) : Result { + if (this._state === State.OK) { + f(this._data as T) + } + return this + } + public unwrap_or(default_: T) : T { + if (this._state === State.OK) { + return this._data as T + } else { + return default_ + } + } + public unwrap_or_else(f: (err: E) => T) : T { + if (this._state === State.OK) { + return this._data as T + } else { + return f(this._data as E) + } + } + + // ***************************************************************************************************************** + // Errors + public is_err() : boolean { + return this._state === State.ERROR + } + public is_err_and(f: (err: E) => boolean) : boolean { + return this._state === State.ERROR && f(this._data as E) + } + public expect_err(msg: string) : E { + log.trace('expect_err') + + if (this._state === State.OK) { + log.error('expect_err on Ok') + log.error(msg) + throw msg + } + + return this._data as E + } + public unwrap_err() : E { + log.trace('unwrap_err') + + if (this._state === State.OK) { + log.error('unwrap_err on Ok') + throw "Result wasn't in error" + } + + return this._data as E + } + public err() : Option { + if (this._state === State.ERROR) { + return Option.some(this._data as E) + } else { + return Option.none() + } + } + public map_err(f: (err: E) => F) : Result { + if (this._state === State.ERROR) { + return Result.error(f(this._data as E)) + } else { + return Result.ok(this._data as T) + } + } + public inspect_err(f: (err: E) => void) : Result { + if (this._state === State.ERROR) { + f(this._data as E) + } + return this + } + + // ***************************************************************************************************************** + // Combine + public and(res: Result) : Result { + if (this._state === State.OK) { + return res + } else { + return Result.error(this._data as E) + } + } + public and_then(f: (data: T) => Result) : Result { + if (this._state === State.OK) { + return f(this._data as T) + } else { + return Result.error(this._data as E) + } + } + public or(res: Result) : Result { + if (this._state === State.OK) { + return new Result(this._data as T, State.OK) + } else { + return res + } + } + public or_else(f: (err: E) => Result) : Result { + if (this._state === State.OK) { + return Result.ok(this._data as T) + } else { + return f(this._data as E) + } + } +} + +export const enum State { + OK, + ERROR +} diff --git a/test/result.test.ts b/test/result.test.ts new file mode 100644 index 0000000..8670b23 --- /dev/null +++ b/test/result.test.ts @@ -0,0 +1,89 @@ +import {expect, test} from 'bun:test' + +import {Result, State} from 'result' + +test('base case Ok', () => { + const value = 12 + + const res = Result.ok(value) + + expect(res.is_err()).toBeFalse() + expect(res.is_ok()).toBeTrue() + expect(res.unwrap()).toBe(value) + expect(res.expect("Shouldn't happen")).toBe(value) + expect(() => res.unwrap_err()).toThrow() + expect(() => res.expect_err("Error")).toThrow() + expect(res.state()).toBe(State.OK) + expect(() => res.as_error()).toThrow() + + expect(res.is_ok_and(v => v === value)).toBeTrue() + expect(res.map(v => v + 1).unwrap()).toBe(value + 1) + expect(res.ok().unwrap()).toBe(value) + expect(res.map_or(0, v => v + 1)).toBe(value + 1) + expect(res.map_or_else(() => 0, v => v + 1)).toBe(value + 1) + + const handler = (v: number) => expect(v).toBe(value) + res.inspect(handler) + + expect(res.unwrap_or(0)).toBe(value) + expect(res.unwrap_or_else(() => 0)).toBe(value) + + expect(res.is_err_and(() => true)).toBeFalse() + expect(res.err().is_none()).toBeTrue() + + const handler_err = (_v: never) => { + throw "Shouldn't happen" + } + expect(res.map_err(handler_err).unwrap()).toBe(value) + expect(res.inspect_err(handler_err).unwrap()).toBe(value) + + const value2 = value + 1 + const res2 = Result.ok(value2) + expect(res.and(res2).unwrap()).toBe(value2) + expect(res.and_then(v => Result.ok(v + 2)).unwrap()).toBe(value + 2) + expect(res.or(res2).unwrap()).toBe(value) + expect(res.or_else(handler_err).unwrap()).toBe(value) +}) + +test('base case Error', () => { + const value = 12 + + const res = Result.error(value) + + expect(res.is_err()).toBeTrue() + expect(res.is_ok()).toBeFalse() + expect(res.unwrap_err()).toBe(value) + expect(res.expect_err("Shouldn't happen")).toBe(value) + expect(() => res.unwrap()).toThrow() + expect(() => res.expect("Error")).toThrow() + expect(res.state()).toBe(State.ERROR) + expect(res.as_error().unwrap_err()).toBe(value) + + const handle_throw = (_v: string) => { + throw "Shouldn't happen" + } + expect(res.is_ok_and(handle_throw)).toBeFalse() + expect(res.map(handle_throw).unwrap_err()).toBe(value) + expect(res.err().unwrap()).toBe(value) + expect(res.map_or(0, handle_throw)).toBe(0) + expect(res.map_or_else(() => 0, handle_throw)).toBe(0) + + const handler = (v: number) => expect(v).toBe(value) + res.inspect_err(handler) + + expect(res.unwrap_or("yes")).toBe("yes") + expect(res.unwrap_or_else(() => "yes")).toBe("yes") + + expect(res.is_err_and(() => true)).toBeTrue() + expect(res.ok().is_none()).toBeTrue() + + expect(res.map_err(v => v + 1).unwrap_err()).toBe(value + 1) + res.inspect_err(v => expect(v).toBe(value)) + + const value2 = "yeet" + const res2 = Result.ok(value2) + expect(res.and(res2).unwrap_err()).toBe(value) + expect(res.and_then(handle_throw).unwrap_err()).toBe(value) + expect(res.or(res2).unwrap()).toBe(value2) + expect(res.or_else(v => Result.ok(`${v}`)).unwrap()).toBe(`${value}`) +})