From 92ea9bd652f3ea7bf18f93c77b6b15c02f2fae11 Mon Sep 17 00:00:00 2001 From: CRIMX Date: Wed, 18 Oct 2023 01:10:37 +0800 Subject: [PATCH] refactor: add tests --- package.json | 3 +- pnpm-lock.yaml | 115 +++------ src/array.ts | 6 +- src/option.ts | 94 ++++--- src/result.ts | 136 ++++++---- src/utils.ts | 8 +- test/array.test.ts | 164 ++++++++++++ test/option.test.ts | 506 ++++++++++++++++++++++++++++++++++++ test/result.test.ts | 613 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 1463 insertions(+), 182 deletions(-) create mode 100644 test/array.test.ts create mode 100644 test/option.test.ts create mode 100644 test/result.test.ts diff --git a/package.json b/package.json index 916c1a9..84a61eb 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "scripts": { "prepublishOnly": "pnpm run build && echo 'Run the npm publish command inside `dist`.' && exit 1", "build": "rimraf dist && tsc && node ./scripts/package.js", + "test": "vitest", "lint": "eslint --ext .ts,.tsx,.js,.mjs . && prettier --check .", "lint:fix": "eslint --ext .ts,.tsx,.js,.mjs . --fix && prettier -w ." }, @@ -32,7 +33,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.4.0", "@typescript-eslint/parser": "^6.4.0", - "@vitest/coverage-c8": "^0.33.0", + "@vitest/coverage-v8": "^0.34.1", "eslint": "^8.47.0", "eslint-config-prettier": "^8.10.0", "eslint-plugin-import": "^2.27.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dd744ca..e5cd827 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,9 @@ devDependencies: '@typescript-eslint/parser': specifier: ^6.4.0 version: 6.4.0(eslint@8.47.0)(typescript@5.1.6) - '@vitest/coverage-c8': - specifier: ^0.33.0 - version: 0.33.0(vitest@0.34.2) + '@vitest/coverage-v8': + specifier: ^0.34.1 + version: 0.34.1(vitest@0.34.2) eslint: specifier: ^8.47.0 version: 8.47.0 @@ -564,17 +564,25 @@ packages: eslint-visitor-keys: 3.4.3 dev: true - /@vitest/coverage-c8@0.33.0(vitest@0.34.2): - resolution: {integrity: sha512-DaF1zJz4dcOZS4k/neiQJokmOWqsGXwhthfmUdPGorXIQHjdPvV6JQSYhQDI41MyI8c+IieQUdIDs5XAMHtDDw==} + /@vitest/coverage-v8@0.34.1(vitest@0.34.2): + resolution: {integrity: sha512-lRgUwjTMr8idXEbUPSNH4jjRZJXJCVY3BqUa+LDXyJVe3pldxYMn/r0HMqatKUGTp0Kyf1j5LfFoY6kRqRp7jw==} peerDependencies: - vitest: '>=0.30.0 <1' + vitest: '>=0.32.0 <1' dependencies: '@ampproject/remapping': 2.2.1 - c8: 7.14.0 + '@bcoe/v8-coverage': 0.2.3 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 magic-string: 0.30.4 picocolors: 1.0.0 std-env: 3.4.3 + test-exclude: 6.0.0 + v8-to-istanbul: 9.1.0 vitest: 0.34.2 + transitivePeerDependencies: + - supports-color dev: true /@vitest/expect@0.34.2: @@ -754,25 +762,6 @@ packages: fill-range: 7.0.1 dev: true - /c8@7.14.0: - resolution: {integrity: sha512-i04rtkkcNcCf7zsQcSv/T9EbUn4RXQ6mropeMcjFOsQXQ0iGLAr/xT6TImQg4+U9hmNpN9XdvPkjUL1IzbgxJw==} - engines: {node: '>=10.12.0'} - hasBin: true - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@istanbuljs/schema': 0.1.3 - find-up: 5.0.0 - foreground-child: 2.0.0 - istanbul-lib-coverage: 3.2.0 - istanbul-lib-report: 3.0.1 - istanbul-reports: 3.1.5 - rimraf: 3.0.2 - test-exclude: 6.0.0 - v8-to-istanbul: 9.1.0 - yargs: 16.2.0 - yargs-parser: 20.2.9 - dev: true - /cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -815,14 +804,6 @@ packages: resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} dev: true - /cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - dev: true - /color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1025,11 +1006,6 @@ packages: '@esbuild/win32-x64': 0.18.20 dev: true - /escalade@3.1.1: - resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} - engines: {node: '>=6'} - dev: true - /escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1277,14 +1253,6 @@ packages: is-callable: 1.2.7 dev: true - /foreground-child@2.0.0: - resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} - engines: {node: '>=8.0.0'} - dependencies: - cross-spawn: 7.0.3 - signal-exit: 3.0.7 - dev: true - /foreground-child@3.1.1: resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} engines: {node: '>=14'} @@ -1323,11 +1291,6 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true - /get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - dev: true - /get-func-name@2.0.0: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} dev: true @@ -1645,6 +1608,17 @@ packages: supports-color: 7.2.0 dev: true + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-reports@3.1.5: resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} engines: {node: '>=8'} @@ -1998,11 +1972,6 @@ packages: functions-have-names: 1.2.3 dev: true - /require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - dev: true - /resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2105,10 +2074,6 @@ packages: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} dev: true - /signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true - /signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -2124,6 +2089,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true @@ -2534,33 +2504,10 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - dev: true - /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: true - /yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - dev: true - - /yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - dependencies: - cliui: 7.0.4 - escalade: 3.1.1 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 - dev: true - /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} diff --git a/src/array.ts b/src/array.ts index ba27596..2d860e7 100644 --- a/src/array.ts +++ b/src/array.ts @@ -117,7 +117,7 @@ export const lastIndex = ( * * @param arr - An array * @param predicate - A predicate function. - * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. + * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, the first element is returned. * @returns The first item that matches the predicate, or `None` if no item matches. */ export const first = ( @@ -131,14 +131,14 @@ export const first = ( * * @param arr - An array * @param predicate - A predicate function. - * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. + * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, the last element is returned. * @returns The last item that matches the predicate, or `None` if no item matches. */ export const last = ( arr: T[], predicate: (value: T, index: number, array: T[]) => boolean = truePredicate, thisArg?: any -): Option => lastIndex(arr, predicate, thisArg).map(valueAtIndex, this); +): Option => lastIndex(arr, predicate, thisArg).map(valueAtIndex, arr); /** * Applies function to the elements of iterator and returns the first non-none result. diff --git a/src/option.ts b/src/option.ts index b50f4d4..19e1c2a 100644 --- a/src/option.ts +++ b/src/option.ts @@ -1,7 +1,7 @@ import type { UnwrapErr, UnwrapOk } from "./result"; import { Result } from "./result"; -import { ANY, OPTION } from "./utils"; +import { NONE, OPTION } from "./utils"; type Falsy = false | 0 | 0n | "" | null | undefined; type Truthy = Exclude; @@ -19,13 +19,13 @@ export class Option { * @param value - A value of type `T` * @returns Wrap a value into an `Option`. */ - public static Some = (value: T): Some => - Object.freeze(new Option(value)) as Some; + public static Some = (value: T): Option => + Object.freeze(new Option(value)) as Option; /** * The `None` value. */ - public static None: None = Option.Some(ANY as never); + public static None: None = /* @__PURE__ */ Option.Some(NONE); /** * Wrap a value in an `Option` if the value is truthy. @@ -76,10 +76,10 @@ export class Option { } private readonly [OPTION] = 1; - private readonly _value_: T; + private readonly _value: T; private constructor(value: T) { - this._value_ = value; + this._value = value; } /** @@ -89,22 +89,22 @@ export class Option { */ *[Symbol.iterator]() { if (this.isSome()) { - yield this._value_; + yield this._value; } } /** * @returns `true` if the `Option` is a `Some`. */ - public isSome(): this is Some { - return this._value_ !== ANY; + public isSome(): boolean { + return this._value !== NONE; } /** * @returns `true` if the `Option` is a `None`. */ - public isNone(): this is None { - return this._value_ === ANY; + public isNone(): boolean { + return this._value === NONE; } /** @@ -113,11 +113,8 @@ export class Option { * @param predicate - A function that returns `true` if the value satisfies the predicate, otherwise `false` * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. */ - public isSomeAnd( - predicate: (value: T) => boolean, - thisArg?: any - ): this is Some { - return this.isSome() && predicate.call(thisArg, this._value_); + public isSomeAnd(predicate: (value: T) => boolean, thisArg?: any): boolean { + return this.isSome() && predicate.call(thisArg, this._value); } /** @@ -128,7 +125,7 @@ export class Option { */ public isSame(other: unknown): boolean { return Option.isOption(other) - ? Object.is(this._value_, other._value_) + ? Object.is(this._value, other._value) : false; } @@ -153,7 +150,7 @@ export class Option { getOptionB: (value: T) => Option, thisArg?: any ): Option { - return this.isSome() ? getOptionB.call(thisArg, this._value_) : Option.None; + return this.isSome() ? getOptionB.call(thisArg, this._value) : Option.None; } /** @@ -195,7 +192,7 @@ export class Option { */ public zip(optionB: Option): Option<[T, B]> { return this.isSome() && optionB.isSome() - ? Option.Some([this._value_, optionB._value_]) + ? Option.Some([this._value, optionB._value]) : Option.None; } @@ -213,7 +210,7 @@ export class Option { thisArg?: any ): Option { return this.isSome() && optionB.isSome() - ? Option.Some(fn.call(thisArg, this._value_, optionB._value_)) + ? Option.Some(fn.call(thisArg, this._value, optionB._value)) : Option.None; } @@ -226,8 +223,8 @@ export class Option { Option, Option ] { - return this.isSome() && Array.isArray(this._value_) - ? [this._value_[0], this._value_[1]] + return this.isSome() && Array.isArray(this._value) + ? [Option.Some(this._value[0]), Option.Some(this._value[1])] : [Option.None, Option.None]; } @@ -235,8 +232,8 @@ export class Option { * Converts from `Option>` to `Option` */ public flatten(): Option> { - return this.isSome() && Option.isOption>(this._value_) - ? this._value_ + return this.isSome() && Option.isOption>(this._value) + ? this._value : (this as Option>); } @@ -249,7 +246,7 @@ export class Option { * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. */ public filter(predicate: (value: T) => boolean, thisArg?: any): Option { - return this.isSome() && predicate.call(thisArg, this._value_) + return this.isSome() && predicate.call(thisArg, this._value) ? this : Option.None; } @@ -263,20 +260,22 @@ export class Option { */ public map(fn: (value: T) => U, thisArg?: any): Option { return this.isSome() - ? Option.Some(fn.call(thisArg, this._value_)) + ? Option.Some(fn.call(thisArg, this._value)) : Option.None; } /** - * Transposes an `Option` of a `Result` into a `Result` of an `Option`. + * Transposes an `Option(Result)` into `Result(Option)`. * - * None will be mapped to `Ok(None)`. `Some(Ok(_))` and `Some(Err(_))` will be mapped to `Ok(Some(_))` and `Err(_)`. + * - `None` will be mapped to `Ok(None)`. + * - `Some(Ok(_))` and `Some(Err(_))` will be mapped to `Ok(Some(_))` and `Err(_)`. + * - `Some(value)` will be mapped to `Ok(Some(value))`. */ - public transpose(): Result>, UnwrapErr> { + public transpose(): Result>, UnwrapErr> { return this.isSome() - ? Result.isResult, UnwrapErr>(this._value_) - ? this._value_.map(Option.Some) - : Result.Ok(this._value_ as Option>) + ? Result.isResult, UnwrapErr>(this._value) + ? this._value.map(Option.Some) + : Result.Ok(this) : Result.Ok(Option.None); } @@ -288,7 +287,7 @@ export class Option { * @param error - The error value for `Err` if the `Option` is `None`. */ public okOr(error: E): Result { - return this.isSome() ? Result.Ok(this._value_) : Result.Err(error); + return this.isSome() ? Result.Ok(this._value) : Result.Err(error); } /** @@ -299,7 +298,7 @@ export class Option { */ public okOrElse(error: () => E, thisArg?: any): Result { return this.isSome() - ? Result.Ok(this._value_) + ? Result.Ok(this._value) : Result.Err(error.call(thisArg)); } @@ -312,7 +311,7 @@ export class Option { */ public unwrap(message = "called `Option.unwrap()` on a `None` value"): T { if (this.isSome()) { - return this._value_; + return this._value; } throw new Error(message); } @@ -330,7 +329,7 @@ export class Option { */ public unwrapOr(defaultValue: U): T | U; public unwrapOr(defaultValue?: T): T | undefined { - return this.isSome() ? this._value_ : defaultValue; + return this.isSome() ? this._value : defaultValue; } /** @@ -340,7 +339,22 @@ export class Option { * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. */ public unwrapOrElse(fn: () => U, thisArg?: any): T | U { - return this.isSome() ? this._value_ : fn.call(thisArg); + return this.isSome() ? this._value : fn.call(thisArg); + } + + /** + * Extract the value from an `Option` in a way that handles both the `Some` and `None` cases. + * + * @param Some - A function that returns a value if the `Option` is a `Some`. + * @param None - A function that returns a value if the `Option` is a `None`. + * @returns The value returned by the provided function. + */ + public match(Some: (value: T) => U, None: () => U): U { + return this.isSome() ? Some(this._value) : None(); + } + + public toString(): string { + return this.isSome() ? `Some(${this._value})` : "None"; } } @@ -348,11 +362,11 @@ export class Option { * @param value - A value of type `T` * @returns Wrap a value into an `Option`. */ -export const Some = Option.Some; +export const Some = /* @__PURE__ */ (() => Option.Some)(); export type Some = Option; /** * The `None` value. */ -export const None = Option.None; -export type None = Option; +export const None = /* @__PURE__ */ (() => Option.None)(); +export type None = Option; diff --git a/src/result.ts b/src/result.ts index 5f1b7e3..30e5cf2 100644 --- a/src/result.ts +++ b/src/result.ts @@ -1,13 +1,18 @@ import type { UnwrapOption } from "./option"; import { Option } from "./option"; -import { ANY, RESULT } from "./utils"; +import { ERR, OK, RESULT } from "./utils"; export type UnwrapOk = T extends Result ? U : Default; export type UnwrapErr = E extends Result ? U : Default; +export interface ResultMatcher { + Ok: (value: T) => U; + Err: (error: E) => U; +} + /** * The `Result` type is an immutable representation of either success (`Ok`) or failure (`Err`). */ @@ -16,15 +21,15 @@ export class Result { * @param value - A value of type `T` * @returns Wrap a value into an `Result`. */ - public static Ok = (value: T): Result => - Object.freeze(new Result(value, ANY)) as Result; + public static Ok = (value: T): Result => + Object.freeze(new Result(value, ERR)) as Result; /** * @param error - An error of type `E` * @returns Wrap an error into an `Result`. */ - public static Err = (error: E): Result => - Object.freeze(new Result(ANY, error)) as Result; + public static Err = (error: E): Result => + Object.freeze(new Result(OK, error)) as Result; /** * `Err` if the value is an `Error`. @@ -115,12 +120,12 @@ export class Result { } private [RESULT] = 1; - private _value_: T; - private _error_: E; + private _value: T; + private _error: E; private constructor(value: T, error: E) { - this._value_ = value; - this._error_ = error; + this._value = value; + this._error = error; } /** @@ -130,44 +135,38 @@ export class Result { */ *[Symbol.iterator]() { if (this.isOk()) { - yield this._value_; + yield this._value; } } /** * @returns `true` if the `Result` is an `Ok`. */ - public isOk(): this is Ok { - return this._value_ !== ANY; + public isOk(): boolean { + return this._value !== OK; } /** * @returns `true` if the `Result` is an `Err`. */ - public isErr(): this is Err { - return this._error_ !== ANY; + public isErr(): boolean { + return this._error !== ERR; } /** * @returns `true` if the `Result` is an `Ok` and and the value inside of it matches a predicate. * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. */ - public isOkAnd( - predicate: (value: T) => boolean, - thisArg?: any - ): this is Ok { - return this.isOk() && predicate.call(thisArg, this._value_); + public isOkAnd(predicate: (value: T) => boolean, thisArg?: any): boolean { + return this.isOk() && predicate.call(thisArg, this._value); } /** * @returns `true` if the `Result` is an `Err` and and the error inside of it matches a predicate. * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. */ - public isErrAnd( - predicate: (value: T) => boolean, - thisArg?: any - ): this is Err { - return this.isErr() && predicate.call(thisArg, this._value_); + public isErrAnd(predicate: (error: E) => boolean, thisArg?: any): boolean { + return this.isErr() && predicate.call(thisArg, this._error); } /** @@ -178,7 +177,7 @@ export class Result { */ public isSame(other: unknown): boolean { return Result.isResult(other) - ? Object.is(this._value_, other._value_) + ? Object.is(this._value, other._value) : false; } @@ -190,12 +189,12 @@ export class Result { */ public isSameErr(other: unknown): boolean { return Result.isResult(other) - ? Object.is(this._error_, other._error_) + ? Object.is(this._error, other._error) : false; } public and(resultB: Result): Result { - return this.isOk() ? resultB : this; + return this.isOk() ? resultB : (this as Err); } /** @@ -208,7 +207,9 @@ export class Result { getResultB: (value: T) => Result, thisArg?: any ): Result { - return this.isOk() ? getResultB.call(thisArg, this._value_) : this; + return this.isOk() + ? getResultB.call(thisArg, this._value) + : (this as Err); } /** @@ -240,8 +241,8 @@ export class Result { */ public flatten(): Result, E | UnwrapErr> { return this.isOk() && - Result.isResult, UnwrapErr>(this._value_) - ? this._value_ + Result.isResult, UnwrapErr>(this._value) + ? this._value : (this as Result, E | UnwrapErr>); } @@ -253,18 +254,38 @@ export class Result { * @returns `Err` if the `Result` is `Err`, otherwise returns `Ok(fn(value))`. */ public map(fn: (value: T) => U, thisArg?: any): Result { - return this.isOk() ? Result.Ok(fn.call(thisArg, this._value_)) : this; + return this.isOk() + ? Result.Ok(fn.call(thisArg, this._value)) + : (this as Err); } /** - * Transposes a `Result` of an `Option` into an `Option` of a `Result`. - * `Ok(Some(_))` and `Err(_)` will be mapped to `Some(Ok(_))` and `Some(Err(_))`. + * Maps a `Result` to `Result` by applying a function to a contained `Err` value, leaving an `Ok` value untouched. + * + * This function can be used to pass through a successful result while handling an error. + * + * @param fn - A function that maps a error to another error + * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. + * @returns `Ok` if the `Result` is `Ok`, otherwise returns `Err(fn(error))`. + */ + public mapErr(fn: (error: E) => U, thisArg?: any): Result { + return this.isErr() + ? Result.Err(fn.call(thisArg, this._error)) + : (this as Ok); + } + + /** + * Transposes a `Result(Option)` into `Option(Result)`. + * + * - `Ok(Some(_))` will be mapped to `Some(Ok(_)) + * - `Err(_)` will be mapped to `Some(Err(_))`. + * - `Ok(_)` will be mapped to `Some(Ok(_))`. */ public transpose(): Option, E>> { return this.isOk() - ? Option.isOption>(this._value_) - ? this._value_.map(Result.Ok) - : Option.Some(this as Result, E>) + ? Option.isOption>(this._value) + ? this._value.map(Result.Ok) + : Option.Some(this) : Option.None; } @@ -272,14 +293,14 @@ export class Result { * Converts from `Result` to `Option` and discarding the error, if any. */ public ok(): Option { - return this.isOk() ? Option.Some(this._value_) : Option.None; + return this.isOk() ? Option.Some(this._value) : Option.None; } /** * Converts from `Result` to `Option` and discarding the value, if any. */ public err(): Option { - return this.isErr() ? Option.Some(this._error_) : Option.None; + return this.isErr() ? Option.Some(this._error) : Option.None; } /** @@ -289,11 +310,9 @@ export class Result { * * @param message - Optional Error message */ - public unwrap( - message = "called `Result.unwrap()` on an `Err` error: " + this._error_ - ): T { + public unwrap(message = "called `Result.unwrap()` on an `Err`"): T { if (this.isOk()) { - return this._value_; + return this._value; } throw new Error(message); } @@ -311,7 +330,7 @@ export class Result { */ public unwrapOr(defaultValue: U): T | U; public unwrapOr(defaultValue?: T): T | undefined { - return this.isOk() ? this._value_ : defaultValue; + return this.isOk() ? this._value : defaultValue; } /** @@ -320,7 +339,7 @@ export class Result { * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. */ public unwrapOrElse(fn: () => U, thisArg?: any): T | U { - return this.isOk() ? this._value_ : fn.call(thisArg); + return this.isOk() ? this._value : fn.call(thisArg); } /** @@ -331,10 +350,10 @@ export class Result { * @param message - Optional Error message */ public unwrapErr( - message = "called `Result.unwrapErr()` on an `Ok` value: " + this._value_ - ): T { + message = "called `Result.unwrapErr()` on an `Ok` value" + ): E { if (this.isErr()) { - return this._value_; + return this._error; } throw new Error(message); } @@ -352,7 +371,7 @@ export class Result { */ public unwrapErrOr(defaultError: U): E | U; public unwrapErrOr(defaultError?: E): E | undefined { - return this.isErr() ? this._error_ : defaultError; + return this.isErr() ? this._error : defaultError; } /** @@ -361,7 +380,22 @@ export class Result { * @param thisArg - If provided, it will be used as the this value for each invocation of predicate. If it is not provided, `undefined` is used instead. */ public unwrapErrOrElse(fn: () => U, thisArg?: any): E | U { - return this.isErr() ? this._error_ : fn.call(thisArg); + return this.isErr() ? this._error : fn.call(thisArg); + } + + /** + * Extract the value from an `Result` in a way that handles both the `Ok` and `Err` cases. + * + * @param Ok - A function that returns a value if the `Result` is a `Ok`. + * @param Err - A function that returns a value if the `Result` is a `Err`. + * @returns The value returned by the provided function. + */ + public match(Ok: (value: T) => U, Err: (error: E) => U): U { + return this.isOk() ? Ok(this._value) : Err(this._error); + } + + public toString(): string { + return this.isOk() ? `Ok(${this._value})` : `Err(${this._error})`; } } @@ -369,12 +403,12 @@ export class Result { * @param value - A value of type `T` * @returns Wrap a value into an `Result`. */ -export const Ok = Result.Ok; +export const Ok = /* @__PURE__ */ (() => Result.Ok)(); export type Ok = Result; /** * @param error - An error of type `E` * @returns Wrap an error into an `Result`. */ -export const Err = Result.Err; +export const Err = /* @__PURE__ */ (() => Result.Err)(); export type Err = Result; diff --git a/src/utils.ts b/src/utils.ts index c1b811e..55a9e3a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,9 @@ /** A meaningless value */ -export const ANY: any = /* @__PURE__ */ Symbol.for("@ANY\u2009$"); -export const OPTION = /* @__PURE__ */ Symbol.for("@OPTION\u2009$"); -export const RESULT = /* @__PURE__ */ Symbol.for("@RESULT\u2009$"); +export const NONE: any = /* @__PURE__ */ Symbol.for("$NONE$\u2009"); +export const OK: any = /* @__PURE__ */ Symbol.for("$OK$\u2009"); +export const ERR: any = /* @__PURE__ */ Symbol.for("$ERR$\u2009"); +export const OPTION = /* @__PURE__ */ Symbol.for("$OPTION$\u2009"); +export const RESULT = /* @__PURE__ */ Symbol.for("$RESULT$\u2009"); export const positiveNumber = (index: number): boolean => index >= 0; diff --git a/test/array.test.ts b/test/array.test.ts new file mode 100644 index 0000000..72c643e --- /dev/null +++ b/test/array.test.ts @@ -0,0 +1,164 @@ +import "../src/patches/array"; +import { expect, describe, it } from "vitest"; + +import { None, Some } from "../src"; + +describe("Array.$filterMap", () => { + it("should return an array of mapped values for truthy predicates", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$filterMap(value => + value % 2 === 0 ? Some(value * 2) : None + ); + expect(result).toEqual([4, 8]); + }); + + it("should return an empty array for falsy predicates", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$filterMap(value => + value > 5 ? Some(value * 2) : None + ); + expect(result).toEqual([]); + }); +}); + +describe("Array.$mapWhile", () => { + it("should return an array of mapped values until the predicate is falsy", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$mapWhile(value => + value < 4 ? Some(value * 2) : None + ); + expect(result).toEqual([2, 4, 6]); + }); + + it("should return an empty array if the first value is falsy", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$mapWhile(value => + value > 5 ? Some(value * 2) : None + ); + expect(result).toEqual([]); + }); +}); + +describe("Array.$reduceWhile", () => { + it("should return the accumulated value until the predicate is falsy", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$reduceWhile( + (acc, value) => (value < 4 ? Some(acc + value) : None), + 0 + ); + expect(result).toEqual(6); + }); + + it("should return the initial value if the first value is falsy", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$reduceWhile( + (acc, value) => (value > 5 ? Some(acc + value) : None), + 0 + ); + expect(result).toEqual(0); + }); +}); + +describe("Array.$firstIndex", () => { + it("should return the index of the first element that matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$firstIndex(value => value > 2); + expect(result).toEqual(Some(2)); + }); + + it("should return None if no element matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$firstIndex(value => value > 5); + expect(result).toBe(None); + }); +}); + +describe("Array.$lastIndex", () => { + it("should return the index of the last element that matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$lastIndex(value => value > 2); + expect(result).toEqual(Some(4)); + }); + + it("should return None if no element matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$lastIndex(value => value > 5); + expect(result).toBe(None); + }); +}); + +describe("Array.$first", () => { + it("should return the first element", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$first(); + expect(result).toEqual(Some(1)); + }); + + it("should return the first element that matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$first(value => value > 2); + expect(result).toEqual(Some(3)); + }); + + it("should return None if no element matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$first(value => value > 5); + expect(result).toBe(None); + }); +}); + +describe("Array.$last", () => { + it("should return the last element", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$last(); + expect(result).toEqual(Some(5)); + }); + + it("should return the last element that matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$last(value => value > 2); + expect(result).toEqual(Some(5)); + }); + + it("should return None if no element matches the predicate", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$last(value => value > 5); + expect(result).toBe(None); + }); +}); + +describe("Array.$firstMap", () => { + it("should return the first mapped value that is Some", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$firstMap(value => + value > 2 ? Some(value * 2) : None + ); + expect(result).toEqual(Some(6)); + }); + + it("should return None if no mapped value is Some", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$firstMap(value => + value > 5 ? Some(value * 2) : None + ); + expect(result).toBe(None); + }); + + describe("Array.$lastMap", () => { + it("should return the last mapped value that is Some", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$lastMap(value => + value > 2 ? Some(value * 2) : None + ); + expect(result).toEqual(Some(10)); + }); + + it("should return None if no mapped value is Some", () => { + const array = [1, 2, 3, 4, 5]; + const result = array.$lastMap(value => + value > 5 ? Some(value * 2) : None + ); + expect(result).toEqual(None); + }); + }); +}); diff --git a/test/option.test.ts b/test/option.test.ts new file mode 100644 index 0000000..2b6c1d8 --- /dev/null +++ b/test/option.test.ts @@ -0,0 +1,506 @@ +import { describe, it, expect, vi } from "vitest"; + +import { Option, Some, None, Err, Ok } from "../src"; + +describe("Option", () => { + describe("Some", () => { + it("creates a Some with a value", () => { + const some = Some("hello"); + expect(some.isSome()).toBe(true); + expect(some.unwrap()).toBe("hello"); + }); + }); + + describe("None", () => { + it("creates a None", () => { + expect(None.isNone()).toBe(true); + expect(() => None.unwrap()).toThrow(); + }); + }); + + describe("from", () => { + it("creates a Some for a truthy value", () => { + const some = Option.from("hello"); + expect(some.isSome()).toBe(true); + expect(some.unwrap()).toBe("hello"); + }); + + it("creates a None for a falsy value", () => { + const none = Option.from(""); + expect(none.isNone()).toBe(true); + expect(() => none.unwrap()).toThrow(); + }); + + it("creates an Option with custom predicate", () => { + const some = Option.from("hello", x => x.length > 3); + expect(some.isSome()).toBe(true); + expect(some.unwrap()).toBe("hello"); + + const none = Option.from("hello", x => x.length > 10); + expect(none.isNone()).toBe(true); + expect(() => none.unwrap()).toThrow(); + }); + }); + + describe("isSome", () => { + it("returns true for a Some", () => { + const some = Some("hello"); + expect(some.isSome()).toBe(true); + }); + + it("returns false for a None", () => { + expect(None.isSome()).toBe(false); + const option = Some(11); + if (option.isSome()) { + expect(option.isNone()).toBe(false); + } + }); + }); + + describe("isNone", () => { + it("returns true for a None", () => { + expect(None.isNone()).toBe(true); + }); + + it("returns false for a Some", () => { + const some = Some("hello"); + expect(some.isNone()).toBe(false); + }); + }); + + describe("unwrap", () => { + it("returns the value for a Some", () => { + const some = Some("hello"); + expect(some.unwrap()).toBe("hello"); + }); + + it("throws an error for a None", () => { + expect(() => None.unwrap()).toThrow(); + }); + }); + + describe("unwrapOr", () => { + it("returns the value for a Some", () => { + const some = Some("hello"); + expect(some.unwrapOr("world")).toBe("hello"); + }); + + it("returns the default value for a None", () => { + expect(None.unwrapOr("world")).toBe("world"); + }); + }); + + describe("unwrapOrElse", () => { + it("returns the value for a Some", () => { + const some = Some("hello"); + expect(some.unwrapOrElse(() => "world")).toBe("hello"); + }); + + it("returns the default value for a None", () => { + expect(None.unwrapOrElse(() => "world")).toBe("world"); + }); + }); + + describe("map", () => { + it("applies a function to a Some", () => { + const some = Some("hello"); + const mapped = some.map(x => x.toUpperCase()); + expect(mapped.isSome()).toBe(true); + expect(mapped.unwrap()).toBe("HELLO"); + }); + + it("returns None for a None", () => { + const mapped = None.map((x: string) => x.toUpperCase()); + expect(mapped.isNone()).toBe(true); + expect(() => mapped.unwrap()).toThrow(); + }); + }); + + describe("filter", () => { + it("returns the Some if it satisfies the predicate", () => { + const some = Some(1); + const filtered = some.filter(x => x > 0); + expect(filtered.isSome()).toBe(true); + expect(filtered.unwrap()).toBe(1); + }); + + it("returns None if it does not satisfy the predicate", () => { + const some = Some(-1); + const filtered = some.filter(x => x > 0); + expect(filtered.isNone()).toBe(true); + expect(() => filtered.unwrap()).toThrow(); + }); + + it("returns None for a None", () => { + const filtered = None.filter(x => x > 0); + expect(filtered.isNone()).toBe(true); + expect(() => filtered.unwrap()).toThrow(); + }); + }); + + describe("or", () => { + it("returns optionB if optionA is None", () => { + const optionA = None; + const optionB = Some("hello"); + const or = optionA.or(optionB); + expect(or.isSome()).toBe(true); + expect(or.isSame(optionB)).toBe(true); + }); + + it("returns optionA if optionA is Some", () => { + const optionA = Some("world"); + const optionB = Some("hello"); + const or = optionA.or(optionB); + expect(or.isSome()).toBe(true); + expect(or.isSame(optionA)).toBe(true); + }); + }); + + describe("orElse", () => { + it("returns the first Option if it is Some", () => { + const some = Some("hello"); + const orElse = some.orElse(() => Some("world")); + expect(orElse.isSome()).toBe(true); + expect(orElse.unwrap()).toBe("hello"); + }); + + it("returns the second Option if the first is None", () => { + const orElse = None.orElse(() => Some("world")); + expect(orElse.isSome()).toBe(true); + expect(orElse.unwrap()).toBe("world"); + }); + }); + + describe("isSomeAnd", () => { + it("returns true when the input is Some and the predicate is true", () => { + const input = Some("hello"); + const predicate = (value: string) => value.length === 5; + const result = input.isSomeAnd(predicate); + expect(result).toBe(true); + }); + + it("returns false when the input is Some and the predicate is false", () => { + const input = Some("hello"); + const predicate = (value: string) => value.length === 4; + const result = input.isSomeAnd(predicate); + expect(result).toBe(false); + }); + + it("returns false when the input is None", () => { + const input = None; + const predicate = (value: string) => value.length === 5; + const result = input.isSomeAnd(predicate); + expect(result).toBe(false); + }); + + it("calls the predicate with the provided thisArg", () => { + const input = Some("hello"); + const predicate = function (this: any, value: string) { + return value === this.message; + }; + const thisArg = { message: "hello" }; + const result = input.isSomeAnd(predicate, thisArg); + expect(result).toBe(true); + }); + }); + + describe("isSame", () => { + it("returns true when the input is the same Some value", () => { + const some1 = Some("hello"); + const some2 = Some("hello"); + const result = some1.isSame(some2); + expect(result).toBe(true); + }); + + it("returns false when the input is a different Some value", () => { + const some1 = Some("hello"); + const some2 = Some("world"); + const result = some1.isSame(some2); + expect(result).toBe(false); + }); + + it("returns false when the input is None", () => { + const some = Some("hello"); + const none = None; + const result = some.isSame(none); + expect(result).toBe(false); + }); + + it("returns false when the input is a different type", () => { + const some = Some("hello"); + const obj = { value: "hello" }; + const result = some.isSame(obj); + expect(result).toBe(false); + }); + }); + + describe("and", () => { + it("returns the second Option if both are Some", () => { + const some1 = Some("hello"); + const some2 = Some("world"); + const and = some1.and(some2); + expect(and.isSome()).toBe(true); + expect(and.unwrap()).toBe("world"); + }); + + it("returns None if the first is None", () => { + const some = Some("world"); + const and = None.and(some); + expect(and.isNone()).toBe(true); + expect(() => and.unwrap()).toThrow(); + }); + + it("returns None if the second is None", () => { + const some = Some("hello"); + const and = some.and(None); + expect(and.isNone()).toBe(true); + expect(() => and.unwrap()).toThrow(); + }); + + it("returns None if both are None", () => { + const and = None.and(None); + expect(and.isNone()).toBe(true); + expect(() => and.unwrap()).toThrow(); + }); + }); + + describe("andThen", () => { + it("applies a function to a Some and returns the result", () => { + const some = Some("hello"); + const andThen = some.andThen(x => Some(x.toUpperCase())); + expect(andThen.isSome()).toBe(true); + expect(andThen.unwrap()).toBe("HELLO"); + }); + + it("returns None for a None", () => { + const andThen = None.andThen(x => Some(x.toUpperCase())); + expect(andThen.isNone()).toBe(true); + expect(() => andThen.unwrap()).toThrow(); + }); + }); + + describe("xor", () => { + it("returns None if both are None", () => { + const xor = None.xor(None); + expect(xor.isNone()).toBe(true); + expect(() => xor.unwrap()).toThrow(); + }); + + it("returns the first Option if it is Some and the second is None", () => { + const some = Some("hello"); + const xor = some.xor(None); + expect(xor.isSome()).toBe(true); + expect(xor.unwrap()).toBe("hello"); + }); + + it("returns the second Option if it is Some and the first is None", () => { + const some = Some("world"); + const xor = None.xor(some); + expect(xor.isSome()).toBe(true); + expect(xor.unwrap()).toBe("world"); + }); + + it("returns None if both are Some", () => { + const some1 = Some("hello"); + const some2 = Some("world"); + const xor = some1.xor(some2); + expect(xor.isNone()).toBe(true); + expect(() => xor.unwrap()).toThrow(); + }); + }); + + describe("zip", () => { + it("returns a Some of a tuple if both are Some", () => { + const some1 = Some("hello"); + const some2 = Some("world"); + const zip = some1.zip(some2); + expect(zip.isSome()).toBe(true); + expect(zip.unwrap()).toEqual(["hello", "world"]); + }); + + it("returns None if the first is None", () => { + const some = Some("world"); + const zip = None.zip(some); + expect(zip.isNone()).toBe(true); + expect(() => zip.unwrap()).toThrow(); + }); + + it("returns None if the second is None", () => { + const some = Some("hello"); + const zip = some.zip(None); + expect(zip.isNone()).toBe(true); + expect(() => zip.unwrap()).toThrow(); + }); + + it("returns None if both are None", () => { + const zip = None.zip(None); + expect(zip.isNone()).toBe(true); + expect(() => zip.unwrap()).toThrow(); + }); + }); + + describe("zipWith", () => { + it("returns a Some with the result of the function when both options are Some", () => { + const some1 = Option.Some(2); + const some2 = Option.Some(3); + const result = some1.zipWith(some2, (a, b) => a + b); + expect(result).toEqual(Option.Some(5)); + }); + + it("returns a None when the first option is None", () => { + const some2 = Option.Some(3); + const result = None.zipWith(some2, (a, b) => a + b); + expect(result).toEqual(None); + }); + + it("returns a None when the second option is None", () => { + const some1 = Option.Some(2); + const result = some1.zipWith(None, (a, b) => a + b); + expect(result).toEqual(None); + }); + + it("returns a None when both options are None", () => { + const result = None.zipWith(None, (a, b) => a + b); + expect(result).toEqual(None); + }); + }); + + describe("unzip", () => { + it("returns a tuple of Some values when the input is Some([a,b])", () => { + const [a, b] = Some(["hello", "world"]).unzip(); + expect(a).toEqual(Some("hello")); + expect(b).toEqual(Some("world")); + }); + + it("returns a tuple of Some values when the input is Some([a,b])", () => { + const [a, b] = None.unzip(); + expect(a).toBe(None); + expect(b).toBe(None); + }); + + it("returns a tuple of Some values when the input is Some([a,b])", () => { + const [a, b] = Some("").unzip(); + expect(a).toBe(None); + expect(b).toBe(None); + }); + }); + + describe("flatten", () => { + it("returns the inner Some when the input is Some(Some)", () => { + const someSome = Some(Some("hello")); + const flattened = someSome.flatten(); + expect(flattened).toEqual(Some("hello")); + }); + + it("returns None when the input is Some(None)", () => { + const someNone = Some(None); + const flattened = someNone.flatten(); + expect(flattened).toEqual(None); + }); + + it("returns None when the input is None", () => { + const none = None; + const flattened = none.flatten(); + expect(flattened).toEqual(None); + }); + }); + + describe("transpose", () => { + it("returns Ok(value) when the input is Some", () => { + const input = Some("hello"); + const transposed = input.transpose(); + expect(transposed).toEqual(Ok(Some("hello"))); + }); + + it("returns Ok(None) when the input is None", () => { + const input = None; + const transposed = input.transpose(); + expect(transposed).toEqual(Ok(None)); + }); + + it("returns Ok(Some(value)) when the input is Some(Ok(value))", () => { + const input = Some(Ok("hello")); + const transposed = input.transpose(); + expect(transposed).toEqual(Ok(Some("hello"))); + }); + + it("returns Err(error) when the input is Some(Err(error))", () => { + const input = Some(Err(42)); + const transposed = input.transpose(); + expect(transposed).toEqual(Err(42)); + }); + }); + + describe("okOr", () => { + it("returns Ok(value) when the input is Some", () => { + const input = Some("hello"); + const result = input.okOr("error"); + expect(result).toEqual(Ok("hello")); + }); + + it("returns Err(error) when the input is None", () => { + const input = None; + const result = input.okOr("error"); + expect(result).toEqual(Err("error")); + }); + }); + + describe("okOrElse", () => { + it("returns Ok(value) when the input is Some", () => { + const input = Some("hello"); + const result = input.okOrElse(() => "error"); + expect(result).toEqual(Ok("hello")); + }); + + it("returns Err(error()) when the input is None", () => { + const input = None; + const result = input.okOrElse(() => "error"); + expect(result).toEqual(Err("error")); + }); + + it("calls the error function with the provided thisArg", () => { + const input = None; + const thisArg = { message: "error" }; + const errorFn = function () { + return this.message; + }; + const result = input.okOrElse(errorFn, thisArg); + expect(result).toEqual(Err("error")); + }); + }); + + describe("Option.match", () => { + it("should call the Some function if the Option is a Some", () => { + const value = 42; + const option = Option.Some(value); + const someFn = vi.fn().mockReturnValue("Some value"); + const noneFn = vi.fn().mockReturnValue("None value"); + const result = option.match(someFn, noneFn); + expect(someFn).toHaveBeenCalledWith(value); + expect(noneFn).not.toHaveBeenCalled(); + expect(result).toBe("Some value"); + }); + + it("should call the None function if the Option is a None", () => { + const option = Option.None; + const someFn = vi.fn().mockReturnValue("Some value"); + const noneFn = vi.fn().mockReturnValue("None value"); + const result = option.match(someFn, noneFn); + expect(someFn).not.toHaveBeenCalled(); + expect(noneFn).toHaveBeenCalled(); + expect(result).toBe("None value"); + }); + }); + + describe("toString", () => { + it('returns "Some(value)" for a Some', () => { + const some = Some("hello"); + expect(some.toString()).toBe("Some(hello)"); + }); + + it('returns "None" for a None', () => { + const none = None; + expect(none.toString()).toBe("None"); + }); + }); +}); diff --git a/test/result.test.ts b/test/result.test.ts new file mode 100644 index 0000000..09ce925 --- /dev/null +++ b/test/result.test.ts @@ -0,0 +1,613 @@ +import { describe, it, expect } from "vitest"; + +import { None, Result, Some, Ok, Err } from "../src"; + +describe("Result", () => { + describe("Ok", () => { + it("should return a Result object with the given value", () => { + const value = 42; + const result = Ok(value); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + expect(result.unwrap()).toBe(value); + }); + }); + + describe("Err", () => { + it("should return a Result object with the given error", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(false); + expect(result.isErr()).toBe(true); + expect(result.unwrapErr()).toBe(error); + }); + }); + + describe("from", () => { + it("should return an Ok result if the value is not an Error", () => { + const value = 42; + const result = Result.from(value); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + expect(result.unwrap()).toBe(value); + }); + + it("should return an Err result if the value is an Error", () => { + const error = new Error("Something went wrong"); + const result = Result.from(error); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(false); + expect(result.isErr()).toBe(true); + expect(result.unwrapErr()).toBe(error); + }); + + it("should return a Result base on predicate", () => { + const value = 42; + const result = Result.from(value, x => x > 10); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + expect(result.unwrap()).toBe(value); + }); + }); + + describe("try", () => { + it("should return an Ok result if the function does not throw an error", () => { + const fn = () => 42; + const result = Result.try(fn); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + expect(result.unwrap()).toBe(42); + }); + + it("should return an Err result if the function throws an error", () => { + const fn = () => { + throw new Error("Something went wrong"); + }; + const result = Result.try(fn); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(false); + expect(result.isErr()).toBe(true); + expect(result.unwrapErr()).toBeInstanceOf(Error); + }); + }); + + describe("tryAsync", () => { + it("should return an Ok result if the function does not throw an error", async () => { + const fn = async () => 42; + const result = await Result.tryAsync(fn); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(true); + expect(result.isErr()).toBe(false); + expect(result.unwrap()).toBe(42); + }); + + it("should return an Err result if the function throws an error", async () => { + const fn = async () => { + throw new Error("Something went wrong"); + }; + const result = await Result.tryAsync(fn); + expect(result).toBeInstanceOf(Result); + expect(result.isOk()).toBe(false); + expect(result.isErr()).toBe(true); + expect(result.unwrapErr()).toBeInstanceOf(Error); + }); + }); + + describe("[Symbol.iterator]", () => { + it("should return an iterator that yields the value of an Ok result", () => { + const result = Ok(42); + const iterator = result[Symbol.iterator](); + expect(iterator.next().value).toBe(42); + expect(iterator.next().done).toBe(true); + }); + + it("should return an iterator that does not yield any values for an Err result", () => { + const result = Err(new Error("Something went wrong")); + const iterator = result[Symbol.iterator](); + expect(iterator.next().done).toBe(true); + }); + }); + + describe("isResult", () => { + it("should return true for a Result object", () => { + const result = Ok(42); + expect(Result.isResult(result)).toBe(true); + }); + + it("should return false for a non-Result object", () => { + const obj = { foo: "bar" }; + expect(Result.isResult(obj)).toBe(false); + }); + }); + + describe("isOk", () => { + it("should return true for an Ok result", () => { + const value = 42; + const result = Ok(value); + expect(result.isOk()).toBe(true); + }); + + it("should return false for an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result.isOk()).toBe(false); + }); + }); + + describe("isErr", () => { + it("should return true for an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result.isErr()).toBe(true); + }); + + it("should return false for an Ok result", () => { + const value = 42; + const result = Ok(value); + expect(result.isErr()).toBe(false); + }); + }); + + describe("unwrap", () => { + it("should return the value for an Ok result", () => { + const value = 42; + const result = Ok(value); + expect(result.unwrap()).toBe(value); + }); + + it("should throw an error for an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(() => result.unwrap()).toThrow(); + }); + }); + + describe("unwrapErr", () => { + it("should return the error for an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result.unwrapErr()).toBe(error); + }); + + it("should throw an error for an Ok result", () => { + const value = 42; + const result = Ok(value); + expect(() => result.unwrapErr()).toThrow(); + }); + }); + + describe("unwrapOr", () => { + it("should return the value for an Ok result", () => { + const value = 42; + const result = Ok(value); + expect(result.unwrapOr(0)).toBe(value); + }); + + it("should return the default value for an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result.unwrapOr(0)).toBe(0); + }); + }); + + describe("unwrapOrElse", () => { + it("should return the value for an Ok result", () => { + const value = 42; + const result = Ok(value); + expect(result.unwrapOrElse(() => 0)).toBe(value); + }); + + it("should return the result of the callback for an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result.unwrapOrElse(() => 0)).toBe(0); + }); + }); + + describe("map", () => { + it("should apply the callback to the value of an Ok result", () => { + const value = 42; + const result = Ok(value); + const mappedResult = result.map(x => x * 2); + expect(mappedResult.isOk()).toBe(true); + expect(mappedResult.unwrap()).toBe(value * 2); + }); + + it("should not apply the callback to an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const mappedResult = result.map(x => x * 2); + expect(mappedResult.isErr()).toBe(true); + expect(mappedResult.unwrapErr()).toBe(error); + }); + }); + + describe("mapErr", () => { + it("should apply the callback to the error of an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const mappedResult = result.mapErr( + e => new Error(e.message.toUpperCase()) + ); + expect(mappedResult.isErr()).toBe(true); + expect(mappedResult.unwrapErr().message).toBe("SOMETHING WENT WRONG"); + }); + + it("should not apply the callback to an Ok result", () => { + const value = 42; + const result = Ok(value); + const mappedResult = result.mapErr( + e => new Error(e.message.toUpperCase()) + ); + expect(mappedResult.isOk()).toBe(true); + expect(mappedResult.unwrap()).toBe(value); + }); + }); + + describe("and", () => { + it("should return the other result for an Ok result", () => { + const value1 = 42; + const value2 = "hello"; + const result1 = Ok(value1); + const result2 = Ok(value2); + const andResult = result1.and(result2); + expect(andResult.isOk()).toBe(true); + expect(andResult.unwrap()).toBe(value2); + }); + + it("should return the Err result for an Err result", () => { + const error1 = new Error("Something went wrong"); + const error2 = new Error("Another thing went wrong"); + const result1 = Err(error1); + const result2 = Err(error2); + const andResult = result1.and(result2); + expect(andResult.isErr()).toBe(true); + expect(andResult.unwrapErr()).toBe(error1); + }); + }); + + describe("andThen", () => { + it("should apply the callback to the value of an Ok result", () => { + const value1 = 42; + const value2 = "hello"; + const result1 = Ok(value1); + const result2 = Ok(value2); + const andThenResult = result1.andThen(() => result2); + expect(andThenResult.isOk()).toBe(true); + expect(andThenResult.unwrap()).toBe(value2); + }); + + it("should not apply the callback to an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const andThenResult = result.andThen(() => Ok("hello")); + expect(andThenResult.isErr()).toBe(true); + expect(andThenResult.unwrapErr()).toBe(error); + }); + }); + + describe("or", () => { + it("should return the first result for an Ok result", () => { + const value1 = 42; + const value2 = "hello"; + const result1 = Ok(value1); + const result2 = Ok(value2); + const orResult = result1.or(result2); + expect(orResult.isOk()).toBe(true); + expect(orResult.unwrap()).toBe(value1); + }); + + it("should return the second result for an Err result", () => { + const error1 = new Error("Something went wrong"); + const error2 = new Error("Another thing went wrong"); + const result1 = Err(error1); + const result2 = Err(error2); + const orResult = result1.or(result2); + expect(orResult.isErr()).toBe(true); + expect(orResult.unwrapErr()).toBe(error2); + }); + }); + + describe("orElse", () => { + it("should not apply the callback to an Ok result", () => { + const value = 42; + const result = Ok(value); + const orElseResult = result.orElse(() => Ok("hello")); + expect(orElseResult.isOk()).toBe(true); + expect(orElseResult.unwrap()).toBe(value); + }); + + it("should apply the callback to the error of an Err result", () => { + const error1 = new Error("Something went wrong"); + const error2 = new Error("Another thing went wrong"); + const result1 = Err(error1); + const result2 = Err(error2); + const orElseResult = result1.orElse(() => result2); + expect(orElseResult.isErr()).toBe(true); + expect(orElseResult.unwrapErr()).toBe(error2); + }); + }); + + describe("match", () => { + it("should apply the callback to the value of an Ok result", () => { + const value = 42; + const result = Ok(value); + const matchResult = result.match( + x => x * 2, + () => 0 + ); + expect(matchResult).toBe(value * 2); + }); + + it("should apply the callback to the error of an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const matchResult = result.match<0 | string>( + () => 0, + e => e.message.toUpperCase() + ); + expect(matchResult).toBe("SOMETHING WENT WRONG"); + }); + + it("should map to another result", () => { + const value = 42; + const result = Ok(value); + const matchResult = result.match( + x => Ok(x * 2), + () => Err(new Error("Something went wrong")) + ); + expect(matchResult.isOk()).toBe(true); + expect(matchResult.unwrap()).toBe(value * 2); + }); + + it("should map to an option", () => { + const value = 42; + const result = Ok(value); + const matchResult = result.match( + x => Some(x * 2), + () => None + ); + expect(matchResult).toEqual(Some(value * 2)); + }); + }); + + describe("isOkAnd", () => { + it("should return true if the Result is an Ok variant and the predicate returns true", () => { + const result = Ok(42); + const predicate = (value: number) => value === 42; + expect(result.isOkAnd(predicate)).toBe(true); + }); + + it("should return false if the Result is an Ok variant and the predicate returns false", () => { + const result = Ok(42); + const predicate = (value: number) => value === 0; + expect(result.isOkAnd(predicate)).toBe(false); + }); + + it("should return false if the Result is an Err variant", () => { + const result = Err(new Error("Something went wrong")); + const predicate = (value: number) => value === 42; + expect(result.isOkAnd(predicate)).toBe(false); + }); + }); + + describe("isErrAnd", () => { + it("should return true if the Result is an Err variant and the predicate returns true", () => { + const result = Err(new Error("Something went wrong")); + const predicate = (error: Error) => + error.message === "Something went wrong"; + expect(result.isErrAnd(predicate)).toBe(true); + }); + + it("should return false if the Result is an Err variant and the predicate returns false", () => { + const result = Err(new Error("Something went wrong")); + const predicate = (error: Error) => + error.message === "Something else went wrong"; + expect(result.isErrAnd(predicate)).toBe(false); + }); + + it("should return false if the Result is an Ok variant", () => { + const result = Ok(42); + const predicate = (error: Error) => + error.message === "Something went wrong"; + expect(result.isErrAnd(predicate)).toBe(false); + }); + }); + + describe("isSame", () => { + it("should return true if the Result is an Ok variant and the value is equal to the given value", () => { + const result = Ok(42); + expect(result.isSame(Ok(42))).toBe(true); + }); + + it("should return false if the Result is an Ok variant and the value is not equal to the given value", () => { + const result = Ok(42); + expect(result.isSame(Ok(0))).toBe(false); + }); + + it("should return false if the Result is an Err variant", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result.isSame(Err(error))).toBe(true); + expect(result.isSame(Some(error))).toBe(false); + }); + }); + + describe("isSameErr", () => { + it("should return true if the Result is an Err variant and the error message is equal to the given message", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + expect(result.isSameErr(Err(error))).toBe(true); + }); + + it("should return false if the Result is an Err variant and the error message is not equal to the given message", () => { + const result = Err(new Error("Something went wrong")); + expect(result.isSameErr("Something else went wrong")).toBe(false); + }); + + it("should return false if the Result is an Ok variant", () => { + const result = Ok(42); + expect(result.isSameErr("Something went wrong")).toBe(false); + }); + }); + + describe("flatten", () => { + it("should return an Ok result if the inner Result is an Ok variant", () => { + const innerResult = Ok(42); + const result = Ok(innerResult); + const flattenedResult = result.flatten(); + expect(flattenedResult).toBeInstanceOf(Result); + expect(flattenedResult.isOk()).toBe(true); + expect(flattenedResult.unwrap()).toBe(42); + }); + + it("should return an Err result if the outer Result is an Err variant", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const flattenedResult = result.flatten(); + expect(flattenedResult).toBeInstanceOf(Result); + expect(flattenedResult.isErr()).toBe(true); + expect(flattenedResult.unwrapErr()).toBe(error); + }); + + it("should return an Err result if the inner Result is an Err variant", () => { + const error = new Error("Something went wrong"); + const innerResult = Err(error); + const result = Ok(innerResult); + const flattenedResult = result.flatten(); + expect(flattenedResult).toBeInstanceOf(Result); + expect(flattenedResult.isErr()).toBe(true); + expect(flattenedResult.unwrapErr()).toBe(error); + }); + }); + + describe("transpose", () => { + it("should return Some(Ok(value)) if the Result is Ok(Some(value))", () => { + const result = Ok(Some(42)); + const transposedResult = result.transpose(); + expect(transposedResult.isSome()).toBe(true); + expect(transposedResult.unwrap()).toBeInstanceOf(Result); + expect(transposedResult.unwrap().isOk()).toBe(true); + expect(transposedResult.unwrap().unwrap()).toBe(42); + }); + + it("should return None if the Result is Err(error)", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const transposedResult = result.transpose(); + expect(transposedResult.isNone()).toBe(true); + }); + + it("should return None if the Result is Ok(None)", () => { + const result = Ok(None); + const transposedResult = result.transpose(); + expect(transposedResult.isNone()).toBe(true); + }); + + it("should return None if the Result is Err(None)", () => { + const result = Err(None); + const transposedResult = result.transpose(); + expect(transposedResult.isNone()).toBe(true); + }); + + it("should return Some(Ok(value) if the Result is Ok(value)", () => { + const result = Ok(42); + const transposedResult = result.transpose(); + expect(transposedResult.isSome()).toBe(true); + expect(transposedResult.unwrap()).toBeInstanceOf(Result); + expect(transposedResult.unwrap().isOk()).toBe(true); + expect(transposedResult.unwrap().unwrap()).toBe(42); + }); + }); + + describe("ok", () => { + it("should return Some(value) if the Result is Ok(value)", () => { + const value = 42; + const result = Ok(value); + const okResult = result.ok(); + expect(okResult.isSome()).toBe(true); + expect(okResult.unwrap()).toBe(value); + }); + + it("should return None if the Result is Err(error)", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const okResult = result.ok(); + expect(okResult.isNone()).toBe(true); + }); + }); + + describe("err", () => { + it("should return Some(error) if the Result is Err(error)", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const errResult = result.err(); + expect(errResult.isSome()).toBe(true); + expect(errResult.unwrap()).toBe(error); + }); + + it("should return None if the Result is Ok(value)", () => { + const value = 42; + const result = Ok(value); + const errResult = result.err(); + expect(errResult.isNone()).toBe(true); + }); + }); + + describe("unwrapErrOr", () => { + it("should return the error if the Result is an Err variant", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const defaultError = new Error("Default error"); + const unwrappedError = result.unwrapErrOr(defaultError); + expect(unwrappedError).toBe(error); + }); + + it("should return the default error if the Result is an Ok variant", () => { + const value = 42; + const result = Ok(value); + const defaultError = new Error("Default error"); + const unwrappedError = result.unwrapErrOr(defaultError); + expect(unwrappedError).toBe(defaultError); + }); + }); + + describe("unwrapErrOrElse", () => { + it("should return the error if the Result is an Err variant", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const defaultError = new Error("Default error"); + const unwrappedError = result.unwrapErrOrElse(() => defaultError); + expect(unwrappedError).toBe(error); + }); + + it("should return the result of the callback if the Result is an Ok variant", () => { + const value = 42; + const result = Ok(value); + const defaultError = new Error("Default error"); + const unwrappedError = result.unwrapErrOrElse(() => defaultError); + expect(unwrappedError).toBe(defaultError); + }); + }); + + describe("toString", () => { + it("should return a string representation of an Ok result", () => { + const value = 42; + const result = Ok(value); + const resultString = result.toString(); + expect(resultString).toBe(`Ok(${value})`); + }); + + it("should return a string representation of an Err result", () => { + const error = new Error("Something went wrong"); + const result = Err(error); + const resultString = result.toString(); + expect(resultString).toBe(`Err(${error})`); + }); + }); +});