From 0919773137c0a7cf9c6270b5ed5bc34cf4b1ccb5 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Thu, 6 Jul 2023 23:42:03 +0900 Subject: [PATCH 01/25] feat(mod): remove all API BREAKING CHANGE: remove all API --- mod.ts | 10 ---- results.ts | 40 ---------------- results_test.ts | 16 ------- types.ts | 41 ----------------- utils.ts | 95 -------------------------------------- utils_test.ts | 118 ------------------------------------------------ 6 files changed, 320 deletions(-) delete mode 100644 results.ts delete mode 100644 results_test.ts delete mode 100644 types.ts delete mode 100644 utils.ts delete mode 100644 utils_test.ts diff --git a/mod.ts b/mod.ts index 4c0a7c4..3b5a399 100644 --- a/mod.ts +++ b/mod.ts @@ -1,12 +1,2 @@ // Copyright 2022-latest Tomoki Miyauchi. All rights reserved. MIT license. // This module is browser compatible. - -export { Result } from "./results.ts"; -export { - type Container, - type ErrContainer, - type OkContainer, - type ResultConstructor, - type ResultContainer, -} from "./types.ts"; -export { isErr, isOk, match, type Patterns, unsafe } from "./utils.ts"; diff --git a/results.ts b/results.ts deleted file mode 100644 index 235d06f..0000000 --- a/results.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2022-latest Tomoki Miyauchi. All rights reserved. MIT license. -// This module is browser compatible. - -import { - ErrContainer, - OkContainer, - ResultConstructor, - ResultContainer, -} from "./types.ts"; - -/** {@link Result} is a type that represents either success ({@link OkContainer}) or failure ({@link ErrContainer}). */ -export type Result = ResultContainer; - -/** Represents either success or failure result. - * - * @example - * ```ts - * import { Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; - * const ok = Result.ok("any success value"); - * const err = Result.err("any failure value"); - * ``` - */ -export const Result: ResultConstructor = { - ok: (value) => new Ok(value), - err: (value) => new Err(value), -}; - -/** Contains the success value. */ -class Ok implements OkContainer { - constructor(public value: T) {} - - type = "ok" as const; -} - -/** Contains the failure value. */ -class Err implements ErrContainer { - constructor(public value: E) {} - - type = "err" as const; -} diff --git a/results_test.ts b/results_test.ts deleted file mode 100644 index 6e42f11..0000000 --- a/results_test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Result } from "./results.ts"; -import { assertEquals, describe, it } from "./dev_deps.ts"; - -describe("Result", () => { - it("should return Ok container", () => { - const result = Result.ok("test"); - assertEquals(result.value, "test"); - assertEquals(result.type, "ok"); - }); - - it("should return Err container", () => { - const result = Result.err("test"); - assertEquals(result.value, "test"); - assertEquals(result.type, "err"); - }); -}); diff --git a/types.ts b/types.ts deleted file mode 100644 index 32b8c1c..0000000 --- a/types.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2022-latest Tomoki Miyauchi. All rights reserved. MIT license. -// This module is browser compatible. - -/** Container API. */ -export interface Container { - /** The contained value. */ - value: unknown; - - /** The container type. */ - type: string; -} - -/** Ok container API. */ -export interface OkContainer extends Container { - /** Ok container value. */ - value: T; - - /** Ok container type. */ - type: "ok"; -} - -/** Err container API. */ -export interface ErrContainer extends Container { - /** Err container value. */ - value: E; - - /** Err container type. */ - type: "err"; -} - -/** Result container API. */ -export type ResultContainer = OkContainer | ErrContainer; - -/** Result constructor. */ -export interface ResultConstructor { - /** Create a new {@link OkContainer}. */ - ok: (value: T) => OkContainer; - - /** Create a new {@link ErrContainer}. */ - err: (value: E) => ErrContainer; -} diff --git a/utils.ts b/utils.ts deleted file mode 100644 index 3b7288f..0000000 --- a/utils.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2022-latest Tomoki Miyauchi. All rights reserved. MIT license. -// This module is browser compatible. - -import { Result } from "./results.ts"; -import { ErrContainer, OkContainer, ResultContainer } from "./types.ts"; - -/** Whether the {@link ResultContainer } is {@link OkContainer} or not. - * - * @example - * ```ts - * import { isOk, Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; - * import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - * - * assertEquals(isOk(Result.ok("OK!!")), true); - * assertEquals(isOk(Result.err("Error!!")), false); - * ``` - */ -export function isOk( - resultContainer: ResultContainer, -): resultContainer is OkContainer { - return resultContainer.type === "ok"; -} - -/** Whether the {@link ResultContainer } is {@link ErrContainer} or not. - * - * @example - * ```ts - * import { isErr, Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; - * import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - * - * assertEquals(isErr(Result.err("Error!!")), true); - * assertEquals(isErr(Result.ok("OK!!")), false); - * ``` - */ -export function isErr( - resultContainer: ResultContainer, -): resultContainer is ErrContainer { - return resultContainer.type === "err"; -} - -/** Wrap code that may throw errors in a container. - * - * @example - * ```ts - * import { unsafe } from "https://deno.land/x/result_js@$VERSION/mod.ts"; - * import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - * - * const result = unsafe(() => { - * throw Error("Dangerous!!"); - * }); - * assertEquals(result.value, Error()); - * assertEquals(result.type, "err"); - * ``` - */ -export function unsafe( - fn: () => T, -): ResultContainer { - try { - return Result.ok(fn()); - } catch (e) { - return Result.err(e); - } -} - -/** Matching patterns. */ -export interface Patterns { - /** Match if the result is {@link OkContainer}. */ - ok: (value: T) => U; - - /** Match if the result is {@link ErrContainer}. */ - err: (value: E) => U; -} - -/** Pattern matching for {@link Result}. - * - * @example - * ```ts - * import { match, Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; - * import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - * - * const value = match(Result.ok("Tom"), { - * ok: (value) => "Hello " + value, - * err: (value) => "Goodby " + value, - * }); - * assertEquals(value, "Hello Tom"); - * ``` - */ -export function match( - resultContainer: ResultContainer, - patterns: Patterns, -): U { - return isOk(resultContainer) - ? patterns.ok(resultContainer.value) - : patterns.err(resultContainer.value); -} diff --git a/utils_test.ts b/utils_test.ts deleted file mode 100644 index 1f7d51b..0000000 --- a/utils_test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Result } from "./results.ts"; -import { isErr, isOk, match, unsafe } from "./utils.ts"; -import { - assertEquals, - assertSpyCall, - assertSpyCalls, - describe, - it, - spy, -} from "./dev_deps.ts"; - -describe("unsafe", () => { - it("should return err container when the function throw error", () => { - const result = unsafe(() => { - throw Error(); - }); - assertEquals(result.value, Error()); - assertEquals(result.type, "err"); - }); - - it("should return err container when the function throw any error", () => { - const result = unsafe(() => - new Headers({ "?": "invalid field name" }) - ); - assertEquals(result.value, new TypeError()); - }); - - it("should return ok container when the function return", () => { - const result = unsafe(() => { - return 0; - }); - assertEquals(result.value, 0); - assertEquals(result.type, "ok"); - }); - - it("should return ok container when the function return undefined", () => { - const result = unsafe(() => {}); - assertEquals(result.value, undefined); - assertEquals(result.type, "ok"); - }); -}); - -describe("match", () => { - it("should call ok handler when the result is Ok container", () => { - const ok = spy(); - const err = spy(); - match({ type: "ok", value: "" }, { - ok, - err, - }); - - assertSpyCall(ok, 0, { - args: [""], - }); - assertSpyCalls(err, 0); - }); - - it("should call err handler when the result is Err container", () => { - const ok = spy(); - const err = spy(); - match({ type: "err", value: 0 }, { - ok, - err, - }); - - assertSpyCalls(ok, 0); - assertSpyCall(err, 0, { - args: [0], - }); - }); - - it("should return matched pattern return value", () => { - const value = match({ type: "err", value: 0 }, { - ok: (value) => value + 1, - err: (value) => value + 2, - }); - - assertEquals(value, 2); - }); - - it("should satisfy example", () => { - const value = match(Result.ok("Tom"), { - ok: (value) => "Hello " + value, - err: (value) => "Goodby " + value, - }); - assertEquals(value, "Hello Tom"); - }); -}); - -describe("isOk", () => { - it("should return true when the result container is Ok container", () => { - assertEquals(isOk({ type: "ok", value: undefined }), true); - }); - - it("should return false when the result container is not Ok container", () => { - assertEquals(isOk({ type: "err", value: undefined }), false); - }); - - it("should pass example", () => { - assertEquals(isOk(Result.ok("OK!!")), true); - assertEquals(isOk(Result.err("Error!!")), false); - }); -}); - -describe("isErr", () => { - it("should return true when the result container is Err container", () => { - assertEquals(isErr({ type: "err", value: undefined }), true); - }); - - it("should return false when the result container is not Err container", () => { - assertEquals(isErr({ type: "ok", value: undefined }), false); - }); - - it("should pass example", () => { - assertEquals(isErr(Result.err("Error!!")), true); - assertEquals(isErr(Result.ok("OK!!")), false); - }); -}); From 1dfcc9a4c6756c8d4fa1e93bad0b87b4be012a30 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Thu, 6 Jul 2023 23:44:26 +0900 Subject: [PATCH 02/25] feat(spec): add specification interface and type constructor --- spec.ts | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 spec.ts diff --git a/spec.ts b/spec.ts new file mode 100644 index 0000000..22c294d --- /dev/null +++ b/spec.ts @@ -0,0 +1,82 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. +// This module is browser compatible. + +/** The result type. */ +export enum ResultType { + Ok, + Err, +} + +/** Common type. */ +interface Container { + /** {@linkcode Result} type. */ + get type(): ResultType; + + /** Return contained value. */ + get get(): unknown; +} + +/** The {@linkcode Err} API. */ +export interface Err extends Container { + get type(): ResultType.Err; + + get get(): E; +} + +/** The {@linkcode Ok} API. */ +export interface Ok extends Container { + get type(): ResultType.Ok; + + get get(): T; +} + +export interface OkConstructor { + /** {@linkcode Ok} value of type {@linkcode T} . */ + (value: T): Ok; +} + +export interface ErrConstructor { + /** {@linkcode Err} value of type {@linkcode E} . */ + (value: E): Err; +} + +/** {@linkcode Ok} constructor. + * + * @example + * ```ts + * import { Ok } from "https://deno.land/x/result_js/spec.ts"; + * const ok = Ok(0); + * ``` + */ +export const Ok: OkConstructor = function Ok(value: T): Ok { + return { + get type(): ResultType.Ok { + return ResultType.Ok; + }, + get get(): T { + return value; + }, + }; +}; + +/** {@linkcode Err} constructor. + * + * @example + * ```ts + * import { Err } from "https://deno.land/x/result_js/spec.ts"; + * const err = Err(0); + * ``` + */ +export const Err: ErrConstructor = function Err(value: E): Err { + return { + get type(): ResultType.Err { + return ResultType.Err; + }, + get get(): E { + return value; + }, + }; +}; + +/** Representation of {@linkcode Ok} or {@linkcode Err}. */ +export type Result = Ok | Err; From 4e429ec83b65fbc14ec753f00bee0c9f714d5d69 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Thu, 6 Jul 2023 23:45:30 +0900 Subject: [PATCH 03/25] chore: rename file --- dev_deps.ts => _dev_deps.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dev_deps.ts => _dev_deps.ts (100%) diff --git a/dev_deps.ts b/_dev_deps.ts similarity index 100% rename from dev_deps.ts rename to _dev_deps.ts From 8ced6fd3fa099a52fef2a99757a3a2097c377eae Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 00:10:28 +0900 Subject: [PATCH 04/25] feat(query): add query operators --- operators/query.ts | 36 ++++++++++++++++++++++++++++++++++++ operators/query_test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 operators/query.ts create mode 100644 operators/query_test.ts diff --git a/operators/query.ts b/operators/query.ts new file mode 100644 index 0000000..02dd37f --- /dev/null +++ b/operators/query.ts @@ -0,0 +1,36 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. +// This module is browser compatible. + +import { type Err, type Ok, type Result, ResultType } from "../spec.ts"; + +/** Returns `true` if the {@linkcode result} is a {@linkcode Ok}. + * + * @example + * ```ts + * import { isOk } from "https://deno.land/x/result_js/operators/query.ts"; + * import { Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * + * const result: Result = Ok(1); + * assert(isOk(result)); + * ``` + */ +export function isOk(result: Result): result is Ok { + return result.type === ResultType.Ok; +} + +/** Returns `true` if the {@linkcode result} is a {@linkcode Err}. + * + * @example + * ```ts + * import { isErr } from "https://deno.land/x/result_js/operators/query.ts"; + * import { Err, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { assert } from "https://deno.land/std/testing/asserts.ts"; + * + * const result: Result = Err(0); + * assert(isErr(result)); + * ``` + */ +export function isErr(result: Result): result is Err { + return result.type === ResultType.Err; +} diff --git a/operators/query_test.ts b/operators/query_test.ts new file mode 100644 index 0000000..9e28e01 --- /dev/null +++ b/operators/query_test.ts @@ -0,0 +1,25 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. + +import { isErr, isOk } from "./query.ts"; +import { Err, Ok } from "../spec.ts"; +import { assert, assertFalse, describe, it } from "../_dev_deps.ts"; + +describe("isOk", () => { + it("should return true if it is Ok", () => { + assert(isOk(Ok(0))); + }); + + it("should return false if it is Err", () => { + assertFalse(isOk(Err(0))); + }); +}); + +describe("isErr", () => { + it("should return true if it is Err", () => { + assert(isErr(Err(0))); + }); + + it("should return false if it is Ok", () => { + assertFalse(isErr(Ok(0))); + }); +}); From a159d31654ef33c2e21a5b42b63e17968f26e3f4 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 00:37:19 +0900 Subject: [PATCH 05/25] feat(logical): add logical operators --- operators/logical.ts | 97 +++++++++++++++++++++++++++++++++++++++ operators/logical_test.ts | 74 +++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 operators/logical.ts create mode 100644 operators/logical_test.ts diff --git a/operators/logical.ts b/operators/logical.ts new file mode 100644 index 0000000..db3e513 --- /dev/null +++ b/operators/logical.ts @@ -0,0 +1,97 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. +// This module is browser compatible. + +import { isErr, isOk } from "./query.ts"; +import { type Result } from "../spec.ts"; + +/** Returns {@linkcode res} if the {@linkcode result} is {@linkcode Err}, otherwise returns the {@linkcode Ok}. + * + * @example + * ```ts + * import { or } from "https://deno.land/x/result_js/operators/logical.ts"; + * import { Err, Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const result: Result; + * + * assertEquals(or(Ok(0), result), Ok(0)); + * assertEquals(or(Err(1), result), result); + * ``` + */ +export function or( + result: Result, + res: Result, +): Result { + if (isErr(result)) return res; + + return result; +} + +/** Calls {@linkcode fn} if the {@linkcode result} is {@linkcode Err}, otherwise returns the {@linkcode Ok}. + * + * @example + * ```ts + * import { orElse } from "https://deno.land/x/result_js/operators/logical.ts"; + * import { Err, Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const square: (value: number) => Ok + * + * assertEquals(orElse(Ok(3), square), Ok(3)); + * assertEquals(orElse(Err(2), square), Ok(4)); + * ``` + */ +export function orElse( + result: Result, + fn: (value: E) => Result, +): Result { + if (isErr(result)) return fn(result.get); + + return result; +} + +/** Returns {@linkcode res} if the {@linkcode result} is {@linkcode Ok}, otherwise returns the {@linkcode Err}. + * + * @example + * ```ts + * import { and } from "https://deno.land/x/result_js/operators/logical.ts"; + * import { Err, Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const result: Result; + * + * assertEquals(and(Ok(0), result), result); + * assertEquals(and(Err(1), result), Err(1)); + * ``` + */ +export function and( + result: Result, + res: Result, +): Result { + if (isOk(result)) return res; + + return result; +} + +/** Calls {@linkcode fn} if the {@linkcode result} is {@linkcode Ok}, otherwise returns the {@linkcode Err}. + * + * @example + * ```ts + * import { andThen } from "https://deno.land/x/result_js/operators/logical.ts"; + * import { Err, Ok } from "https://deno.land/x/result_js/spec.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const square: (value: number) => Ok; + * + * assertEquals(andThen(Ok(3), square), Ok(9)); + * assertEquals(andThen(Err(1), square), Err(1)); + * ``` + */ +export function andThen( + result: Result, + fn: (value: T) => Result, +): Result { + if (isOk(result)) return fn(result.get); + + return result; +} diff --git a/operators/logical_test.ts b/operators/logical_test.ts new file mode 100644 index 0000000..1e3588b --- /dev/null +++ b/operators/logical_test.ts @@ -0,0 +1,74 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. + +import { and, andThen, or, orElse } from "./logical.ts"; +import { Err, Ok } from "../spec.ts"; +import { + assertEquals, + assertSpyCallArgs, + assertSpyCalls, + describe, + it, + spy, +} from "../_dev_deps.ts"; + +describe("or", () => { + it("should return res if it is Err, otherwise return Ok", () => { + assertEquals(or(Ok(0), Ok(1)), Ok(0)); + assertEquals(or(Ok(0), Err(1)), Ok(0)); + assertEquals(or(Err(1), Err(2)), Err(2)); + assertEquals(or(Err(1), Ok(0)), Ok(0)); + }); +}); + +describe("and", () => { + it("should return Err if it is Err, otherwise return res", () => { + assertEquals(and(Ok(0), Err(1)), Err(1)); + assertEquals(and(Ok(0), Ok(-0)), Ok(-0)); + assertEquals(and(Err(0), Err(1)), Err(0)); + assertEquals(and(Err(0), Ok(1)), Err(0)); + }); +}); + +describe("andThen", () => { + it("should return Ok and call fn if result is Ok", () => { + const fn = spy((v: number) => Ok(v ** 3)); + assertEquals(andThen(Ok(2), fn), Ok(8)); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [2]); + }); + + it("should return Err and call fn if result is Ok", () => { + const fn = spy((v: number) => Err(v ** 3)); + assertEquals(andThen(Ok(2), fn), Err(8)); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [2]); + }); + + it("should return Err and not call fn if result is Err", () => { + const fn = spy((v: number) => Err(v ** 3)); + assertEquals(andThen(Err(2), fn), Err(2)); + assertSpyCalls(fn, 0); + }); +}); + +describe("orElse", () => { + it("should return Err and call fn if result is Err", () => { + const fn = spy(() => Err(1)); + assertEquals(orElse(Err(0), fn), Err(1)); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [0]); + }); + + it("should return Ok and call fn if result is Err", () => { + const fn = spy(() => Ok(1)); + assertEquals(orElse(Err(0), fn), Ok(1)); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [0]); + }); + + it("should return Ok and not call fn if result is Ok", () => { + const fn = spy(() => Ok(1)); + assertEquals(orElse(Ok(0), fn), Ok(0)); + assertSpyCalls(fn, 0); + }); +}); From 9bb241bf639958d172a655a82dd9f247d2a58330 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 12:28:37 +0900 Subject: [PATCH 06/25] feat(transform): add transform operator --- operators/transform.ts | 72 ++++++++++++++++++++++++++++++++ operators/transform_test.ts | 82 +++++++++++++++++++++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 operators/transform.ts create mode 100644 operators/transform_test.ts diff --git a/operators/transform.ts b/operators/transform.ts new file mode 100644 index 0000000..fa29655 --- /dev/null +++ b/operators/transform.ts @@ -0,0 +1,72 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. +// This module is browser compatible. + +import { isErr, isOk } from "./query.ts"; +import { Ok, type Result } from "../spec.ts"; + +/** Maps a {@linkcode Result} to {@linkcode Result} by applying {@linkcode fn} to a contained {@linkcode Ok}, leaving an {@linkcode Err}. + * + * @example + * ```ts + * import { Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { map } from "https://deno.land/x/result_js/operators/transform.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * const result: Result = Ok("Hello, World!"); + * const resultLen = map(result, (v) => v.length); + * + * assertEquals(resultLen, Ok(13)); + * ``` + */ +export function map( + result: Result, + fn: (value: T) => U, +): Result { + if (isOk(result)) return Ok(fn(result.get)); + + return result; +} + +/** Returns the provided {@linkcode defaultValue} (if {@linkcode Err}), or applies {@linkcode fn} to the contained value (if {@linkcode Ok}), + * + * @example + * ```ts + * import { Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { mapOr } from "https://deno.land/x/result_js/operators/transform.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * const result: Result = Ok("Hello"); + * assertEquals(mapOr(result, 0, ({ length }) => length), 5); + * ``` + */ +export function mapOr( + result: Result, + defaultValue: U, + fn: (value: T) => U, +): U { + if (isErr(result)) return defaultValue; + + return fn(result.get); +} + +/** Maps a {@linkcode Result} to {@linkcode U} by applying {@linkcode defaultFn} to a contained {@linkcode Err}, or {@linkcode fn} to a contained {@linkcode Ok}. + * + * @example + * ```ts + * import { Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { mapOrElse } from "https://deno.land/x/result_js/operators/transform.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * const result: Result = Ok("Hello"); + * assertEquals(mapOrElse(result, () => 2 ** 3, ({ length }) => length), 5); + * ``` + */ +export function mapOrElse( + result: Result, + defaultFn: (value: E) => U, + fn: (value: T) => U, +): U { + if (isErr(result)) return defaultFn(result.get); + + return fn(result.get); +} diff --git a/operators/transform_test.ts b/operators/transform_test.ts new file mode 100644 index 0000000..9c90847 --- /dev/null +++ b/operators/transform_test.ts @@ -0,0 +1,82 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. + +import { map, mapOr, mapOrElse } from "./transform.ts"; +import { Err, Ok } from "../spec.ts"; +import { + assertEquals, + assertSpyCallArgs, + assertSpyCalls, + describe, + it, + spy, +} from "../_dev_deps.ts"; + +describe("map", () => { + it("should call mapper if it is Ok", () => { + const INPUT = "Hello, World!"; + const option: Ok = Ok(INPUT); + const fn = spy((v: string) => v.length); + const optionLen = map(option, fn); + + assertEquals(optionLen, Ok(13)); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [INPUT]); + }); + + it("should not call mapper function if it is Err", () => { + const option: Err = Err(""); + const fn = spy((v: string) => v.length); + const optionLen = map(option, fn); + + assertEquals(optionLen, Err("")); + assertSpyCalls(fn, 0); + }); +}); + +describe("mapOr", () => { + it("should call mapper if it is Ok", () => { + const INPUT = "Hello, World!"; + const option: Ok = Ok(INPUT); + const fn = spy((v: string) => v.length); + const optionLen = mapOr(option, 0, fn); + + assertEquals(optionLen, INPUT.length); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [INPUT]); + }); + + it("should return default value if it is Err", () => { + const option: Err = Err(""); + const fn = spy((v: string) => v.length); + const optionLen = mapOr(option, 0, fn); + + assertEquals(optionLen, 0); + assertSpyCalls(fn, 0); + }); +}); + +describe("mapOrElse", () => { + it("should call mapper if it is Ok", () => { + const INPUT = "Hello, World!"; + const option: Ok = Ok(INPUT); + const fn = spy((v: string) => v.length); + const defaultFn = spy(() => 0); + const optionLen = mapOrElse(option, defaultFn, fn); + + assertEquals(optionLen, INPUT.length); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [INPUT]); + assertSpyCalls(defaultFn, 0); + }); + + it("should return default value if it is Err", () => { + const option: Err = Err(""); + const fn = spy((v: string) => v.length); + const defaultFn = spy(() => 0); + const optionLen = mapOrElse(option, defaultFn, fn); + + assertEquals(optionLen, 0); + assertSpyCalls(fn, 0); + assertSpyCalls(defaultFn, 1); + }); +}); From 497626fc11ca6a2db4d10fc03f0b90fc4b4c4825 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 12:56:14 +0900 Subject: [PATCH 07/25] feat(extract): add extract operators --- operators/extract.ts | 222 ++++++++++++++++++++++++++++++++++++++ operators/extract_test.ts | 136 +++++++++++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 operators/extract.ts create mode 100644 operators/extract_test.ts diff --git a/operators/extract.ts b/operators/extract.ts new file mode 100644 index 0000000..225b8b1 --- /dev/null +++ b/operators/extract.ts @@ -0,0 +1,222 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. +// This module is browser compatible. + +import { inspect } from "../deps.ts"; +import { isErr, isOk } from "./query.ts"; +import { type Result } from "../spec.ts"; + +/** Returns the contained {@linkcode Ok} value. + * + * @example + * ```ts + * import { Ok } from "https://deno.land/x/result_js/spec.ts"; + * import { unwrap } from "https://deno.land/x/result_js/operators/extract.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * assertEquals(unwrap(Ok(0)), 0); + * ``` + * @throws {Error} if the {@linkcode result} is {@linkcode Err}. + * @example + * ```ts + * import { Err } from "https://deno.land/x/result_js/spec.ts"; + * import { unwrap } from "https://deno.land/x/result_js/operators/extract.ts"; + * import { assertThrows } from "https://deno.land/std/testing/asserts.ts"; + * + * assertThrows(() => unwrap(Err(1))); + * ``` + */ +export function unwrap(result: Result): T { + if (isOk(result)) return result.get; + + throw new Error(`called unwrap on an Err: ${inspect(result.get)}`); +} + +/** Returns the contained {@linkcode Err} value. + * + * @example + * ```ts + * import { Err } from "https://deno.land/x/result_js/spec.ts"; + * import { unwrapErr } from "https://deno.land/x/result_js/operators/extract.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * assertEquals(unwrapErr(Err(0)), 0); + * ``` + * @throws {Error} if the {@linkcode result} is {@linkcode Ok}. + * @example + * ```ts + * import { Ok } from "https://deno.land/x/result_js/spec.ts"; + * import { unwrapErr } from "https://deno.land/x/result_js/operators/extract.ts"; + * import { assertThrows } from "https://deno.land/std/testing/asserts.ts"; + * + * assertThrows(() => unwrapErr(Ok(0))); + * ``` + */ +export function unwrapErr(result: Result): E { + if (isErr(result)) return result.get; + + throw new Error(`called unwrapErr on an Ok: ${inspect(result.get)}`); +} + +/** Returns the contained {@linkcode Ok} value, otherwise {@linkcode defaultValue}. + * + * @example + * ```ts + * import { Ok, Err } from "https://deno.land/x/result_js/spec.ts"; + * import { unwrapOr } from "https://deno.land/x/result_js/mod.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * assertEquals(unwrapOr(Ok(0), 1), 0); + * assertEquals(unwrapOr(Err(1), 1), 1); + * ``` + */ +export function unwrapOr(result: Result, defaultValue: T): T { + if (isOk(result)) return result.get; + + return defaultValue; +} + +/** Returns the contained {@linkcode Ok} value, otherwise computes it from a closure. + * + * @example + * ```ts + * import { Ok, Err } from "https://deno.land/x/result_js/spec.ts"; + * import { unwrapOrElse } from "https://deno.land/x/result_js/operators/extract.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * assertEquals(unwrapOrElse(Ok(0), () => 2 ** 3), 0); + * assertEquals(unwrapOrElse(Err(1), () => 2 ** 3), 8); + * ``` + */ +export function unwrapOrElse( + result: Result, + fn: (value: E) => T, +): T { + if (isOk(result)) return result.get; + + return fn(result.get); +} + +/** Returns the contained {@linkcode Ok} value. + * + * @example + * ```ts + * import { Ok } from "https://deno.land/x/result_js/spec.ts"; + * import { expect } from "https://deno.land/x/result_js/operators/extract.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * const result = Ok(0); + * declare const message: string; + * + * assertEquals(expect(result, message), 0); + * ``` + * + * @throws {Error} {@linkcode msg} + * @example + * ```ts + * import { Err } from "https://deno.land/x/result_js/spec.ts"; + * import { expect } from "https://deno.land/x/result_js/mod.ts"; + * import { assertThrows } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const message: string; + * assertThrows(() => expect(Err(0), message), Error, message); + * ``` + * + * Change error constructor: + * @example + * ```ts + * import { Err } from "https://deno.land/x/result_js/spec.ts"; + * import { expect } from "https://deno.land/x/result_js/mod.ts"; + * import { assertThrows } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const message: string; + * assertThrows(() => expect(Err(0), message, RangeError), RangeError, message); + * ``` + */ +export function expect( + result: Result, + msg: string, + error: ErrorConstructor = Error, +): T { + if (isOk(result)) return result.get; + + throw new error(msg); +} + +/** Returns the contained {@linkcode Err} value. + * + * @example + * ```ts + * import { Err } from "https://deno.land/x/result_js/spec.ts"; + * import { expectErr } from "https://deno.land/x/result_js/operators/extract.ts"; + * import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; + * + * const result = Err(0); + * declare const message: string; + * + * assertEquals(expectErr(result, message), 0); + * ``` + * + * @throws {Error} {@linkcode msg} + * @example + * ```ts + * import { Ok } from "https://deno.land/x/result_js/spec.ts"; + * import { expectErr } from "https://deno.land/x/result_js/mod.ts"; + * import { assertThrows } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const message: string; + * assertThrows(() => expectErr(Ok(0), message), Error, message); + * ``` + * + * Change error constructor: + * @example + * ```ts + * import { Ok } from "https://deno.land/x/result_js/spec.ts"; + * import { expectErr } from "https://deno.land/x/result_js/mod.ts"; + * import { assertThrows } from "https://deno.land/std/testing/asserts.ts"; + * + * declare const message: string; + * assertThrows(() => expectErr(Ok(0), message, RangeError), RangeError, message); + * ``` + */ +export function expectErr( + result: Result, + msg: string, + error: ErrorConstructor = Error, +): E { + if (isErr(result)) return result.get; + + throw new error(msg); +} + +/** {@linkcode Result} matcher. */ +export interface Matcher { + /** Match on {@linkcode Ok}. */ + Ok: (value: T) => U; + + /** Match on {@linkcode Err}. */ + Err: (value: E) => U; +} + +/** Pattern matching for {@linkcode result}. Match on {@linkcode matcher.Ok} if {@linkcode Ok}, otherwise match on {@linkcode matcher.Err}. + * + * @example + * ```ts + * import { Ok, type Result } from "https://deno.land/x/result_js/spec.ts"; + * import { match } from "https://deno.land/x/result_js/operators/extract.ts"; + * + * declare const result: Result; + * + * match(result, { + * Ok: (value) => value, + * Err: (value) => 500, + * }); + * ``` + */ +export function match( + result: Result, + matcher: Readonly>, +): U { + if (isOk(result)) return matcher.Ok(result.get); + + return matcher.Err(result.get); +} diff --git a/operators/extract_test.ts b/operators/extract_test.ts new file mode 100644 index 0000000..1581c3c --- /dev/null +++ b/operators/extract_test.ts @@ -0,0 +1,136 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. + +import { + expect, + expectErr, + match, + unwrap, + unwrapErr, + unwrapOr, + unwrapOrElse, +} from "./extract.ts"; +import { Err, Ok } from "../spec.ts"; +import { + assertEquals, + assertSpyCallArgs, + assertSpyCalls, + assertThrows, + describe, + it, + spy, +} from "../_dev_deps.ts"; + +describe("unwrap", () => { + it("should return Ok value", () => { + assertEquals(unwrap(Ok(0)), 0); + }); + + it("should throw error if Err", () => { + assertThrows( + () => unwrap(Err(1)), + Error, + "called unwrap on an Err: 1", + ); + }); +}); + +describe("unwrapErr", () => { + it("should return Err value", () => { + assertEquals(unwrapErr(Err(1)), 1); + }); + + it("should throw error if Ok", () => { + assertThrows( + () => unwrapErr(Ok(0)), + Error, + "called unwrapErr on an Ok: 0", + ); + }); +}); + +describe("unwrapOr", () => { + it("should Ok value if Ok", () => { + assertEquals(unwrapOr(Ok(0), 1), 0); + }); + + it("should default value if Err", () => { + assertEquals(unwrapOr(Err(1), 0), 0); + }); +}); + +describe("unwrapOrElse", () => { + it("should Ok value if Ok", () => { + const fn = spy(() => 1); + assertEquals(unwrapOrElse(Ok(0), fn), 0); + assertSpyCalls(fn, 0); + }); + + it("should default value if Err", () => { + const fn = spy(() => 0); + assertEquals(unwrapOrElse(Err(1), fn), 0); + assertSpyCalls(fn, 1); + assertSpyCallArgs(fn, 0, [1]); + }); +}); + +describe("expect", () => { + it("should return Ok value", () => { + assertEquals(expect(Ok(0), ""), 0); + }); + + it("should throw error if Err", () => { + const message = ""; + assertThrows(() => expect(Err(1), message), Error, message); + }); + + it("should throw custom error instance if Err", () => { + const message = ""; + assertThrows( + () => expect(Err(1), message, RangeError), + RangeError, + message, + ); + }); +}); + +describe("expectErr", () => { + it("should return Err value", () => { + assertEquals(expectErr(Err(1), ""), 1); + }); + + it("should throw error if Ok", () => { + const message = ""; + assertThrows(() => expectErr(Ok(0), message), Error, message); + }); + + it("should throw custom error instance if Ok", () => { + const message = ""; + assertThrows( + () => expectErr(Ok(0), message, RangeError), + RangeError, + message, + ); + }); +}); + +describe("match", () => { + it("should call Ok if Ok", () => { + const s = spy(() => 1); + const n = spy(() => 2); + + assertEquals(match(Ok(0), { Ok: s, Err: n }), 1); + assertSpyCalls(s, 1); + assertSpyCalls(n, 0); + assertSpyCallArgs(s, 0, [0]); + }); + + it("should call Err if Err", () => { + const s = spy(() => 1); + const n = spy(() => 2); + + assertEquals(match(Err(1), { Ok: s, Err: n }), 2); + assertSpyCalls(s, 0); + assertSpyCalls(n, 1); + assertSpyCallArgs(n, 0, [1]); + }); +}); From 184b485a569c56d0b8cd89d95193fde8a66761ee Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 13:45:33 +0900 Subject: [PATCH 08/25] chore(deps): add deps --- deps.ts | 16 ++++++++++++++++ deps_test.ts | 21 +++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 deps.ts create mode 100644 deps_test.ts diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..0671470 --- /dev/null +++ b/deps.ts @@ -0,0 +1,16 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. +// This module is browser compatible. + +/** Light-weight JavaScript value inspection. */ +export function inspect(value: unknown): string { + switch (typeof value) { + case "string": + return `"${value}"`; + + case "bigint": + return `${value}n`; + + default: + return String(value); + } +} diff --git a/deps_test.ts b/deps_test.ts new file mode 100644 index 0000000..e3b6ae8 --- /dev/null +++ b/deps_test.ts @@ -0,0 +1,21 @@ +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. + +import { inspect } from "./deps.ts"; +import { assertEquals, describe, it } from "./_dev_deps.ts"; + +describe("inspect", () => { + it("should return string", () => { + const table: [actual: unknown, expected: string][] = [ + ["", `""`], + [0, "0"], + [0n, "0n"], + [false, "false"], + [null, "null"], + [{}, "[object Object]"], + ]; + + table.forEach(([actual, expected]) => { + assertEquals(inspect(actual), expected); + }); + }); +}); From e033f82713df356d3b9d8dffc69de424051a22e4 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 13:49:45 +0900 Subject: [PATCH 09/25] feat(mod): export related modules --- _dev_deps.ts | 16 ++++++++++++---- mod.ts | 24 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/_dev_deps.ts b/_dev_deps.ts index 89d9c11..c9c40a4 100644 --- a/_dev_deps.ts +++ b/_dev_deps.ts @@ -1,7 +1,15 @@ -export { assertEquals } from "https://deno.land/std@0.157.0/testing/asserts.ts"; +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. +// This module is browser compatible. + export { - assertSpyCall, + assert, + assertEquals, + assertFalse, + assertThrows, +} from "https://deno.land/std@0.190.0/testing/asserts.ts"; +export { describe, it } from "https://deno.land/std@0.190.0/testing/bdd.ts"; +export { + assertSpyCallArgs, assertSpyCalls, spy, -} from "https://deno.land/std@0.157.0/testing/mock.ts"; -export { describe, it } from "https://deno.land/std@0.157.0/testing/bdd.ts"; +} from "https://deno.land/std@0.190.0/testing/mock.ts"; diff --git a/mod.ts b/mod.ts index 3b5a399..c0d4902 100644 --- a/mod.ts +++ b/mod.ts @@ -1,2 +1,24 @@ -// Copyright 2022-latest Tomoki Miyauchi. All rights reserved. MIT license. +// Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. // This module is browser compatible. + +export { + Err, + type ErrConstructor, + Ok, + type OkConstructor, + type Result, + ResultType, +} from "./spec.ts"; +export { isErr, isOk } from "./operators/query.ts"; +export { and, andThen, or, orElse } from "./operators/logical.ts"; +export { map, mapOr, mapOrElse } from "./operators/transform.ts"; +export { + expect, + expectErr, + match, + type Matcher, + unwrap, + unwrapErr, + unwrapOr, + unwrapOrElse, +} from "./operators/extract.ts"; From 2cdd8b3b60baa37e31294d599f3ca51dd306f287 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 14:21:02 +0900 Subject: [PATCH 10/25] docs(mod): add module description --- mod.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/mod.ts b/mod.ts index c0d4902..cd6e3cf 100644 --- a/mod.ts +++ b/mod.ts @@ -1,6 +1,66 @@ // Copyright © 2023 Tomoki Miyauchi. All rights reserved. MIT license. // This module is browser compatible. +/** Error handling with the {@linkcode Result} type. + * {@linkcode Result} is the type used for returning and propagating errors. + * It is an enum with the variants, {@linkcode Ok(T)}, representing success and containing a value, and {@linkcode Err(E)}, representing error and containing an error value. + * + * ## Operators overview + * {@linkcode Result} provides a wide variety of different operators. + * + * ## Querying the variant + * The {@linkcode isOk} and {@linkcode isErr} return `true` if the {@linkcode Result} is {@linkcode Ok} or {@linkcode Err}, respectively. + * + * ## Extracting contained values + * Extract the contained value in a {@linkcode Result} when it is the {@linkcode Ok}. If the {@linkcode Result} is {@linkcode Err}: + * - {@linkcode expect} throws with a provided custom message + * - {@linkcode unwrap} throws with a generic message + * - {@linkcode unwrapOr} returns the provided default value + * - {@linkcode unwrapOrElse} returns the result of evaluating the provided function + * + * Extract the contained value in a {@linkcode Result} when it is the {@linkcode Err}. If the {@linkcode Result} is {@linkcode Ok}: + * - {@linkcode expectErr} throws with a provided custom message + * - {@linkcode unwrapErr} throws with a generic message + * + * ## Transforming contained values + * Transforms the contained value of the {@linkcoce Ok}: + * - {@linkcode map} transforms {@linkcode Result} into {@linkcode Result} by applying the provided function to the contained value of {@linkcode Ok} and leaving {@linkcode Err} + * + * Transforms the contained value of the {@linkcode Err}: + * - {@linkcode mapErr} transforms {@linkcode Result} into {@linkcode Result} by applying the provided function to the contained value of {@linkcode Err} and leaving {@linkcode Ok} + * + * Transform a {@linkcode Result} into a value of a possibly different type `U`: + * - {@linkcode mapOr} applies the provided function to the contained value of {@linkcode Ok}, or returns the provided default value if the {@linkcode Result} is {@linkcode Err} + * - {@linkcode mapOrElse} applies the provided function to the contained value of {@linkcode Ok}, or applies the provided default fallback function to the contained value of {@linkcode Err} + * + * ## Logical operators + * Treat the {@linkcode Result} as a boolean value, where {@linkcode Ok} acts like `true` and {@linkcode Err} acts like `false`. + * + * The {@linkcode and} and {@linkcode or} take another {@linkcode Result} as input, and produce a {@linkcode Result} as output. + * The {@linkcode and} can produce a {@linkcode Result} value having a different inner type `U` than {@linkcode Result}. + * The {@linkcode or} can produce a {@linkcode Result} value having a different error type `F` than {@linkcode Result}. + * + * | name | result | input | output | + * | --------------- | ------ | ------ | ------ | + * | {@linkcode and} | ok | result | result | + * | {@linkcode and} | err | result | err | + * | {@linkcode or} | ok | result | ok | + * | {@linkcode or} | err | result | result | + * + * The {@linkcode andThen} and {@linkcode orElse} take a function as input, and only evaluate the function when they need to produce a new value. + * The {@linkcode andThen} can produce a {@linkcode Result} value having a different inner type `U` than {@linkcode Result}. + * The {@linkcode orElse} can produce a {@linkcode Result} value having a different error type `F` than {@linkcode Result}. + * + * | name | result | function input | function result | output | + * | ------------------- | -------- | -------------- | --------------- | ------ | + * | {@linkcode andThen} | `Ok` | `T` | result | result | + * | {@linkcode andThen} | err | - | - | err | + * | {@linkcode orElse} | ok | - | - | ok | + * | {@linkcode orElse} | `Err` | `E` | result | result | + * + * @module + */ + export { Err, type ErrConstructor, From 6e6d19fad0f5ac962252fe914e95231791013bfb Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 14:27:36 +0900 Subject: [PATCH 11/25] chore: update config --- _test_import_map.json | 4 ++-- deno.json | 24 ++++++++++-------------- deno.lock | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 16 deletions(-) create mode 100644 deno.lock diff --git a/_test_import_map.json b/_test_import_map.json index bcee288..0b1ca46 100644 --- a/_test_import_map.json +++ b/_test_import_map.json @@ -1,6 +1,6 @@ { "imports": { - "https://deno.land/x/result_js@$VERSION/": "./", - "https://deno.land/std@$VERSION/": "https://deno.land/std@0.157.0/" + "https://deno.land/x/result_js/": "./", + "https://deno.land/std/": "https://deno.land/std@0.190.0/" } } diff --git a/deno.json b/deno.json index c79a66e..1b5e085 100644 --- a/deno.json +++ b/deno.json @@ -1,24 +1,20 @@ { "tasks": { - "test": "deno test --doc" - }, - "compilerOptions": { - "noUncheckedIndexedAccess": true + "test": "deno test --import-map=./_test_import_map.json --doc", + "build:npm": "deno run -A --no-lock _tools/build_npm.ts" }, "fmt": { - "files": { - "exclude": ["CHANGELOG.md", "CODE_OF_CONDUCT.md"] - } + "exclude": ["CHANGELOG.md"] }, "lint": { - "files": { - "exclude": ["CHANGELOG.md", "CODE_OF_CONDUCT.md"] - } + "exclude": ["CHANGELOG.md"] }, "test": { - "files": { - "exclude": ["CHANGELOG.md", "CODE_OF_CONDUCT.md", "specs/api.md"] - } + "exclude": ["CHANGELOG.md", "npm"] }, - "importMap": "_test_import_map.json" + "compilerOptions": { + "noImplicitReturns": true, + "noImplicitOverride": true, + "noUncheckedIndexedAccess": true + } } diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..b819aa6 --- /dev/null +++ b/deno.lock @@ -0,0 +1,17 @@ +{ + "version": "2", + "remote": { + "https://deno.land/std@0.157.0/fmt/colors.ts": "ff7dc9c9f33a72bd48bc24b21bbc1b4545d8494a431f17894dbc5fe92a938fc4", + "https://deno.land/std@0.157.0/testing/_diff.ts": "a23e7fc2b4d8daa3e158fa06856bedf5334ce2a2831e8bf9e509717f455adb2c", + "https://deno.land/std@0.157.0/testing/_format.ts": "cd11136e1797791045e639e9f0f4640d5b4166148796cad37e6ef75f7d7f3832", + "https://deno.land/std@0.157.0/testing/asserts.ts": "8696c488bc98d8d175e74dc652a0ffbc7fca93858da01edc57ed33c1148345da", + "https://deno.land/std@0.190.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e", + "https://deno.land/std@0.190.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.190.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.190.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.190.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f", + "https://deno.land/std@0.190.0/testing/bdd.ts": "59f7f7503066d66a12e50ace81bfffae5b735b6be1208f5684b630ae6b4de1d0", + "https://deno.land/std@0.190.0/testing/mock.ts": "220ed9b8151cb2cac141043d4cfea7c47673fab5d18d1c1f0943297c8afb5d13", + "https://deno.land/std@0.190.0/testing/types.ts": "6832887bea73ee3549dda953a8df41f8bfa8790219ab56a3b94b984a71a3b3e9" + } +} From 10f746d125d911693a814a515a52022016b2f130 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 14:27:58 +0900 Subject: [PATCH 12/25] chore: remove unused --- specs/api.md | 75 ---------------------------------------------------- 1 file changed, 75 deletions(-) delete mode 100644 specs/api.md diff --git a/specs/api.md b/specs/api.md deleted file mode 100644 index 17db935..0000000 --- a/specs/api.md +++ /dev/null @@ -1,75 +0,0 @@ -# API - -## Nomenclature - -- Container is value-wrapped object. It has a type representing the container - type. -- Ok container representing success. The type of the container is `ok`. -- Err container representing failure. The type of the container is `err`. -- Result container is a set of Ok container and Err container. - -## Signature - -A container is one that satisfies the following interfaces. - -### Container - -```ts -interface Container { - /** The contained value. */ - value: unknown; - - /** The container type. */ - type: string; -} -``` - -### Ok container - -```ts -interface OkContainer { - /** Ok container value. */ - value: T; - - /** Ok container type. */ - type: "ok"; -} -``` - -### Err container - -```ts -interface ErrContainer { - /** Err container value. */ - value: E; - - /** Err container type. */ - type: "err"; -} -``` - -### Result container - -```ts -type ResultContainer = OkContainer | ErrContainer; -``` - -### Result constructor - -```ts -interface ResultConstructor { - /** Create a new {@link OkContainer}. */ - ok: (value: T) => OkContainer; - - /** Create a new {@link ErrContainer}. */ - err: (value: E) => ErrContainer; -} -``` - -### Result - -The `Result` must implement the `ResultConstructor` and the `ResultContainer`. - -```ts -type Result = ResultContainer & ResultConstructor; -``` From be0bcf8c987e1f52f0ee6e795573d4a5b163a430 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 14:48:41 +0900 Subject: [PATCH 13/25] docs(readme): update usage --- README.md | 219 ++++++++++++------------------------------------------ 1 file changed, 49 insertions(+), 170 deletions(-) diff --git a/README.md b/README.md index 4c234ac..dd8df95 100644 --- a/README.md +++ b/README.md @@ -3,204 +3,83 @@ [![deno land](http://img.shields.io/badge/available%20on-deno.land/x-lightgrey.svg?logo=deno)](https://deno.land/x/result_js) [![deno doc](https://doc.deno.land/badge.svg)](https://doc.deno.land/https/deno.land/x/result_js/mod.ts) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/TomokiMiyauci/result-js)](https://github.com/TomokiMiyauci/result-js/releases) -[![codecov](https://codecov.io/github/TomokiMiyauci/result-js/branch/main/graph/badge.svg?token=xCMzpv1veg)](https://codecov.io/github/TomokiMiyauci/result-js) -[![bundle size](https://bundlejs.com/api/badge?q=https://deno.land/x/result_js/mod.ts)](https://bundlejs.com/?q=https%3A%2F%2Fdeno.land%2Fx%2Fresult_js%2Fmod.ts) -[![GitHub](https://img.shields.io/github/license/TomokiMiyauci/result-js)](https://github.com/TomokiMiyauci/result-js/blob/main/LICENSE) +[![codecov](https://codecov.io/github/TomokiMiyauci/result-js/branch/main/graph/badge.svg)](https://codecov.io/github/TomokiMiyauci/result-js) +[![GitHub](https://img.shields.io/github/license/TomokiMiyauci/result-js)](LICENSE) [![test](https://github.com/TomokiMiyauci/result-js/actions/workflows/test.yaml/badge.svg)](https://github.com/TomokiMiyauci/result-js/actions/workflows/test.yaml) [![NPM](https://nodei.co/npm/@miyauci/result.png?mini=true)](https://nodei.co/npm/@miyauci/result/) +[![standard-readme compliant](https://img.shields.io/badge/readme%20style-standard-brightgreen.svg)](https://github.com/RichardLitt/standard-readme) +[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) -The standard API for result in JavaScript. +Minimum result type port of Rust. -## What +## Table of Contents -It brings a standard Result representation to the JavaScript world. +- [Install](#install) +- [Usage](#usage) +- [API](#api) +- [Acknowledgements](#acknowledgements) +- [Contributing](#contributing) +- [License](#license) -Provides an API for complete representation of two states representing success -and failure. +## Install -Our goal is to become the standard error handling method for third-party -libraries. - -It has the following features: - -- Simple and lean - - Designed to be used as client JavaScript (Off Course server side). It provides - an API that is as small and lean as possible. There is not a single useless - API for you to use. - -- `Ok` and `Err` - - The `Result` consists of two containers, `Ok` and `Err`. The naming is - inspired by - [Rust::std:Result](https://doc.rust-lang.org/std/result/enum.Result.html#) and - is short and easy to understand. - -- Self-contained - - `Result` is self-contained. This means that type declarations, container - creation, retrieval of values from containers, and comparison of container - types are provided by `Result`. - -If you want to know more, see [Definition](./specs/api.md). - -### What is not - -Client JavaScript is a special execution environment. Unnecessary API bloat is -reflected in bundle size, to the detriment of users. - -Unfortunately, being a dynamic language, JavaScript tree-shaking has its -limitations. - -Therefore, intentionally, it does not have the following features. - -- Monad laws -- Rust::std::result like - -These may include some APIs that are not used for all libraries. - -## Create Ok(success) container - -Ok container is created with `Result#ok`. Accepts any value. +deno.land: ```ts -import { Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -const result = Result.ok("any success value"); +import * as mod from "https://deno.land/x/result_js/mod.ts"; ``` -## Create Err(failure) container - -Err container is created with `Result#err`. Accepts any value. +npm: -```ts -import { Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -const result = Result.err("any failure value"); +```bash +npm i @miyauci/result ``` -## Define a container as the return value +## Usage -The `Result` defines OK on the left and failure on the right. +Type [Result](https://deno.land/x/result_js/mod.ts?s=Result) represents an +success or failure. ```ts -import { Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; - -function div(left: number, right: number): Result { - if (right === 0) { - return Result.err(new RangeError("Division by zero")); - } - return Result.ok(left / right); +import { + Err, + Ok, + type Result, + unwrap, +} from "https://deno.land/x/result_js/mod.ts"; +import { assertThrows } from "https://deno.land/std/testing/asserts.ts"; + +function divide( + numerator: number, + denominator: number, +): Result { + if (!denominator) return Err("divide by 0"); + + return Ok(numerator / denominator); } -``` - -## Retrieving values from containers - -The container has a `type` that represents the type of its own container. This -allows you to identify `Ok` containers and `Err` containers. - -```ts -import { Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -declare const result: Result; - -if (result.type === "ok") { - result.value; -} else { - result.value; -} -``` - -These are all the functions of a container. - -## Handle dangerous code - -Wrap code that may throw errors in a container. -```ts -import { unsafe } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - -const result = unsafe(() => { - throw Error("Dangerous!!"); -}); -assertEquals(result.value, Error()); -assertEquals(result.type, "err"); -``` - -By default, the Err container value is of type `unknown`. If you know more about -the error being thrown, you can give a more detailed type. - -For example, instantiation of `Headers` is known to throw `TypeError`. - -```ts -import { unsafe } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - -const result = unsafe(() => - new Headers({ "?": "invalid field name" }) -); -assertEquals(result.value, new TypeError()); -``` - -> You would have wanted to specify only the type of error. -> `unsafe(() => new Headers());` Unfortunately, this is not possible. -> TypeScript will only allow partial generics to be specified if The remaining -> generics become default type arguments. For that reason, `unsafe` accepts the -> type `Ok` on the left and `Err` on the right. - -## Pattern matching - -It provides an interface similar to pattern matching in functional programming -languages. - -```ts -import { match, Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - -const value = match(Result.ok("Tom"), { - ok: (value) => "Hello " + value, - err: (value) => "Goodby " + value, -}); -assertEquals(value, "Hello Tom"); -``` - -## Validate container - -Provides a function to check the type of container with Type guard. - -### isOk - -Whether the `ResultContainer` is `OkContainer` or not. - -```ts -import { isOk, Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - -assertEquals(isOk(Result.ok("OK!!")), true); -assertEquals(isOk(Result.err("Error!!")), false); +const opt = divide(100, 0); +assertThrows(() => unwrap(opt)); ``` -### isErr - -Whether the `ResultContainer` is `ErrContainer` or not. - -```ts -import { isErr, Result } from "https://deno.land/x/result_js@$VERSION/mod.ts"; -import { assertEquals } from "https://deno.land/std@$VERSION/testing/asserts.ts"; - -assertEquals(isErr(Result.err("Error!!")), true); -assertEquals(isErr(Result.ok("OK!!")), false); -``` +All [operators](https://deno.land/x/result_js/mod.ts#Functions) for +[Result](https://deno.land/x/optio/mod.ts?s=Result) are separated from +prototype. ## API All APIs can be found in the [deno doc](https://doc.deno.land/https/deno.land/x/result_js/mod.ts). -## License +## Acknowledgements -Copyright © 2022-present [Tomoki Miyauchi](https://github.com/TomokiMiyauci). +- [Rust std::result](https://doc.rust-lang.org/std/result/index.html) -Released under the [MIT](./LICENSE) license +## Contributing + +See [contribution](CONTRIBUTING.md). + +## License -Inspired by -[Rust, Result](https://doc.rust-lang.org/std/result/enum.Result.html#). +[MIT](LICENSE) © 2023 Tomoki Miyauchi From ed11b37a5f0a8ae5c248af95da4f6b4e8d0e2e70 Mon Sep 17 00:00:00 2001 From: Tomoki Miyauchi Date: Fri, 7 Jul 2023 14:50:02 +0900 Subject: [PATCH 14/25] docs: add policy files --- .github/CODE_OF_CONDUCT.md | 129 ++++++++++++++++++++++++++++++++++ .github/SECURITY.md | 11 +++ CONTRIBUTING.md | 138 +++++++++++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+) create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/SECURITY.md create mode 100644 CONTRIBUTING.md diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..45a9613 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,129 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. All complaints will be reviewed and investigated promptly +and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +. Translations are available at +. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..f35eb9c --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,11 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| :-----: | :----------------: | +| 2.y.z | :white_check_mark: | + +## Reporting a Vulnerability + +Please report to regarding the vulnerability. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a3e97ac --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,138 @@ +# Contributing + +We welcome and appreciate all contributions. + +We follow the [code of conduct](./.github/CODE_OF_CONDUCT.md). + +## Submitting a pull request + +Before submitting a PR to any of the repos, please make sure the following is +done: + +1. Give the PR a descriptive title +2. Run [fmt](#format) in the root of repository +3. Run [lint](#lint) +4. Run [test](#test) + +## Coding rules + +We follow the +[style guide](https://deno.com/manual/references/contributing/style_guide). + +### Source code + +To ensure consistency and quality throughout the source code, all code +modifications must have: + +- No [linting](#lint) errors +- A [test](#test) for every possible case introduced by your code change +- 100% test coverage +- [Valid commit message](#commit-message-guidelines) + +### Format + +This will format all of the code to adhere to the consistent style in the +repository. + +```bash +deno fmt +``` + +### Lint + +This will check TypeScript code for common mistakes and errors. + +```bash +deno lint +``` + +### Test + +This will run all logic test and document test. + +```bash +deno test --import-map=./_test_import_map.json --doc +``` + +or + +```bash +deno task test +``` + +### Commit message guidelines + +Each commit message consists of a **header**, a **body** and a **footer**. The +header has a special format that includes a **type**, a **scope** and a +**subject**: + +```commit +(): + + + +