diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3e8f521..bbfde31 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -12,12 +12,6 @@ - 📝 Use descriptive commit messages. - 📗 Update any related documentation and include any relevant screenshots. - NOTE: Pull Requests from forked repositories will need to be reviewed by - a Forem Team member before any CI builds will run. Once your PR is approved - with a `/ci` reply to the PR, it will be allowed to run subsequent builds without - manual approval. ---> - ## What type of PR is this? (check all applicable) - [ ] Refactor @@ -31,19 +25,6 @@ -## Related Tickets & Documents - - - -- Related Issue # -- Closes # ## Added/updated tests? @@ -56,4 +37,5 @@ _We encourage you to keep the quality of the code by creating test._ ## [optional] What gif best describes this PR or how it makes you feel? -![alt_text](gif_link) +![alt_text](https://example.com/image.gif) + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e282a94..b35947c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,10 @@ name: CI on: push: - branches: ["main"] + branches: ["main", "beta"] pull_request: - branches: ["main"] + branches: ["main", "beta"] + jobs: build: diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..da99483 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index f8362f0..b7d7aa8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # monads This is a set of implementations of monads in TypeScript with OOP perspective. -It is a work in progress and the first monad implemented is the Either monad. * [monads](#monads) @@ -27,6 +26,26 @@ It is a work in progress and the first monad implemented is the Either monad. * [Using `map`](#using-map) * [Matching an Option](#matching-an-option) * [Checking if an Option is Some or None](#checking-if-an-option-is-some-or-none) + * [Try Monad](#try-monad) + * [Usage](#usage-2) + * [Using `map`](#using-map-1) + * [Using `flatMap`](#using-flatmap-1) + * [Matching a Try](#matching-a-try) + * [Handling errors in Infrastructure code](#handling-errors-in-infrastructure-code) + * [Checking if a Try is Success or Failure](#checking-if-a-try-is-success-or-failure) + * [Future Monad](#future-monad) + * [Usage](#usage-3) + * [Creating a Future](#creating-a-future) + * [Mapping over a Future](#mapping-over-a-future) + * [Using `flatMap`](#using-flatmap-2) + * [Using `map`](#using-map-2) + * [Evaluate a Future](#evaluate-a-future) + * [IO Monad](#io-monad) + * [Usage](#usage-4) + * [Creating an IO](#creating-an-io) + * [Mapping over an IO](#mapping-over-an-io) + * [Using `flatMap`](#using-flatmap-3) + * [Using `map`](#using-map-3) ## Installation @@ -51,7 +70,7 @@ while `Left` is used to hold an error or failure. You can create an `Either` using the static methods `Either.right` and `Either.left`. ```typescript -import { Either } from 'monads'; +import { Either } from '@leanmind/monads'; // Creating a Right const right = Either.right(42); @@ -65,7 +84,7 @@ const left = Either.left('Error'); You can create an `Either` from a failed operations using the static method `Either.catch`. ```typescript -import { Either } from 'monads'; +import { Either } from '@leanmind/monads'; const findUser = (id: number): User => { if (id === 42) { @@ -86,6 +105,8 @@ transform the value inside a `Left`. ##### Using `flatMap` and `flatMapLeft` ```typescript +import { Either } from '@leanmind/monads';m + const right = Either.right(42).flatMap(x => Either.right(x + 1)); // Right(43) const left = Either.left('Error').flatMapLeft(err => Either.left(`New ${err}`)); // Left('New Error') ``` @@ -93,6 +114,8 @@ const left = Either.left('Error').flatMapLeft(err => Either.left(`New ${err}`)); ##### Using `map` and `mapLeft` ```typescript +import { Either } from '@leanmind/monads'; + const right = Either.right(42).map(x => x + 1); // Right(43) const left = Either.left('Error').mapLeft(err => `New ${err}`); // Left('New Error') ``` @@ -102,6 +125,8 @@ const left = Either.left('Error').mapLeft(err => `New ${err}`); // Left('New Err You can use the `match` method to handle both `Right` and `Left` cases and unwrap the result. ```typescript +import { Either } from '@leanmind/monads'; + const sucess = Either.right(42).match( err => `Error: ${err}`, x => (x + 1).toString() @@ -120,6 +145,8 @@ Probably you will not need to use these methods, but they are available in case of refactoring from try-catch blocks or other situations. ```typescript +import { Either } from '@leanmind/monads'; + const right = Either.right(42); const left = Either.left('Error'); @@ -136,6 +163,8 @@ You can chain operations using the `map`, `mapLeft`, `flatMap` and `flatMapLeft` The following example demonstrates how to chain operations using the map method: ```typescript +import { Either } from '@leanmind/monads'; + const result = Either.right(42) .map(x => x + 1) .map(x => x * 2) @@ -152,7 +181,7 @@ console.log(result); // 86 Here is a complete example demonstrating the usage of the `Either` monad: ```typescript -import { Either } from 'monads'; +import { Either } from '@leanmind/monads'; function divide(a: number, b: number): Either { if (b === 0) { @@ -187,7 +216,7 @@ An `Option` is either a `Some` or a `None`. You can create an `Option` using the static methods `Option.of`. ```typescript -import { Option } from 'monads'; +import { Option } from '@leanmind/monads'; // Creating a Some const some = Option.of(42); // Some(42) @@ -201,6 +230,8 @@ const none = Option.of(null); // None You can use the `getOrElse` method to retrieve the value of an `Option` or provide a default value if it is `None`. ```typescript +import { Option } from '@leanmind/monads'; + const some = Option.of(42); some.getOrElse(0); // 42 @@ -213,6 +244,8 @@ none.getOrElse(0); // 0 You can use the `filter` method to keep the `Some` value if it satisfies a predicate. ```typescript +import { Option } from '@leanmind/monads';m + const some = Option.of(42).filter(x => x > 40); // Some(42) const none = Option.of(42).filter(x => x > 50); // None ``` @@ -224,6 +257,8 @@ You can use the `flatMap` or `map` method to transform the `Some` value. ##### Using `flatMap` ```typescript +import { Option } from '@leanmind/monads';m + const some = Option.of(42).flatMap(x => Option.of(x + 1)); // Some(43) const none = Option.of(null).flatMap(x => Option.of(x + 1)); // None ``` @@ -231,6 +266,8 @@ const none = Option.of(null).flatMap(x => Option.of(x + 1)); // None ##### Using `map` ```typescript +import { Option } from '@leanmind/monads'; + const some = Option.of(42).map(x => x + 1); // Some(43) const none = Option.of(null).map(x => x + 1); // None ``` @@ -240,6 +277,8 @@ const none = Option.of(null).map(x => x + 1); // None You can use the `match` method to handle both `Some` and `None` cases and unwrap the result. ```typescript +import { Option } from '@leanmind/monads'; + const some = Option.of(42).match( x => x + 1, () => 'No value' @@ -256,6 +295,8 @@ const none = Option.of(null).match( If needed, you can check explicitly if an `Option` is `Some` or `None` using the `isSome` and `isNone` methods. ```typescript +import { Option } from '@leanmind/monads'; + const some = Option.of(42); some.isSome(); // true some.isNone(); // false @@ -264,3 +305,218 @@ const none = Option.of(undefined); none.isSome(); // false none.isNone(); // true ``` + +## Try Monad + +The `Try` monad represents a computation that may fail. + +### Usage + +You can create a `Try` using the static method `Try.execute`. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.execute(() => 42); // Success(42) + +const failure = Try.execute(() => { + throw new Error('Error'); +}); // Failure(Error('Error')) +``` + +### Using `map` + +You can use the `map` method to transform the value inside a `Success`. + +```typescript +import { Try } from '@leanmind/monads';m + +const success = Try.execute(() => 42).map(x => x + 1); // Success(43) +``` + +### Using `flatMap` + +You can use the `flatMap` method to transform the value inside a `Success` with a fallible closure. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.execute(() => 42).flatMap(x => Try.execute(() => x + 1)); // Success(43) +``` + +### Matching a Try + +You can use the `match` method to handle both `Success` and `Failure` cases and unwrap the result. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.execute(() => 42).match( + err => `Error: ${err}`, + x => x + 1 +); // 43 + +const failure = Try.execute(() => { + throw new Error('Error'); +}).match( + err => `Error: ${err}`, + x => x + 1 +); // 'Error: Error' +``` + +### Handling errors in Infrastructure code + +Normally, Try is used to handle `Exceptions` that are raise by third party libraries + +```typescript +import { Try } from '@leanmind/monads'; + +const result = Try.execute(() => { + // Some API of a library that may throw an exception + return 42; +}).match( + err => `Error: ${err}`, + x => x + 1 +); + +console.log(result); // 43 +``` + +### Checking if a Try is Success or Failure + +If needed, you can check explicitly if a `Try` is `Success` or `Failure` using the `isSuccess` and `isFailure` methods. + +```typescript +import { Try } from '@leanmind/monads'; + +const success = Try.execute(() => 42); +success.isSuccess(); // true + +const failure = Try.execute(() => { + throw new Error('Error'); +}); +failure.isFailure(); // true +``` + +## Future Monad + +The `Future` monad represents a computation that may be executed asynchronously. + +### Usage + +#### Creating a Future + +You can create a `Future` using the static method `Future.of`. + +```typescript +import { Future } from '@leanmind/monads'; + +const future = Future.of(() => Promise.resolve(42)); +``` + +#### Mapping over a Future + +You can use the `map` or `flatMap` method to transform the computed value inside a `Future`. The operation will not +execute the transformation (_lazy evaluation_) until `complete` method is called. + +##### Using `flatMap` + +```typescript +import { Future } from '@leanmind/monads'; + +const future = Future.of(() => Promise.resolve(42)) + .flatMap(x => Future.of(() => Promise.resolve(x + 1))) + .complete( + x => console.log(x), + err => console.error(err) + ); // 43 +``` + +##### Using `map` + +```typescript +import { Future } from '@leanmind/monads'; + +const future = Future.of(() => Promise.resolve(42)) + .map(x => x + 1) + .complete( + x => console.log(x), + err => console.error(err) + ); // 43 +``` + +#### Evaluate a Future + +You can evaluate a `Future` using the `complete` method. The `complete` method takes two functions as arguments: +one for the success case and one for the failure case. + +```typescript +import { Future } from '@leanmind/monads'; + +const successFuture = Future.of(() => Promise.resolve(42)); + +await successFuture.complete( + x => console.log(x), + err => console.error(err) +); // 42 + +const failureFuture = Future.of(() => Promise.reject(new Error('Error'))); + +await failureFuture.complete( + x => console.log(x), + err => console.error(err) +); // Error('Error') +``` + +## IO Monad + +The `IO` monad represents a computation that may have side effects. + +In this way, the `IO` monad is used to encapsulate side effects in a pure functional way. + +So, you can operate as pure functions until you call the `runUnsafe` method. + +### Usage + +#### Creating an IO +You can create an `IO` using the static method `IO.of`. + +```typescript +import { IO } from '@leanmind/monads'; + +const io = IO.of(() => 42); +``` + +#### Mapping over an IO + +You can use the `flatMap` or `map` method to concatenate `IO` operations. + +The operation is not executed until you call the `runUnsafe` method. + +##### Using `flatMap` + +```typescript +import { IO } from '@leanmind/monads'; + +const io = IO.of(() => 42).flatMap(x => IO.of(() => x + 1)); + +io.run(); // 43 +``` + +##### Using `map` + +```typescript +import { IO } from '@leanmind/monads'; + +const io = IO.of(() => 42).map(x => x + 1); + +io.runUnsafe(); // 43 +``` + + + + + + + + diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..422b194 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1 @@ +module.exports = { extends: ['@commitlint/config-conventional'] }; diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e3e0d2d..0b93c5d 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -5,3 +5,63 @@ - `main` is the main branch. - `beta` is the branch where the latest changes are merged into. - / + +## Testing + +### Role based testing +From testing perspective, we are applying a role based approach in order to unify the testing +of the monads and matchable roles. So, in this way, we have just to create tests cases for each role when +we create a new monad or matchable. + +#### Monad +For this role, `map` and `flatMap` are the main operations that we have to test. + +Check the file `src/monads/monads.test.ts` for more information. + +#### Matchable +For this role, we have to test the `match` operation. + +check the `src/matchable/matchable.test.ts` folder for more information. + + +### Specific tests for each monad + +Each `monad` has a particular way to be created or a particular API. So, we have to create +specific tests for them. + +for example, the 'Try' monad has a particular way to be created, so we have to create +tests for that. + +```typescript +import { describe, it, expect } from 'vitest'; +import { Failure, Success, Try } from './try'; + + it.each([ + { type: 'Success', executable: () => 2, expected: new Success(2) }, + { + type: 'Failure', + executable: () => { + throw new Error(); + }, + expected: new Failure(new Error()), + }, +])('should create $type correctly', ({ executable, expected }) => { + expect(Try.execute(executable)).toEqual(expected); +}); +``` + +or the 'Option' monad has a particular API, so we have to create tests for that. + +```typescript +import { describe, it, expect } from 'vitest'; +import { None, Option, Some } from './option'; + +it.each([ + { type: 'Some', option: Option.of(2), expected: 2 }, + { type: 'None', option: Option.of(undefined), expected: 2 }, +])('$type should handle getOrElse operation correctly', ({ option, expected }) => { + expect(option.getOrElse(2)).toEqual(expected); +}); +``` + + diff --git a/package-lock.json b/package-lock.json index 0222e9a..b256610 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "@leanmind/monads", - "version": "0.0.0-development", + "version": "0.0.0-semantically-released", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@leanmind/monads", - "version": "0.0.0-development", + "version": "0.0.0-semantically-released", "license": "MIT", "devDependencies": { + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", "@types/node": "^20.12.7", "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", @@ -16,7 +18,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-watch": "^8.0.0", - "husky": "^9.0.11", + "husky": "^9.1.6", "lint-staged": "^15.2.2", "npm-check-updates": "^16.14.18", "prettier": "^3.2.5", @@ -156,6 +158,447 @@ "node": ">=0.1.90" } }, + "node_modules/@commitlint/cli": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.5.0.tgz", + "integrity": "sha512-gaGqSliGwB86MDmAAKAtV9SV1SHdmN8pnGq4EJU4+hLisQ7IFfx4jvU4s+pk6tl0+9bv6yT+CaZkufOinkSJIQ==", + "dev": true, + "dependencies": { + "@commitlint/format": "^19.5.0", + "@commitlint/lint": "^19.5.0", + "@commitlint/load": "^19.5.0", + "@commitlint/read": "^19.5.0", + "@commitlint/types": "^19.5.0", + "tinyexec": "^0.3.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.5.0.tgz", + "integrity": "sha512-OBhdtJyHNPryZKg0fFpZNOBM1ZDbntMvqMuSmpfyP86XSfwzGw4CaoYRG4RutUPg0BTK07VMRIkNJT6wi2zthg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.5.0", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.5.0.tgz", + "integrity": "sha512-CHtj92H5rdhKt17RmgALhfQt95VayrUo2tSqY9g2w+laAXyk7K/Ef6uPm9tn5qSIwSmrLjKaXK9eiNuxmQrDBw==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.5.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@commitlint/config-validator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/@commitlint/ensure": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.5.0.tgz", + "integrity": "sha512-Kv0pYZeMrdg48bHFEU5KKcccRfKmISSm9MvgIgkpI6m+ohFTB55qZlBW6eYqh/XDfRuIO0x4zSmvBjmOwWTwkg==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.5.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.5.0.tgz", + "integrity": "sha512-aqyGgytXhl2ejlk+/rfgtwpPexYyri4t8/n4ku6rRJoRhGZpLFMqrZ+YaubeGysCP6oz4mMA34YSTaSOKEeNrg==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.5.0.tgz", + "integrity": "sha512-yNy088miE52stCI3dhG/vvxFo9e4jFkU1Mj3xECfzp/bIS/JUay4491huAlVcffOoMK1cd296q0W92NlER6r3A==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.5.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.5.0.tgz", + "integrity": "sha512-0XQ7Llsf9iL/ANtwyZ6G0NGp5Y3EQ8eDQSxv/SRcfJ0awlBY4tHFAvwWbw66FVUaWICH7iE5en+FD9TQsokZ5w==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.5.0", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.5.0.tgz", + "integrity": "sha512-cAAQwJcRtiBxQWO0eprrAbOurtJz8U6MgYqLz+p9kLElirzSCc0vGMcyCaA1O7AqBuxo11l1XsY3FhOFowLAAg==", + "dev": true, + "dependencies": { + "@commitlint/is-ignored": "^19.5.0", + "@commitlint/parse": "^19.5.0", + "@commitlint/rules": "^19.5.0", + "@commitlint/types": "^19.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.5.0.tgz", + "integrity": "sha512-INOUhkL/qaKqwcTUvCE8iIUf5XHsEPCLY9looJ/ipzi7jtGhgmtH7OOFiNvwYgH7mA8osUWOUDV8t4E2HAi4xA==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^19.5.0", + "@commitlint/execute-rule": "^19.5.0", + "@commitlint/resolve-extends": "^19.5.0", + "@commitlint/types": "^19.5.0", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^5.0.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@commitlint/message": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.5.0.tgz", + "integrity": "sha512-R7AM4YnbxN1Joj1tMfCyBryOC5aNJBdxadTZkuqtWi3Xj0kMdutq16XQwuoGbIzL2Pk62TALV1fZDCv36+JhTQ==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.5.0.tgz", + "integrity": "sha512-cZ/IxfAlfWYhAQV0TwcbdR1Oc0/r0Ik1GEessDJ3Lbuma/MRO8FRQX76eurcXtmhJC//rj52ZSZuXUg0oIX0Fw==", + "dev": true, + "dependencies": { + "@commitlint/types": "^19.5.0", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse/node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@commitlint/parse/node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@commitlint/parse/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/parse/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/@commitlint/read": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.5.0.tgz", + "integrity": "sha512-TjS3HLPsLsxFPQj6jou8/CZFAmOP2y+6V4PGYt3ihbQKTY1Jnv0QG28WRKl/d1ha6zLODPZqsxLEov52dhR9BQ==", + "dev": true, + "dependencies": { + "@commitlint/top-level": "^19.5.0", + "@commitlint/types": "^19.5.0", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^0.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.5.0.tgz", + "integrity": "sha512-CU/GscZhCUsJwcKTJS9Ndh3AKGZTNFIOoQB2n8CmFnizE0VnEuJoum+COW+C1lNABEeqk6ssfc1Kkalm4bDklA==", + "dev": true, + "dependencies": { + "@commitlint/config-validator": "^19.5.0", + "@commitlint/types": "^19.5.0", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@commitlint/rules": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.5.0.tgz", + "integrity": "sha512-hDW5TPyf/h1/EufSHEKSp6Hs+YVsDMHazfJ2azIk9tHPXS6UqSz1dIRs1gpqS3eMXgtkT7JH6TW4IShdqOwhAw==", + "dev": true, + "dependencies": { + "@commitlint/ensure": "^19.5.0", + "@commitlint/message": "^19.5.0", + "@commitlint/to-lines": "^19.5.0", + "@commitlint/types": "^19.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.5.0.tgz", + "integrity": "sha512-R772oj3NHPkodOSRZ9bBVNq224DOxQtNef5Pl8l2M8ZnkkzQfeSTr4uxawV2Sd3ui05dUVzvLNnzenDBO1KBeQ==", + "dev": true, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.5.0.tgz", + "integrity": "sha512-IP1YLmGAk0yWrImPRRc578I3dDUI5A2UBJx9FbSOjxe9sTlzFiwVJ+zeMLgAtHMtGZsC8LUnzmW1qRemkFU4ng==", + "dev": true, + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level/node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/top-level/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@commitlint/top-level/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@commitlint/types": { + "version": "19.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.5.0.tgz", + "integrity": "sha512-DSHae2obMSMkAtTBSOulg5X7/z+rGLxcXQIkg3OmWvY6wifojge5uVMydfhUvs7yQj+V7jNmRZ2Xzl8GJyqRgg==", + "dev": true, + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1965,6 +2408,15 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-loB369iXNmAZglwWATL+WRe+CRMmmBPtpolYzIebFaX4YA3x+BEfLqhUAV9WanycKI3TG1IMr5bMJDajDKLlUQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -3276,6 +3728,18 @@ "node": ">=18" } }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/conventional-changelog-writer": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.0.0.tgz", @@ -3363,6 +3827,23 @@ } } }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-5.0.0.tgz", + "integrity": "sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==", + "dev": true, + "dependencies": { + "jiti": "^1.19.1" + }, + "engines": { + "node": ">=v16" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=8.2", + "typescript": ">=4" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -3404,6 +3885,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", @@ -4179,6 +4672,12 @@ "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==", "dev": true }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -4570,6 +5069,44 @@ "traverse": "0.6.8" } }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "dev": true, + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-raw-commits/node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "engines": { + "node": ">= 10.x" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -4611,6 +5148,30 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory/node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", @@ -4877,9 +5438,9 @@ } }, "node_modules/husky": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.5.tgz", - "integrity": "sha512-rowAVRUBfI0b4+niA4SJMhfQwc107VLkBUgEYYAOQAbqDCnra1nYh83hF/MDmhYs9t9n1E3DuKOrs2LYNC+0Ag==", + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", "dev": true, "bin": { "husky": "bin.js" @@ -5227,6 +5788,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -5306,6 +5879,15 @@ "node": ">= 0.6.0" } }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/jju": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", @@ -5417,6 +5999,22 @@ "node >= 0.2.0" ] }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, "node_modules/keypress": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz", @@ -5732,6 +6330,12 @@ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "dev": true }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true + }, "node_modules/lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", @@ -5786,18 +6390,48 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true + }, "node_modules/lodash.unionwith": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.unionwith/-/lodash.unionwith-4.6.0.tgz", "integrity": "sha512-Hk8otPCkVM4UxRoft3E5dAREwExyXci6iVPCibHIEiG7neb9KAdWHYS75MYpVTvxDrnpp7WCJNZ84vAk7j7tVA==", "dev": true }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, "node_modules/lodash.uniqby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", "dev": true }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true + }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -11684,6 +12318,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11711,6 +12357,12 @@ "node": ">=0.8" } }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -11772,6 +12424,12 @@ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", + "dev": true + }, "node_modules/tinypool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", diff --git a/package.json b/package.json index ba334ac..fc2ecdd 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "format:check": "npm run format -- --check", "format:fix": "npm run format -- --write", "test": "vitest run", - "semantic-release": "semantic-release" + "semantic-release": "semantic-release", + "prepare": "husky", + "commitlint": "commitlint --edit" }, "author": "Lean Mind", "contributors": [ @@ -48,6 +50,8 @@ }, "license": "MIT", "devDependencies": { + "@commitlint/cli": "^19.5.0", + "@commitlint/config-conventional": "^19.5.0", "@types/node": "^20.12.7", "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", @@ -55,7 +59,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-watch": "^8.0.0", - "husky": "^9.0.11", + "husky": "^9.1.6", "lint-staged": "^15.2.2", "npm-check-updates": "^16.14.18", "prettier": "^3.2.5", diff --git a/src/complete/completable.ts b/src/complete/completable.ts new file mode 100644 index 0000000..7560790 --- /dev/null +++ b/src/complete/completable.ts @@ -0,0 +1,3 @@ +export interface Completable { + complete(onSuccess: (value: T) => S, onFailure: (error: Error) => S): Promise; +} diff --git a/src/complete/index.ts b/src/complete/index.ts new file mode 100644 index 0000000..88d4e17 --- /dev/null +++ b/src/complete/index.ts @@ -0,0 +1 @@ +export { Completable } from './completable'; diff --git a/src/either/either.test.ts b/src/either/either.test.ts index 5af7a7c..c4fdb45 100644 --- a/src/either/either.test.ts +++ b/src/either/either.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { Either, Right } from './either'; +import { Either } from './either'; import { Option } from '../option'; describe('Either monad', () => { @@ -28,13 +28,6 @@ describe('Either monad', () => { expect(Either.from(matchable)).toEqual(expected); }); - it.each([ - { type: 'Right', either: Either.right(2), closure: (x: number) => x, expected: Either.right(2) }, - { type: 'Left', either: Either.left(2), closure: (x: number) => x * 2, expected: Either.left(2) }, - ])('$type should handle map operation correctly', ({ either, closure, expected }) => { - expect(either.map(closure)).toEqual(expected); - }); - it.each([ { type: 'Right', either: Either.right(2), closure: (x: number) => x, expected: Either.right(2) }, { type: 'Left', either: Either.left(2), closure: (x: number) => x * 2, expected: Either.left(4) }, @@ -42,15 +35,6 @@ describe('Either monad', () => { expect(either.mapLeft(closure)).toEqual(expected); }); - it.each([ - { type: 'Right', either: Either.right(2), closure: (x: number) => Either.right(x), expected: Either.right(2) }, - { type: 'Left', either: Either.left(2), closure: (x: number) => Either.left(x), expected: Either.left(2) }, - ])('$type should handle flatMap operation correctly', ({ either, closure, expected }) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - expect(either.flatMap(closure)).toEqual(expected); - }); - it.each([ { type: 'Right', either: Either.right(2), closure: (x: number) => Either.right(x), expected: Either.right(2) }, { type: 'Left', either: Either.left(2), closure: (x: number) => Either.left(x), expected: Either.left(2) }, @@ -60,18 +44,6 @@ describe('Either monad', () => { expect(either.flatMapLeft(closure)).toEqual(expected); }); - it.each([ - { type: 'Right', either: Either.right(2), expected: '2' }, - { type: 'Left', either: Either.left('Error'), expected: 'Error' }, - ])('$type should handle match operation correctly', ({ either, expected }) => { - expect( - either.match( - (x) => x.toString(), - (x) => x - ) - ).toEqual(expected); - }); - it.each([ { type: 'Right', diff --git a/src/either/either.ts b/src/either/either.ts index 5fe4ce5..7e025bd 100644 --- a/src/either/either.ts +++ b/src/either/either.ts @@ -25,13 +25,13 @@ abstract class Either implements Monad, Matchable { } } - abstract map(f: (r: R) => T): Either; + abstract map(transform: (r: R) => T): Either; - abstract mapLeft(f: (l: L) => T): Either; + abstract mapLeft(transform: (l: L) => T): Either; - abstract flatMap(f: (r: R) => Either): Either; + abstract flatMap(transform: (r: R) => Either): Either; - abstract flatMapLeft(f: (l: L) => Either): Either; + abstract flatMapLeft(transform: (l: L) => Either): Either; abstract match(ifRight: (r: R) => T, ifLeft: (l: L) => T): T; @@ -45,33 +45,33 @@ class Left extends Either { super(); } - map(_: (r: R) => T): Either { + map(_: (r: never) => never): Either { return new Left(this.value); } - mapLeft(f: (l: L) => T): Either { - return new Left(f(this.value)); + mapLeft(transform: (l: L) => T): Either { + return new Left(transform(this.value)); } - flatMap(_: (r: never) => Either): Either { + flatMap(_: (r: never) => Either): Either { return new Left(this.value); } + flatMapLeft(transform: (l: L) => Either): Either { + return transform(this.value); + } + match(_: (_: never) => never, ifLeft: (l: L) => T): T { return ifLeft(this.value); } - isLeft(): this is Left { + isLeft(): this is Left { return true; } - isRight(): this is Right { + isRight(): this is Right { return false; } - - flatMapLeft(f: (l: L) => Either): Either { - return f(this.value); - } } class Right extends Either { @@ -79,33 +79,33 @@ class Right extends Either { super(); } - map(f: (r: R) => T): Either { - return new Right(f(this.value)); + map(transform: (r: R) => T): Either { + return new Right(transform(this.value)); } - mapLeft(_: (l: L) => T): Either { + mapLeft(_: (l: L) => never): Either { return new Right(this.value); } - flatMap(f: (r: R) => Either): Either { - return f(this.value); + flatMap(transform: (r: R) => Either): Either { + return transform(this.value); + } + + flatMapLeft(_: (l: never) => Either): Either { + return new Right(this.value); } match(ifRight: (r: R) => T, _: (_: never) => never): T { return ifRight(this.value); } - isLeft(): this is Left { + isLeft(): this is Left { return false; } - isRight(): this is Right { + isRight(): this is Right { return true; } - - flatMapLeft(_: (l: never) => Either): Either { - return new Right(this.value); - } } export { Either, Right, Left }; diff --git a/src/future/future.test.ts b/src/future/future.test.ts new file mode 100644 index 0000000..def7050 --- /dev/null +++ b/src/future/future.test.ts @@ -0,0 +1,84 @@ +import { assert, describe, expect, it, vi } from 'vitest'; +import { Future } from './future'; + +describe('Future monad', () => { + it.each([ + { + completionType: 'success', + action: () => Promise.resolve(2), + expected: { + ifSuccess: (value: number) => expect(value).toBe(2), + ifFailure: (_) => assert.fail('Error should not be thrown'), + }, + }, + { + completionType: 'failure', + action: () => Promise.reject(new Error('Error')), + expected: { + ifSuccess: () => assert.fail('Value should not be emitted'), + ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')), + }, + }, + ])('should handle $completionType completion', async ({ action, expected }) => { + const future = Future.of(action); + await future.complete(expected.ifSuccess, expected.ifFailure); + }); + + it.each([ + { + completionType: 'Success', + future: Future.of(() => Promise.resolve(2)), + expected: { + ifSuccess: (value: number) => expect(value).toBe(4), + ifFailure: (_) => assert.fail('Error should not be thrown'), + }, + }, + { + completionType: 'Failure', + future: Future.of(() => Promise.reject(new Error('Error'))), + expected: { + ifSuccess: () => assert.fail('Value should not be emitted'), + ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')), + }, + }, + ])('$completionType completion should handle map correctly', async ({ future, expected }) => { + const actual = future.map((value) => value * 2); + await actual.complete(expected.ifSuccess, expected.ifFailure); + }); + + it.each([ + { + completionType: 'Success', + future: Future.of(() => Promise.resolve(2)), + expected: { + ifSuccess: (value: number) => expect(value).toBe(4), + ifFailure: (error: Error) => assert.fail('Error should not be thrown'), + }, + }, + { + completionType: 'Failure', + future: Future.of(() => Promise.reject(new Error('Error'))), + expected: { + ifSuccess: () => assert.fail('Value should not be emitted'), + ifFailure: (error: Error) => expect(error).toEqual(new Error('Error')), + }, + }, + ])('$completionType completion should handle flatMap correctly', async ({ future, expected }) => { + const actual = future.flatMap((value) => Future.of(() => Promise.resolve(value * 2))); + await actual.complete(expected.ifSuccess, expected.ifFailure); + }); + + it('should not execute async action when a mapping is performed', async () => { + const asyncAction = vi.fn(async () => 2); + const future = Future.of(asyncAction); + future.map((value) => value * 2); + expect(asyncAction).not.toHaveBeenCalled(); + }); + + it('should not execute async action when a flat mapping is performed', async () => { + const asyncAction = vi.fn(async () => 2); + const future = Future.of(asyncAction); + future.flatMap((value) => Future.of(() => Promise.resolve(value * 2))); + expect(asyncAction).not.toHaveBeenCalled(); + }); +}); diff --git a/src/future/future.ts b/src/future/future.ts new file mode 100644 index 0000000..f3a613e --- /dev/null +++ b/src/future/future.ts @@ -0,0 +1,24 @@ +import { Monad } from '../monad'; +import { Completable } from '../complete'; + +class Future implements Monad, Completable { + private constructor(private readonly action: () => Promise) {} + + static of(action: () => Promise): Future { + return new Future(action); + } + + map(transform: (value: T) => U): Future { + return new Future(() => this.action().then(transform)); + } + + flatMap(transform: (value: T) => Future): Future { + return new Future(() => this.action().then((value) => transform(value).action())); + } + + complete(onSuccess: (value: T) => S, onFailure: (error: Error) => S): Promise { + return this.action().then(onSuccess).catch(onFailure); + } +} + +export { Future }; diff --git a/src/future/index.ts b/src/future/index.ts new file mode 100644 index 0000000..d3099a1 --- /dev/null +++ b/src/future/index.ts @@ -0,0 +1 @@ +export { Future } from './future'; diff --git a/src/index.ts b/src/index.ts index d44e850..cabf47a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,5 @@ export * from './either'; +export * from './future'; export * from './option'; +export * from './try'; +export * from './io'; diff --git a/src/io/index.ts b/src/io/index.ts new file mode 100644 index 0000000..bd96954 --- /dev/null +++ b/src/io/index.ts @@ -0,0 +1 @@ +export { IO } from './io'; diff --git a/src/io/io.test.ts b/src/io/io.test.ts new file mode 100644 index 0000000..ea83e18 --- /dev/null +++ b/src/io/io.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, vi } from 'vitest'; +import { IO } from './io'; + +describe('IO Monad', () => { + it('should create IO monad correctly', () => { + const spySideEffect = vi.fn(); + const io = IO.of(spySideEffect); + + expect(io).toBeInstanceOf(IO); + expect(spySideEffect).not.toHaveBeenCalled(); + }); + + it('should not execute the side-effect during map operations', () => { + const spySideEffect = vi.fn(); + + IO.of(() => { + spySideEffect(); + return 1; + }).map((x) => x * 2); + + expect(spySideEffect).not.toHaveBeenCalled(); + }); + + it('should execute the pipeline created with map operations', () => { + const unsafeOperation = IO.of(() => 2).map((x) => x * 2); + expect(unsafeOperation.runUnsafe()).toEqual(4); + }); + + it('should not execute any side-effect during flatMap operations', () => { + const spySideEffect = vi.fn(); + const spySideEffect2 = vi.fn(); + + IO.of(() => spySideEffect()).flatMap(() => IO.of(() => spySideEffect2())); + + expect(spySideEffect).not.toHaveBeenCalled(); + expect(spySideEffect2).not.toHaveBeenCalled(); + }); + + it('should execute the pipeline created with flatMap operations', () => { + const unsafeOperation = IO.of(() => 2).flatMap((x) => IO.of(() => x * 2)); + expect(unsafeOperation.runUnsafe()).toEqual(4); + }); +}); diff --git a/src/io/io.ts b/src/io/io.ts new file mode 100644 index 0000000..8543122 --- /dev/null +++ b/src/io/io.ts @@ -0,0 +1,22 @@ +import { Monad } from '../monad'; + +class IO implements Monad { + private constructor(private description: () => T) {} + + static of(sideEffect: () => T): IO { + return new IO(sideEffect); + } + + flatMap(transform: (value: T) => IO): IO { + return new IO(() => transform(this.description()).runUnsafe()); + } + map(transform: (value: T) => U): IO { + return new IO(() => transform(this.description())); + } + + runUnsafe(): T { + return this.description(); + } +} + +export { IO }; diff --git a/src/match/matchable.test.ts b/src/match/matchable.test.ts new file mode 100644 index 0000000..4bf77fa --- /dev/null +++ b/src/match/matchable.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest'; +import { Try } from '../try'; +import { Either } from '../either'; +import { Option } from '../option'; + +const testsMatchableCases = [ + { + type: 'Either Right', + matchable: Either.right(2), + closureF: (x: number) => x * 2, + closureG: () => 2, + expected: 4, + }, + { + type: 'Either Left', + matchable: Either.left(2), + closureF: () => 2, + closureG: (x: number) => x * 2, + expected: 4, + }, + { + type: 'Try Success', + matchable: Try.execute(() => 2), + closureF: (x: number) => x * 2, + closureG: () => 2, + expected: 4, + }, + { + type: 'Try Failure', + matchable: Try.execute(() => { + throw new Error(); + }), + closureF: () => 2, + closureG: (x: Error) => x, + expected: new Error(), + }, + { + type: 'Option Some', + matchable: Option.of(2), + closureF: (x: number) => x * 2, + closureG: () => 2, + expected: 4, + }, + { + type: 'Option None', + matchable: Option.of(undefined), + closureF: () => 2, + closureG: (x: undefined) => x, + expected: undefined, + }, +]; + +describe('Matchable', () => { + it.each(testsMatchableCases)( + '$type matchable should handle match operation correctly', + ({ matchable, expected, closureF, closureG }) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // the closures can be different for each monad but in the end the behavior is the same + expect(matchable.match(closureF, closureG)).toEqual(expected); + } + ); +}); diff --git a/src/monad/monad.test.ts b/src/monad/monad.test.ts new file mode 100644 index 0000000..b79aed0 --- /dev/null +++ b/src/monad/monad.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from 'vitest'; +import { Either } from '../either'; +import { Option } from '../option'; +import { Try } from '../try'; + +const testCasesMonadMapOperation = [ + { + type: 'Either Right', + monad: Either.right(2), + closure: (x: number) => x * 2, + expected: Either.right(4), + }, + { + type: 'Either Left', + monad: Either.left(2), + closure: (x: number) => x * 2, + expected: Either.left(2), + }, + { + type: 'Option Some', + monad: Option.of(2), + closure: (x: number) => x * 2, + expected: Option.of(4), + }, + { + type: 'Option None', + monad: Option.of(undefined), + closure: (x: number) => x * 2, + expected: Option.of(undefined), + }, + { + type: 'Try Success', + monad: Try.execute(() => 2), + closure: (x: number) => x * 2, + expected: Try.execute(() => 4), + }, + { + type: 'Try Failure', + monad: Try.execute(() => { + throw new Error('Error'); + }), + closure: (x: number) => x * 2, + expected: Try.execute(() => { + throw new Error('Error'); + }), + }, +]; + +const testCasesMonadFlatMapOperation = [ + { + type: 'Either Right', + monad: Either.right(2), + closure: (x: number) => Either.right(x * 2), + expected: Either.right(4), + }, + { + type: 'Either Left', + monad: Either.left(2), + closure: (x: number) => Either.right(x * 2), + expected: Either.left(2), + }, + { + type: 'Option Some', + monad: Option.of(2), + closure: (x: number) => Option.of(x * 2), + expected: Option.of(4), + }, + { + type: 'Option None', + monad: Option.of(undefined), + closure: (x: number) => Option.of(x * 2), + expected: Option.of(undefined), + }, + { + type: 'Try Success', + monad: Try.execute(() => 2), + closure: (x: number) => Try.execute(() => x * 2), + expected: Try.execute(() => 4), + }, + { + type: 'Try Failure', + monad: Try.execute(() => { + throw new Error('Error'); + }), + closure: (x: number) => Try.execute(() => x * 2), + expected: Try.execute(() => { + throw new Error('Error'); + }), + }, +]; + +describe('Monad', () => { + it.each(testCasesMonadMapOperation)('$type Monad should handle map correctly', ({ monad, closure, expected }) => { + expect(monad.map(closure)).toEqual(expected); + }); + + it.each(testCasesMonadFlatMapOperation)( + '$type Monad should handle flatMap correctly', + ({ monad, closure, expected }) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + // the closure can be different for each monad but in the end the behavior is the same + expect(monad.flatMap(closure)).toEqual(expected); + } + ); +}); diff --git a/src/option/option.test.ts b/src/option/option.test.ts index a595cd2..336ef06 100644 --- a/src/option/option.test.ts +++ b/src/option/option.test.ts @@ -36,37 +36,6 @@ describe('Option monad', () => { expect(option.filter(closure)).toEqual(expected); }); - it.each([ - { type: 'Some', option: Option.of(2), closure: (x: number) => x * 2, expected: new Some(4) }, - { type: 'None', option: Option.of(undefined), closure: (x: number) => x * 2, expected: new None() }, - ])('$type should handle map operation correctly', ({ option, closure, expected }) => { - expect(option.map(closure)).toEqual(expected); - }); - - it.each([ - { type: 'Some', option: Option.of(2), closure: (x: number) => Option.of(x * 2), expected: new Some(4) }, - { - type: 'None', - option: Option.of(undefined), - closure: (x: number) => Option.of(x * 2), - expected: new None(), - }, - ])('$type should handle flatMap operation correctly', ({ option, closure, expected }) => { - expect(option.flatMap(closure)).toEqual(expected); - }); - - it.each([ - { type: 'Some', option: Option.of(2), expected: 2 }, - { type: 'None', option: Option.of(undefined), expected: 2 }, - ])('$type should handle match operation correctly', ({ option, expected }) => { - expect( - option.match( - (x) => x, - () => 2 - ) - ).toEqual(expected); - }); - it.each([ { type: 'Some', option: Option.of(2), expected: false }, { type: 'None', option: Option.of(undefined), expected: true }, diff --git a/src/try/index.ts b/src/try/index.ts new file mode 100644 index 0000000..8e7b916 --- /dev/null +++ b/src/try/index.ts @@ -0,0 +1 @@ +export { Try, Success, Failure } from './try'; diff --git a/src/try/try.test.ts b/src/try/try.test.ts new file mode 100644 index 0000000..d3f5088 --- /dev/null +++ b/src/try/try.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from 'vitest'; +import { Failure, Success, Try } from './try'; +import { Either } from '../either'; +import { Option } from '../option'; + +describe('Try monad', () => { + it.each([ + { type: 'Success', executable: () => 2, expected: new Success(2) }, + { + type: 'Failure', + executable: () => { + throw new Error(); + }, + expected: new Failure(new Error()), + }, + ])('should create $type correctly', ({ executable, expected }) => { + expect(Try.execute(executable)).toEqual(expected); + }); + + it.each([ + { typeMatchable: 'Right', tryType: 'Success', matchable: Either.right(2), expected: new Success(2) }, + { + typeMatchable: 'Left', + tryType: 'Failure', + matchable: Either.left(new Error('An error occurred')), + expected: new Failure(new Error('An error occurred')), + }, + { + typeMatchable: 'Some', + tryType: 'Success', + matchable: Option.of(2), + expected: new Success(2), + }, + { + typeMatchable: 'None', + tryType: 'Failure', + matchable: Option.of(undefined), + expected: Failure.NO_ERROR_PROVIDED, + }, + ])('$tryType should be created from $typeMatchable', ({ matchable, expected }) => { + expect(Try.from(matchable)).toEqual(expected); + }); + + it.each([ + { type: 'Success', tryMonad: Try.execute(() => 2), expected: false }, + { + type: 'Failure', + tryMonad: Try.execute(() => { + throw new Error(); + }), + expected: true, + }, + ])('$type should handle isFailure operation correctly', ({ tryMonad, expected }) => { + expect(tryMonad.isFailure()).toEqual(expected); + }); + + it.each([ + { type: 'Success', tryMonad: Try.execute(() => 2), expected: true }, + { + type: 'Failure', + tryMonad: Try.execute(() => { + throw new Error(); + }), + expected: false, + }, + ])('$type should handle isSuccess operation correctly', ({ tryMonad, expected }) => { + expect(tryMonad.isSuccess()).toEqual(expected); + }); +}); diff --git a/src/try/try.ts b/src/try/try.ts new file mode 100644 index 0000000..c3f4a38 --- /dev/null +++ b/src/try/try.ts @@ -0,0 +1,85 @@ +import { Monad } from '../monad'; +import { Matchable } from '../match'; + +abstract class Try implements Monad, Matchable { + static execute(executable: () => T): Try { + try { + return new Success(executable()); + } catch (error) { + return new Failure(error as Error); + } + } + + static from(matchable: Matchable): Try { + return matchable.match>( + (value: T) => new Success(value), + (error: unknown) => (error instanceof Error ? new Failure(error) : Failure.NO_ERROR_PROVIDED) + ); + } + + abstract map(transform: (value: T) => U): Try; + + abstract flatMap(transform: (value: T) => Try): Try; + + abstract match(ifSuccess: (value: T) => U, ifFailure: (error: Error) => U): U; + + abstract isSuccess(): this is Success; + + abstract isFailure(): this is Failure; +} + +class Success extends Try { + constructor(private value: T) { + super(); + } + + map(transform: (value: T) => U): Try { + return new Success(transform(this.value)); + } + + flatMap(transform: (value: T) => Try): Try { + return transform(this.value); + } + + match(ifSuccess: (value: T) => U, _: (_: never) => U): U { + return ifSuccess(this.value); + } + + isSuccess(): this is Success { + return true; + } + + isFailure(): this is Failure { + return false; + } +} + +class Failure extends Try { + constructor(private error: Error) { + super(); + } + + static NO_ERROR_PROVIDED = new Failure(new Error('No error provided')); + + map(_: (_: never) => never): Try { + return new Failure(this.error); + } + + flatMap(_: (_: never) => Try): Try { + return new Failure(this.error); + } + + match(_: (_: never) => never, ifFailure: (error: Error) => U): U { + return ifFailure(this.error); + } + + isSuccess(): this is Success { + return false; + } + + isFailure(): this is Failure { + return true; + } +} + +export { Try, Success, Failure };