-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add option support and sandboxing
- Loading branch information
Showing
13 changed files
with
642 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,3 +15,4 @@ | |
|
||
dist | ||
node_modules | ||
coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,14 +3,7 @@ | |
"type": "module", | ||
"version": "0.3.0", | ||
"description": "Type safe result utilities for TypeScript", | ||
"keywords": [ | ||
"result", | ||
"types", | ||
"error", | ||
"throw", | ||
"function", | ||
"immutable" | ||
], | ||
"keywords": ["result", "types", "error", "throw", "function", "immutable"], | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
|
@@ -25,16 +18,16 @@ | |
}, | ||
"scripts": { | ||
"test": "vitest run", | ||
"coverage": "vitest run --coverage", | ||
"build": "tsc" | ||
}, | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"files": [ | ||
"/dist" | ||
], | ||
"files": ["/dist"], | ||
"packageManager": "[email protected]", | ||
"devDependencies": { | ||
"@biomejs/biome": "1.8.3", | ||
"@vitest/coverage-v8": "^1.6.0", | ||
"typescript": "^5.5.2", | ||
"vitest": "^1.6.0" | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export class UnwrapError<E> extends Error { | ||
readonly originalError: E; | ||
|
||
constructor(error: E) { | ||
super("unwrap error"); | ||
|
||
this.originalError = error; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
/* v8 ignore next 2 */ | ||
export * from "./result.js"; | ||
export * from "./option.js"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { expect, test } from "vitest"; | ||
import { kOptionKind } from "./symbols.js"; | ||
import { isNone, isOption, isSome, none, some } from "./option.js"; | ||
|
||
test("creates an immutable option", () => { | ||
const res = some("hello"); | ||
|
||
expect(res).toEqual({ data: "hello", [kOptionKind]: "some" }); | ||
|
||
expect(() => { | ||
/* @ts-expect-error */ | ||
res.data = "goodbye"; | ||
}).toThrowError(/^Cannot assign to read only property 'data'*/i); | ||
}); | ||
|
||
test("creates an immutable none option", () => { | ||
const res = none(); | ||
|
||
expect(res).toEqual({ [kOptionKind]: "none" }); | ||
}); | ||
|
||
test("isSome returns true if the option contains data", () => { | ||
expect(isSome(some("hello"))).toBeTruthy(); | ||
expect(isSome(none())).toBeFalsy(); | ||
}); | ||
|
||
test("isNone returns true if the option does not contain data", () => { | ||
expect(isNone(none())).toBeTruthy(); | ||
expect(isNone(some("hello"))).toBeFalsy(); | ||
}); | ||
|
||
test("isOption returns true if the input is an option", () => { | ||
expect(isOption(some("hello"))).toBeTruthy(); | ||
expect(isOption(none())).toBeTruthy(); | ||
expect(isOption(123)).toBeFalsy(); | ||
expect(isOption({})).toBeFalsy(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import { kOptionKind } from "./symbols.js"; | ||
|
||
const KIND_SOME = "some" as const; | ||
const KIND_NONE = "none" as const; | ||
|
||
export type Some<T> = { | ||
readonly data: T; | ||
readonly [kOptionKind]: typeof KIND_SOME; | ||
}; | ||
|
||
export type None = { | ||
readonly [kOptionKind]: typeof KIND_NONE; | ||
}; | ||
|
||
export type Option<T> = Some<T> | None; | ||
|
||
const NONE: None = Object.freeze({ [kOptionKind]: KIND_NONE }); | ||
|
||
export function some<T>(data: T): Some<T> { | ||
return Object.freeze({ data, [kOptionKind]: KIND_SOME }); | ||
} | ||
|
||
export function none(): None { | ||
return NONE; | ||
} | ||
|
||
export function isSome<T>(maybeSome: Option<T>): maybeSome is Some<T> { | ||
return maybeSome[kOptionKind] === KIND_SOME; | ||
} | ||
|
||
export function isNone<T>(maybeNone: Option<T>): maybeNone is None { | ||
return maybeNone[kOptionKind] === KIND_NONE; | ||
} | ||
|
||
export function isOption<T = unknown>( | ||
maybeOption: unknown | ||
): maybeOption is Option<T> { | ||
return ( | ||
!!maybeOption && | ||
typeof maybeOption === "object" && | ||
kOptionKind in maybeOption | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,35 @@ | ||
export type OkResult<T> = { readonly ok: T }; | ||
export type ErrResult<E> = { readonly err: E }; | ||
export type Result<T, E> = OkResult<T> | ErrResult<E>; | ||
|
||
export const ok = <T>(data: T): OkResult<T> => Object.freeze({ ok: data }); | ||
export const err = <E>(error: E): ErrResult<E> => Object.freeze({ err: error }); | ||
|
||
export const isOk = <T, E>(maybeOk: Result<T, E>): maybeOk is OkResult<T> => | ||
"ok" in maybeOk; | ||
export const isErr = <T, E>(maybeErr: Result<T, E>): maybeErr is ErrResult<E> => | ||
"err" in maybeErr; | ||
|
||
export const unwrap = <T, E>(res: Result<T, E>): T | never => { | ||
if (isErr(res)) { | ||
throw new TypeError("failed to unwrap result"); | ||
} | ||
import { kResultKind } from "./symbols.js"; | ||
|
||
return res.ok; | ||
}; | ||
const KIND_OK = "ok"; | ||
const KIND_ERR = "err"; | ||
|
||
export const unwrapOr = <T, E, O extends T>( | ||
res: Result<T, E>, | ||
fallback: O | ||
): T | O => { | ||
if (isErr(res)) { | ||
return fallback; | ||
} | ||
export type OkResult<T> = { readonly data: T; [kResultKind]: typeof KIND_OK }; | ||
export type ErrResult<E> = { readonly err: E; [kResultKind]: typeof KIND_ERR }; | ||
|
||
return res.ok; | ||
}; | ||
|
||
export type MapFn<T, R> = (item: T) => R; | ||
|
||
export const map = <T, E, R>(res: Result<T, E>, fn: MapFn<T, R>): R | never => | ||
fn(unwrap(res)); | ||
|
||
export const mapErr = <T, E, R>( | ||
res: Result<T, E>, | ||
fn: MapFn<E, R> | ||
): R | never => { | ||
if (isOk(res)) { | ||
throw new TypeError("cannot error map an ok result"); | ||
} | ||
export type Result<T, E> = OkResult<T> | ErrResult<E>; | ||
|
||
return fn(res.err); | ||
}; | ||
export function ok<T>(data: T): OkResult<T> { | ||
return Object.freeze({ data, [kResultKind]: KIND_OK }); | ||
} | ||
|
||
export function err<E>(err: E): ErrResult<E> { | ||
return Object.freeze({ err, [kResultKind]: KIND_ERR }); | ||
} | ||
|
||
export function isOk<T, E>(maybeOk: Result<T, E>): maybeOk is OkResult<T> { | ||
return maybeOk[kResultKind] === KIND_OK; | ||
} | ||
|
||
export function isErr<T, E>(maybeErr: Result<T, E>): maybeErr is ErrResult<E> { | ||
return maybeErr[kResultKind] === KIND_ERR; | ||
} | ||
|
||
export function isResult<T = unknown, E = unknown>( | ||
maybeResult: unknown | ||
): maybeResult is Result<T, E> { | ||
return ( | ||
!!maybeResult && | ||
typeof maybeResult === "object" && | ||
kResultKind in maybeResult | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const kResultKind = Symbol.for("resultKind"); | ||
export const kOptionKind = Symbol.for("optionKind"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { expect, test } from "vitest"; | ||
import { map, mapErr, run, unwrap, unwrapOr } from "./utils.js"; | ||
import { err, isErr, isOk, ok, type Result } from "./result.js"; | ||
import { UnwrapError } from "./error.js"; | ||
import { none, type Option, some } from "./option.js"; | ||
|
||
test("should unwrap a success result", () => { | ||
expect(unwrap(ok("hello"))).toStrictEqual("hello"); | ||
}); | ||
|
||
test("should throw if we try to unwrap an error", () => { | ||
expect(() => { | ||
unwrap(err("hello")); | ||
}).toThrowError(UnwrapError); | ||
}); | ||
|
||
test("should return a fallback if an error is unwrapped", () => { | ||
expect(unwrapOr(err("badcode"), "hello")).toStrictEqual("hello"); | ||
}); | ||
|
||
test("should return a fallback if none is unwrapped", () => { | ||
expect(unwrapOr(none(), "hello")).toStrictEqual("hello"); | ||
}); | ||
|
||
test("unwrapOr should return a original data", () => { | ||
expect(unwrapOr(some("hi"), "hello")).toStrictEqual("hi"); | ||
}); | ||
|
||
test("it should map a result", () => { | ||
expect(map(ok("hello"), (str) => str.toUpperCase())).toEqual(ok("HELLO")); | ||
}); | ||
|
||
test("mapError should map an error result", () => { | ||
expect(mapErr(err("badcode"), (str) => str.replace("bad", "good"))).toEqual( | ||
err("goodcode") | ||
); | ||
}); | ||
|
||
test("mapError should return the data of an ok result", () => { | ||
expect( | ||
mapErr(ok("badcode") as Result<string, string>, (str) => | ||
str.replace("bad", "good") | ||
) | ||
).toEqual(ok("badcode")); | ||
}); | ||
|
||
test("it should map an option", () => { | ||
expect(map(some("hello"), (str) => str.toUpperCase())).toEqual(some("HELLO")); | ||
}); | ||
|
||
test("it should map an none option", () => { | ||
expect(map(none() as Option<string>, (str) => str.toUpperCase())).toEqual( | ||
none() | ||
); | ||
}); | ||
|
||
test("run should sandbox an execution", () => { | ||
const res = run(() => { | ||
return ok("hello"); | ||
}); | ||
|
||
expect(isOk(res)).toBeTruthy(); | ||
expect(unwrap(res)).toStrictEqual("hello"); | ||
}); | ||
|
||
test("run should sandbox an async execution", async () => { | ||
const res = await run(async () => { | ||
return ok("hello"); | ||
}); | ||
|
||
expect(isOk(res)).toBeTruthy(); | ||
expect(unwrap(res)).toStrictEqual("hello"); | ||
}); | ||
|
||
test("run should return an error result if an unwrap error happens inside", () => { | ||
const res = run(() => { | ||
unwrap(err("fail")); | ||
|
||
return ok("hello"); | ||
}); | ||
|
||
expect(isErr(res)).toBeTruthy(); | ||
}); | ||
|
||
test("run throws with non-unwrap errors", () => { | ||
expect(() => { | ||
run(() => { | ||
throw new Error("test"); | ||
|
||
// biome-ignore lint: test purposes | ||
return ok("hello"); | ||
}); | ||
}).toThrow(); | ||
}); |
Oops, something went wrong.