Initial commit
- Separate out from old mono repository. - Adjust name including new namespace. - Adjust version to pre-1.0
This commit is contained in:
commit
34a930fa5d
27 changed files with 10455 additions and 0 deletions
2
.eslintignore
Normal file
2
.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
dist
|
||||
node_modules
|
10
.eslintrc.js
Normal file
10
.eslintrc.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier",
|
||||
],
|
||||
};
|
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
# compiled outputs
|
||||
dist
|
||||
*tgz
|
||||
|
||||
# generated type definitions
|
||||
*d.ts
|
||||
|
||||
# generated source maps
|
||||
*map
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
# incremental compilation status (not used in CI/CD)
|
||||
tsconfig.tsbuildinfo
|
||||
|
||||
# yarn internals
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# test coverage report
|
||||
coverage
|
||||
.envrc
|
||||
.env
|
8
.prettierignore
Normal file
8
.prettierignore
Normal file
|
@ -0,0 +1,8 @@
|
|||
/node_modules/
|
||||
/dist/
|
||||
|
||||
*md
|
||||
*bash
|
||||
*py
|
||||
*js
|
||||
*txt
|
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
363
.yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
35
.yarn/plugins/@yarnpkg/plugin-outdated.cjs
vendored
Normal file
35
.yarn/plugins/@yarnpkg/plugin-outdated.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
9
.yarn/plugins/@yarnpkg/plugin-typescript.cjs
vendored
Normal file
File diff suppressed because one or more lines are too long
768
.yarn/releases/yarn-3.1.1.cjs
vendored
Executable file
768
.yarn/releases/yarn-3.1.1.cjs
vendored
Executable file
File diff suppressed because one or more lines are too long
22
.yarnrc.yml
Normal file
22
.yarnrc.yml
Normal file
|
@ -0,0 +1,22 @@
|
|||
nodeLinker: node-modules
|
||||
|
||||
npmAlwaysAuth: false
|
||||
|
||||
npmRegistryServer: "https://registry.npmjs.com"
|
||||
|
||||
npmScopes:
|
||||
caredge:
|
||||
npmAlwaysAuth: true
|
||||
npmAuthToken: "${NPM_AUTH_TOKEN:-}"
|
||||
npmPublishRegistry: "https://npm.pkg.github.com"
|
||||
npmRegistryServer: "https://npm.pkg.github.com"
|
||||
|
||||
plugins:
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs
|
||||
spec: "@yarnpkg/plugin-typescript"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-outdated.cjs
|
||||
spec: "https://mskelton.dev/yarn-outdated/v3"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.1.1.cjs
|
125
README.md
Normal file
125
README.md
Normal file
|
@ -0,0 +1,125 @@
|
|||
# @cmdln/result
|
||||
|
||||
This module implements my error type for Typescript, `Result<T, E>`. After trying out `@snowfrog/result`, I implemented my own for a couple of reasons.
|
||||
|
||||
1. The `@snowfrog` package does not play well with async code.
|
||||
1. The `isOk` and `isErr` methods could have done more in `@snowfrog` to help access values and errors safely with less boiler plate. In the `@snowfrog` code, the guard methods do not narrow in all cases. In particular, the implicit else of a short-circuit pattern doesn't work. This creates toil in that the call has to use `expect`/`expectErr` which reduces readability and with Typescript's flow control analysis shouldn't be necessary.
|
||||
|
||||
In addition to `Result`, this package includes some useful facilities for working with that type and converting code that uses thrown exceptions into `Result` returning code.
|
||||
|
||||
## Converting Thrown Exceptions
|
||||
|
||||
The high order functions `trySync` and `tryAsync` each accept two function as arguments. The first is a no argument closure for running some code that may thrown an exception. The second is a closure accepting an `unknown` error that can then be narrowed, wrapped, and returned to change any thrown exception into an `Err`.
|
||||
|
||||
## Chains of Promises and Result
|
||||
|
||||
`Result` shines when using its various combinators, like `andThenAsync` and `yield`. These high order functions accepts closures that allow for changing the success and error types to new values of different types and even changing an `Ok` to an `Err` and vice versa.
|
||||
|
||||
`Promise` has a similar design and in theory the types work well together. There are some problems due to the special handling `Promise` gets when used with the `await` keyword. The compiler and runtime literally expect a `Promise` and won't accept a shape that is merely assignable. This forces some diagonal code, similar to nested callbacks, to call `Result` combinators from closures passed to `Promise`'s own combinators, especially `then`.
|
||||
|
||||
The `resultify` function sugars over the two types to allow use with `await` without the added boiler plate needed to access the `Result` combinators without always fully resolving the promise.
|
||||
|
||||
## Example
|
||||
|
||||
This example is excerpted from a past project and uses some structured http errors from another package I wrote.
|
||||
|
||||
```typescript
|
||||
import { BaseContext, BaseError, Result } from "@cmdln/error";
|
||||
import { passOrYield, resultify, trySync } from "@cmdln/result";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import { FanOutName } from "fanout";
|
||||
import { IncomingHttpHeaders } from "http";
|
||||
import { getLogger } from "index";
|
||||
import { sendMessage } from "producer";
|
||||
import { Webhook } from "svix";
|
||||
import { ID_HEADER, SIGNATURE_HEADER, TIMESTAMP_HEADER } from "./constants";
|
||||
|
||||
export async function handleClerkWebhook(
|
||||
request: Request,
|
||||
response: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const logger = getLogger(__filename);
|
||||
logger.info("Received Clerk webhook request.");
|
||||
const { body: payload, headers } = request;
|
||||
// without resultify here, the result's combinators would have to be nested
|
||||
// in a .then call
|
||||
await resultify(tryVerify(payload, headers))
|
||||
.andThenAsync(async (verified) => {
|
||||
logger.debug({ verified }, "Verified call from Clerk");
|
||||
return await sendMessage(FanOutName.Clerk, verified);
|
||||
})
|
||||
.yield(() => {
|
||||
request.log.debug("Webhook successfully received message");
|
||||
response.status(200).json({ message: "Success" });
|
||||
next();
|
||||
})
|
||||
.yieldErr((cause) => {
|
||||
request.log.error({ cause }, "Clerk webhook failed");
|
||||
response.status(400).json({});
|
||||
next(cause);
|
||||
});
|
||||
}
|
||||
|
||||
function tryVerify(
|
||||
payload: string,
|
||||
headers: IncomingHttpHeaders
|
||||
): Result<unknown> {
|
||||
const wh = new Webhook(process.env.CLERK_WEBHOOK_SECRET);
|
||||
// there is also tryAsync which works the same way for async code
|
||||
// remember that if your logic in the first argument returns a value, you
|
||||
// will need to return the call as well; a common mistake is leaving out a
|
||||
// return or worse an await with tryAsync
|
||||
return trySync<unknown, BaseError>(
|
||||
() => {
|
||||
// a common practice in our code is to use assertion functions in
|
||||
// trySync/tryAsync since often we are using a client library and need to
|
||||
// narrow the result; this use case is a little different but a valid
|
||||
// example of the pattern
|
||||
assertClerkHeaders(headers);
|
||||
return wh.verify(payload, headers);
|
||||
},
|
||||
// if we know of any thrown errors that are compatible with our Result's
|
||||
// generic bindings, this helper function saves more boiler plate
|
||||
passOrYield(VerifyError, (cause) => {
|
||||
return VerifyError.err(VerifyErrorKind.InvalidSignature, { cause });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
enum VerifyErrorKind {
|
||||
InvalidSignature = "Failed to verify signature on call from Clerk",
|
||||
MissingHeaders = "Missing required headers on call from Clerk",
|
||||
}
|
||||
|
||||
type VerifyContext = BaseContext & {
|
||||
headers?: unknown;
|
||||
};
|
||||
|
||||
class VerifyError extends BaseError {
|
||||
static err(kind: VerifyErrorKind, context: VerifyContext) {
|
||||
return new VerifyError(500, kind, context);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return VerifyError.name;
|
||||
}
|
||||
}
|
||||
|
||||
function assertClerkHeaders(
|
||||
headers: IncomingHttpHeaders
|
||||
): asserts headers is Record<string, string> {
|
||||
if (
|
||||
!headers[ID_HEADER] ||
|
||||
!headers[TIMESTAMP_HEADER] ||
|
||||
!headers[SIGNATURE_HEADER]
|
||||
) {
|
||||
throw VerifyError.err(VerifyErrorKind.MissingHeaders, { headers });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## To Do
|
||||
|
||||
- [ ] Figure out if async and sync combinators can be cleanly merged similar to Jest's signature for its builder HOF's
|
||||
- [ ] Remove `Async` suffix for methods names, use `Sync` for non-awaitable code since it is likely less frequently used
|
37
jest.config.js
Normal file
37
jest.config.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
const { compilerOptions } = require("./tsconfig.json");
|
||||
|
||||
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
testPathIgnorePatterns: ["dist"],
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{ tsconfig: compilerOptions },
|
||||
],
|
||||
},
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ["**/src/**/*.ts"],
|
||||
coveragePathIgnorePatterns: ["node_modules/", "__test__/"],
|
||||
coverageReporters: ["html", "text-summary"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 100,
|
||||
},
|
||||
// shared enums
|
||||
"packages/db-enums/src/": {
|
||||
branches: 0
|
||||
},
|
||||
"packages/orm/src/repository": {
|
||||
branches: 0
|
||||
},
|
||||
// test utilities
|
||||
"packages/orm-test/src/": {
|
||||
branches: 0
|
||||
},
|
||||
"packages/result-test/src/": {
|
||||
branches: 0
|
||||
}
|
||||
}
|
||||
};
|
45
package.json
Normal file
45
package.json
Normal file
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "@cmdln/result",
|
||||
"author": "cmdln",
|
||||
"version": "0.1.2",
|
||||
"description": "Try type for functional programming plus useful utilities.",
|
||||
"repository": "https://git.cmdln.net/cmdln/result",
|
||||
"license": "MIT",
|
||||
"packageManager": "yarn@3.1.1",
|
||||
"main": "dist/index.js",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "yarn cache clean && rm -rf dist",
|
||||
"lint": "yarn run eslint . --ext .ts",
|
||||
"lint:fix": "yarn run eslint --fix . --ext .ts",
|
||||
"format:check": "yarn run prettier --check src/**",
|
||||
"format": "yarn run prettier --write src/**",
|
||||
"test": "yarn jest",
|
||||
"pre:push": "yarn lint && yarn format:check && yarn compile && yarn test && yarn outdated -c -s minor",
|
||||
"compile": "yarn tsc -b"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash.isnil": "^4.0.9",
|
||||
"@types/node": "^20.11.20",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"jest": "^29.7.0",
|
||||
"lerna": "^8.1.2",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash.isnil": "^4.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist/*"
|
||||
]
|
||||
}
|
95
src/__test__/guards.test.ts
Normal file
95
src/__test__/guards.test.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
import { Err, Ok } from "..";
|
||||
import { isResult, __internals } from "../guards";
|
||||
|
||||
describe("guards.ts", () => {
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
const combinators = {
|
||||
and: () => {},
|
||||
andThen: () => {},
|
||||
andThenAsync: () => {},
|
||||
or: () => {},
|
||||
orElse: () => {},
|
||||
orElseAsync: () => {},
|
||||
yield: () => {},
|
||||
yieldAsync: () => {},
|
||||
yieldErr: () => {},
|
||||
yieldOr: () => {},
|
||||
yieldOrElse: () => {},
|
||||
};
|
||||
/* eslint-enable */
|
||||
|
||||
const mockSuccess = {
|
||||
kind: "ok",
|
||||
_value: true,
|
||||
isOk: () => true,
|
||||
...combinators,
|
||||
};
|
||||
|
||||
const mockError = {
|
||||
kind: "err",
|
||||
_error: true,
|
||||
isErr: () => true,
|
||||
...combinators,
|
||||
};
|
||||
|
||||
describe("isResult", () => {
|
||||
it.each([mockError, mockSuccess, Err(true), Ok(true)])(
|
||||
"successfully narrows %s",
|
||||
(r) => {
|
||||
expect(isResult(r)).toBeTruthy();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ kind: "ok", _value: true },
|
||||
prune(mockSuccess, "isOk"),
|
||||
prune(mockSuccess, "andThen", "orElse"),
|
||||
{ kind: "err", _error: true },
|
||||
prune(mockError, "isErr"),
|
||||
prune(mockError, "andThen", "orElse"),
|
||||
])("rejects %s correctly", (r) => {
|
||||
expect(isResult(r)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSuccess", () => {
|
||||
it.each([Ok(true), mockSuccess])("narrows %s successfully", (r) => {
|
||||
expect(__internals.isSuccess(r)).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
Err(true),
|
||||
mockError,
|
||||
{ kind: "ok", _value: true },
|
||||
prune(mockSuccess, "isOk"),
|
||||
prune(mockSuccess, "andThen", "orElse"),
|
||||
])("rejects %s correctly", (r) => {
|
||||
expect(__internals.isSuccess(r)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isError", () => {
|
||||
it.each([Err(true), mockError])("narrows %s successfully", (r) => {
|
||||
expect(__internals.isError(r)).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
Ok(true),
|
||||
mockSuccess,
|
||||
{ kind: "err", _error: true },
|
||||
prune(mockError, "isErr"),
|
||||
prune(mockError, "andThen", "orElse"),
|
||||
])("rejects %s correctly", (r) => {
|
||||
expect(__internals.isError(r)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function prune(u: object, ...fields: string[]): object {
|
||||
const copy = { ...u };
|
||||
for (const field of fields) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
delete (copy as any)[field];
|
||||
}
|
||||
return copy;
|
||||
}
|
178
src/__test__/index.test.ts
Normal file
178
src/__test__/index.test.ts
Normal file
|
@ -0,0 +1,178 @@
|
|||
import { inspect } from "util";
|
||||
import { AsyncResult, Err, Ok, Result, resultify } from "../";
|
||||
import { tryAssertErr, tryAssertOk } from ".";
|
||||
|
||||
function ntoa(n: number): string {
|
||||
return `${n}`;
|
||||
}
|
||||
|
||||
function tryNtoA(n: number): Result<string, []> {
|
||||
return Ok(ntoa(n));
|
||||
}
|
||||
|
||||
async function tryNtoAAsync(n: number): AsyncResult<string, []> {
|
||||
return tryNtoA(n);
|
||||
}
|
||||
|
||||
function tryAtoN(a: string): Result<number, []> {
|
||||
try {
|
||||
const n = Number(a);
|
||||
return Number.isNaN(n) ? Err([]) : Ok(n);
|
||||
} catch (e) {
|
||||
return Err([]);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryAtoNAsync(a: string): AsyncResult<number, []> {
|
||||
return tryAtoN(a);
|
||||
}
|
||||
|
||||
describe("@/api/common/result", () => {
|
||||
describe("Result", () => {
|
||||
it("isOk narrows in short circuit", () => {
|
||||
const r = tryNtoA(1).andThen(tryAtoN);
|
||||
|
||||
if (r.isOk()) {
|
||||
const t: number = r.value;
|
||||
expect(t).toEqual(1);
|
||||
return;
|
||||
}
|
||||
// never executes but does compile
|
||||
expect(r.error).toEqual([]);
|
||||
});
|
||||
|
||||
it("isErr narrows field in short circuit", () => {
|
||||
const r = tryAtoN("x");
|
||||
|
||||
if (r.isErr()) {
|
||||
const e: [] = r.error;
|
||||
expect(e).toEqual([]);
|
||||
return;
|
||||
}
|
||||
// will never execute, demonstrates that tsc still narrows
|
||||
expect(r.value).toEqual(1);
|
||||
});
|
||||
|
||||
it("flat maps a success", () => {
|
||||
const r = tryNtoA(1);
|
||||
expect(r.isOk()).toBeTruthy();
|
||||
expect(r.isErr()).toBeFalsy();
|
||||
tryAssertOk(r, (t) => {
|
||||
expect(t).toEqual("1");
|
||||
});
|
||||
const r2 = r.andThen(tryAtoN);
|
||||
expect(r2.isOk()).toBeTruthy();
|
||||
expect(r2.isErr()).toBeFalsy();
|
||||
tryAssertOk(r2, (t) => {
|
||||
expect(t).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("async flat maps a success", async () => {
|
||||
const r = await tryNtoAAsync(1);
|
||||
expect(r.isOk()).toBeTruthy();
|
||||
expect(r.isErr()).toBeFalsy();
|
||||
tryAssertOk(r, (t) => {
|
||||
expect(t).toEqual("1");
|
||||
});
|
||||
const r2 = await resultify(tryNtoAAsync(1)).andThenAsync(tryAtoNAsync);
|
||||
expect(r2.isOk()).toBeTruthy();
|
||||
expect(r2.isErr()).toBeFalsy();
|
||||
tryAssertOk(r2, (t) => {
|
||||
expect(t).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("flat maps an error", () => {
|
||||
const r = tryAtoN("x");
|
||||
expect(r.isErr()).toBeTruthy();
|
||||
expect(r.isOk()).toBeFalsy();
|
||||
const e = r.andThen(tryNtoA);
|
||||
expect(e.isErr()).toBeTruthy();
|
||||
expect(e.isOk()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("async flat maps an error", async () => {
|
||||
const r = tryAtoN("x");
|
||||
expect(r.isErr()).toBeTruthy();
|
||||
expect(r.isOk()).toBeFalsy();
|
||||
const e = await r.andThenAsync(tryNtoAAsync);
|
||||
expect(e.isErr()).toBeTruthy();
|
||||
expect(e.isOk()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("maps a success", () => {
|
||||
const r = tryAtoN("1").yield((n) => n + 1);
|
||||
tryAssertOk(r, (t) => {
|
||||
expect(t).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("async maps a success", async () => {
|
||||
const r = await tryAtoN("1").yieldAsync(async (n) => n + 1);
|
||||
tryAssertOk(r, (t) => {
|
||||
expect(t).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("maps an error", () => {
|
||||
const e = tryAtoN("x").yield((n) => n + 1);
|
||||
expect(e.isErr()).toBeTruthy();
|
||||
expect(e.isOk()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("maps an error type on success", () => {
|
||||
const r = tryAtoN("1").yieldErr((e) => {
|
||||
inspect(e);
|
||||
return "failed";
|
||||
});
|
||||
expect(r.isOk()).toBeTruthy();
|
||||
expect(r.isErr()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("maps an error type on error", () => {
|
||||
const e = tryAtoN("x").yieldErr((e) => {
|
||||
inspect(e);
|
||||
return "failed";
|
||||
});
|
||||
expect(e.isOk()).toBeFalsy();
|
||||
expect(e.isErr()).toBeTruthy();
|
||||
tryAssertErr(e, (e) => {
|
||||
expect(e).toEqual("failed");
|
||||
});
|
||||
});
|
||||
|
||||
it("and handles success", () => {
|
||||
const r: Result<string, []> = tryAtoN("1").and(Ok("success"));
|
||||
expect(r.isOk()).toBeTruthy();
|
||||
expect(r.isErr()).toBeFalsy();
|
||||
tryAssertOk(r, (t) => {
|
||||
expect(t).toEqual("success");
|
||||
});
|
||||
});
|
||||
|
||||
it("and handles error", () => {
|
||||
const r: Result<string, []> = tryAtoN("x").and(Ok("success"));
|
||||
expect(r.isOk()).toBeFalsy();
|
||||
expect(r.isErr()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("or handles success", () => {
|
||||
const r: Result<number, string> = tryAtoN("1").or(Ok(0));
|
||||
expect(r.isOk()).toBeTruthy();
|
||||
expect(r.isErr()).toBeFalsy();
|
||||
tryAssertOk(r, (t) => {
|
||||
expect(t).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("or handles error", () => {
|
||||
const r: Result<number, string> = tryAtoN("x").or(Ok(0));
|
||||
expect(r.isOk()).toBeTruthy();
|
||||
expect(r.isErr()).toBeFalsy();
|
||||
tryAssertOk(r, (t) => {
|
||||
expect(t).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
18
src/__test__/index.ts
Normal file
18
src/__test__/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { inspect } from "util";
|
||||
import { Result } from "..";
|
||||
|
||||
export function tryAssertOk<T, E>(r: Result<T, E>, a: (t: T) => void) {
|
||||
if (r.isOk()) {
|
||||
a(r.value);
|
||||
} else {
|
||||
throw `Result should have been Ok but was ${inspect(r, undefined, 15)}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function tryAssertErr<T, E>(r: Result<T, E>, a: (e: E) => void) {
|
||||
if (r.isErr()) {
|
||||
a(r.error);
|
||||
} else {
|
||||
throw `Result should have been Err but was ${inspect(r, undefined, 15)}`;
|
||||
}
|
||||
}
|
38
src/__test__/resultify.test.ts
Normal file
38
src/__test__/resultify.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Ok, Result } from "..";
|
||||
import { resultify } from "../resultify";
|
||||
|
||||
describe("resultify", () => {
|
||||
it("skips if a promise is already resultified", () => {
|
||||
const promise = Promise.resolve(Ok(0));
|
||||
const resultified = resultify(promise);
|
||||
// some state to close on to help testing
|
||||
let called = 0;
|
||||
// replace a simple combinator with a compatible one that tracks state
|
||||
resultified.and = <U>(r: Result<U, unknown>) => {
|
||||
called += 1;
|
||||
return resultify(Promise.resolve(r));
|
||||
};
|
||||
resultified.and(Ok(1));
|
||||
expect(called).toEqual(1);
|
||||
// calling again should *not* replace the custom combinator
|
||||
resultify(resultified);
|
||||
resultified.and(Ok(2));
|
||||
// should still increment if resultify skipped an already resultified
|
||||
// promise
|
||||
expect(called).toEqual(2);
|
||||
});
|
||||
|
||||
it("throws if called with undefined", () => {
|
||||
expect(() =>
|
||||
resultify(undefined as unknown as Promise<Result<unknown, unknown>>),
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("accepts a result and wraps with a promise", async () => {
|
||||
expect(
|
||||
await resultify(Ok(3)).andThenAsync(
|
||||
async (i): Promise<Result<number, never>> => Ok(i * 2),
|
||||
),
|
||||
).toEqual(Ok(6));
|
||||
});
|
||||
});
|
30
src/__test__/unit.test.ts
Normal file
30
src/__test__/unit.test.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { passOrYield } from "../unit";
|
||||
|
||||
class TestError {
|
||||
constructor(readonly message: string) {}
|
||||
}
|
||||
|
||||
describe("passOrYield", () => {
|
||||
const f = () => new TestError("Assert!");
|
||||
it("passes constructed error correctly", () => {
|
||||
const e = new TestError("Failed!");
|
||||
expect(passOrYield(TestError, f)(e)).toEqual(e);
|
||||
});
|
||||
|
||||
it("handles thrown object", () => {
|
||||
try {
|
||||
throw new TestError("Thrown!");
|
||||
} catch (e) {
|
||||
expect(passOrYield(TestError, f)(e)).toEqual(e);
|
||||
}
|
||||
});
|
||||
|
||||
it("handles a synthetic error", () => {
|
||||
// 3rd party code can mangle our errors but usually produce a descendant
|
||||
// of the builtin, Error, and preserve the name from our classes
|
||||
// constructor function
|
||||
const synthetic = new Error("Synthetic!");
|
||||
synthetic.name = TestError.name;
|
||||
expect(passOrYield(TestError, f)(synthetic)).toEqual(synthetic);
|
||||
});
|
||||
});
|
105
src/carry.ts
Normal file
105
src/carry.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { AsyncResult, Result } from ".";
|
||||
import { resultify } from "./resultify";
|
||||
|
||||
type Arg = unknown;
|
||||
|
||||
/**
|
||||
* This function is designed to make for comprehensions a little easier to compose without diagonalizing.
|
||||
*
|
||||
* A for comprehension is a pattern of chaining `Result` with a series of `andThen`/`andThenAsync` calls, passing
|
||||
* along incremental plus accumulated results to each next step, then making a final yield call to get the total
|
||||
* output from the chain if all were successful.
|
||||
*
|
||||
* NOTE: Because of limitations of Typescript, incremental results, that is the new value produce by an `andThen`/
|
||||
* `andThenAsync`, will be *pre*-pended to the front of the accumulated results. Also, tsc can quickly fail to infer
|
||||
* the changes to the accumulated type, a tuple to which each incremental type will be added. Adding a type annotation
|
||||
* between the `andThen`/`andThenAsync` and the open parenthesis for the arguments should be sufficient to clear up
|
||||
* tsc's confusion.
|
||||
*
|
||||
* Example, from MQ's side effect handling when a subscription changes:
|
||||
*
|
||||
* return await resultify(tryNarrowSubscription(event.data)).andThenAsync(
|
||||
* async ({ object: subscription }) => {
|
||||
* return await resultify(tryGetSubscription(subscription))
|
||||
* .andThen(tryNarrowSubscriptionCustomerId)
|
||||
* // type hint here because `carry` is used to add the determined plan
|
||||
* // to the tuple already containing the retrieved and narrowed subscription
|
||||
* .andThen<[PlanType, SubscriptionWithCustomerId]>(
|
||||
* (subscription) => {
|
||||
* const { price } = subscription.items.data[0];
|
||||
* return tryDeterminePlan(price).yield(carry(subscription));
|
||||
* },
|
||||
* )
|
||||
* // and another link in the chain, adding the user from the database to
|
||||
* // the tuple that has already accumulated the subscription and plan type
|
||||
* .andThenAsync<[User, PlanType, SubscriptionWithCustomerId]>(
|
||||
* async ([planType, subscription]) => {
|
||||
* const { customer: stripeCustomerId } = subscription;
|
||||
* return resultify(
|
||||
* getRepository(User).tryFindStripeCustomer(stripeCustomerId),
|
||||
* ).yield(carry(planType, subscription));
|
||||
* },
|
||||
* )
|
||||
* // this is still part of the for comprehension, it happens to consume some of the state
|
||||
* // and uses a simple yield call without carry to set a constant output, that the event was
|
||||
* // handled
|
||||
* .andThenAsync(async ([user, planType, subscription]) => {
|
||||
* return await resultify(
|
||||
* createMauticContact(
|
||||
* QueueName.MauticStripe,
|
||||
* user.email,
|
||||
* planType,
|
||||
* user?.firstName,
|
||||
* subscription.status,
|
||||
* ),
|
||||
* ).yield(() => EventOutcome.Handled);
|
||||
* })
|
||||
* .orElse(skipUnknownPlan)
|
||||
* // here the final yield is simply doing some logging but in API-land, a more typical pattern
|
||||
* // would be to add the accumulated outputs from the rest of the comprehension into some singular value
|
||||
* // other than the tuple used simply to carry those outputs to this last step
|
||||
* .yield(
|
||||
* logOutcome(
|
||||
* logger,
|
||||
* outcomeMessage("mautic", "Stripe", event.type),
|
||||
* event,
|
||||
* ),
|
||||
* );
|
||||
* },
|
||||
* );
|
||||
*/
|
||||
export function carry<T extends Array<Arg>, U>(...ts: T): (u: U) => [U, ...T] {
|
||||
return (u: U) => {
|
||||
return [u, ...ts];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative to `carry` that can be used to wrap a closure before passing it to `andThen` so that there is no
|
||||
* need to manually add a `.yield(carry(...))` in the closure's logic.
|
||||
*
|
||||
* NOTE: This currently doesn't work well with longer for comprehensions since it makes the flow of types a little
|
||||
* more opaque to tsc than simply using `carry`.
|
||||
*/
|
||||
export function yieldWithSync<T extends Array<Arg>, U, E>(
|
||||
f: (ts: T) => Result<U, E>,
|
||||
): (ts: T) => Result<[U, ...T], E> {
|
||||
return (ts: T) => {
|
||||
return f(ts).yield((u) => [u, ...ts]);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative to `carry` that can be used to wrap a closure before passing it to `andThen` so that there is no
|
||||
* need to manually add a `.yield(carry(...))` in the closure's logic.
|
||||
*
|
||||
* NOTE: This currently doesn't work well with longer for comprehensions since it makes the flow of types a little
|
||||
* more opaque to tsc than simply using `carry`.
|
||||
*/
|
||||
export function yieldWith<T extends Array<Arg>, U, E>(
|
||||
f: (ts: T) => AsyncResult<U, E>,
|
||||
): (ts: T) => AsyncResult<[U, ...T], E> {
|
||||
return async (ts: T) => {
|
||||
return await resultify(f(ts)).yield((u) => [u, ...ts]);
|
||||
};
|
||||
}
|
22
src/combinators.ts
Normal file
22
src/combinators.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Ok, Result } from ".";
|
||||
|
||||
export function and<T, U, E>(r: Result<T, E>, u: Result<U, E>): Result<U, E> {
|
||||
return r.andThen(() => u);
|
||||
}
|
||||
|
||||
export function or<T, E, F>(r: Result<T, E>, u: Result<T, F>): Result<T, F> {
|
||||
return r.orElse(() => u);
|
||||
}
|
||||
|
||||
export async function mapAsync<T, U, E>(
|
||||
r: Result<T, E>,
|
||||
f: (t: T) => Promise<U>,
|
||||
): Promise<Result<U, E>> {
|
||||
return await r.andThenAsync(async (t) => Ok(await f(t)));
|
||||
}
|
||||
export function mapSync<T, U, E>(
|
||||
r: Result<T, E>,
|
||||
f: (t: T) => U,
|
||||
): Result<U, E> {
|
||||
return r.andThen((t) => Ok(f(t)));
|
||||
}
|
67
src/guards.ts
Normal file
67
src/guards.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import { Result } from ".";
|
||||
|
||||
/**
|
||||
* A useful guard for the our handler framework where a route function can
|
||||
* return `void` to indicate it is handling the response or a `Result` so the
|
||||
* framework can automatically handle success and failure.
|
||||
*/
|
||||
export function isResult<T, E>(u: unknown): u is Result<T, E> {
|
||||
return (
|
||||
u instanceof Result.Ok ||
|
||||
u instanceof Result.Err ||
|
||||
isSuccess(u) ||
|
||||
isError(u)
|
||||
);
|
||||
}
|
||||
|
||||
function isSuccess<T>(u: unknown): u is Result.Ok<T> {
|
||||
return (
|
||||
isObject(u) &&
|
||||
"kind" in u &&
|
||||
u.kind === "ok" &&
|
||||
"_value" in u &&
|
||||
hasMethod(u, "isOk") &&
|
||||
hasCombinators(u)
|
||||
);
|
||||
}
|
||||
|
||||
function isError<T>(u: unknown): u is Result.Ok<T> {
|
||||
return (
|
||||
isObject(u) &&
|
||||
"kind" in u &&
|
||||
u.kind === "err" &&
|
||||
"_error" in u &&
|
||||
hasMethod(u, "isErr") &&
|
||||
hasCombinators(u)
|
||||
);
|
||||
}
|
||||
|
||||
function isObject(u: unknown): u is object {
|
||||
return u !== undefined && u !== null && typeof u === "object";
|
||||
}
|
||||
|
||||
function hasMethod(u: object, method: string): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return method in u && typeof (u as any)[method] === "function";
|
||||
}
|
||||
|
||||
function hasCombinators(u: object): boolean {
|
||||
return [
|
||||
"and",
|
||||
"andThen",
|
||||
"andThenAsync",
|
||||
"or",
|
||||
"orElse",
|
||||
"orElseAsync",
|
||||
"yield",
|
||||
"yieldAsync",
|
||||
"yieldErr",
|
||||
"yieldOr",
|
||||
"yieldOrElse",
|
||||
].every((combinator) => hasMethod(u, combinator));
|
||||
}
|
||||
|
||||
export const __internals = {
|
||||
isError,
|
||||
isSuccess,
|
||||
};
|
312
src/index.ts
Normal file
312
src/index.ts
Normal file
|
@ -0,0 +1,312 @@
|
|||
import * as combinators from "./combinators";
|
||||
|
||||
export { carry, yieldWith, yieldWithSync } from "./carry";
|
||||
export { isResult } from "./guards";
|
||||
export { resultify } from "./resultify";
|
||||
export { passOrYield, tryAsync, trySync } from "./unit";
|
||||
|
||||
export function collectErrs<T, E>(rs: Result<T, E>[]): E[] {
|
||||
return rs.filter((r) => r.isErr()).map((e) => e.error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the success variant of a result, includes the tag, field, and
|
||||
* guards to narrow and access the contained value.
|
||||
*/
|
||||
export interface Ok<T> {
|
||||
kind: "ok";
|
||||
value: T;
|
||||
|
||||
isOk(this: Result<T, unknown>): this is Ok<T>;
|
||||
isErr(this: Result<T, unknown>): this is Err<never>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for the failure variant of a result, includes the tag, field, and
|
||||
* guards to narrow and access the contained value.
|
||||
*/
|
||||
export interface Err<E> {
|
||||
kind: "err";
|
||||
error: E;
|
||||
|
||||
isOk(this: Result<unknown, E>): this is Ok<never>;
|
||||
isErr(this: Result<unknown, E>): this is Err<E>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common interface for accessing the inner values of results. In use, one or
|
||||
* the other accessor will be of type never. A use of a result will first need
|
||||
* to narrow via one of the type guard methods, {@method isOk} and {@method
|
||||
* isErr} to narrow the generics correctly and use the methods of this
|
||||
* interface.
|
||||
*/
|
||||
export interface Unwrap<T, E> {
|
||||
get value(): T;
|
||||
|
||||
get error(): E;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common interface for all of the useful combinators that allow accessing and
|
||||
* working with interior values of {@type Result}.
|
||||
*/
|
||||
export interface Combinators<T, E> {
|
||||
/**
|
||||
* If this result is {@type OK}, return {@param r} otherwise preserve any
|
||||
* {@type Err}. If you need to run any logic to produce a new value, use
|
||||
* {@method andThen} instead.
|
||||
*/
|
||||
and<U>(r: Result<U, E>): Result<U, E>;
|
||||
|
||||
/**
|
||||
* If this result is {@type OK}, evaluates {@param f} and returns, otherwise
|
||||
* preserve any {@type Err}. `andThen` is a synonym for `flatMap`.
|
||||
*/
|
||||
andThen<U>(f: (t: T) => Result<U, E>): Result<U, E>;
|
||||
|
||||
/**
|
||||
* Variation of {@method andThen} that accepts an asynchronous lambda, useful
|
||||
* with {@function thenPipeAsync} to compose {@type Result} resolving async
|
||||
* functions.
|
||||
*/
|
||||
andThenAsync<U>(f: (t: T) => AsyncResult<U, E>): AsyncResult<U, E>;
|
||||
|
||||
/**
|
||||
* If this result is {@type Err}, return {@param r} otherwise preserve any
|
||||
* {@type Ok}. If you need to run any logic to produce a new value, use
|
||||
* {@method orElse} instead.
|
||||
*/
|
||||
or<F>(r: Result<T, F>): Result<T, F>;
|
||||
|
||||
/**
|
||||
* Accept a lambda to execute in case of an error.
|
||||
*/
|
||||
orElse<F>(f: (e: E) => Result<T, F>): Result<T, F>;
|
||||
|
||||
/**
|
||||
* Accept an async lambda to execute in case of an error.
|
||||
*/
|
||||
orElseAsync<F>(f: (e: E) => AsyncResult<T, F>): AsyncResult<T, F>;
|
||||
|
||||
/**
|
||||
* Synonym for map, useful at the end of a chain of {@method andThen} calls
|
||||
* to adjust the final value before unwrapping.
|
||||
*/
|
||||
yield<U>(f: (t: T) => U): Result<U, E>;
|
||||
|
||||
/**
|
||||
* Variation of yield that accepts an asynchronous closure.
|
||||
*/
|
||||
yieldAsync<U>(f: (t: T) => Promise<U>): AsyncResult<U, E>;
|
||||
|
||||
/**
|
||||
* A flat map for the error of this {@type Result}.
|
||||
*/
|
||||
yieldErr<F>(f: (e: E) => F): Result<T, F>;
|
||||
|
||||
/**
|
||||
* If this result is {@type Err}, return an {@type Ok} containing {@param u},
|
||||
* otherwise execute {@param f}. If you need to run some logic to produce the
|
||||
* value for the argument, {@param u}, use {@method yieldOrElse} instead.
|
||||
*/
|
||||
yieldOr<U>(u: U, f: (t: T) => U): Result<U, E>;
|
||||
|
||||
/**
|
||||
* Accept a lambda to execute and produce a default value in case of an
|
||||
* error.
|
||||
*/
|
||||
yieldOrElse<U>(u: (e: E) => U, f: (t: T) => U): Result<U, E>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Top type for our Try type.
|
||||
*
|
||||
* This is based on the approach explained in
|
||||
* https://www.jmcelwa.in/posts/rust-like-enums/ and after several attempts is
|
||||
* the only way to get the type guard methods of the variants to narrow
|
||||
* correctly for all use cases.
|
||||
*
|
||||
* Using never here with {@type Unwrap} works to ensure only the correct inner
|
||||
* values are accessible and avoids the problems that arise using
|
||||
* never/any/unknown directly on the implementation classes.
|
||||
*/
|
||||
export type Result<T, E> = (
|
||||
| (Ok<T> & Unwrap<T, never>)
|
||||
| (Err<E> & Unwrap<never, E>)
|
||||
) &
|
||||
Combinators<T, E>;
|
||||
|
||||
export function Ok<T, E>(t: T): Result<T, E> {
|
||||
return new Result.Ok(t);
|
||||
}
|
||||
|
||||
export function Err<T, E>(e: E): Result<T, E> {
|
||||
return new Result.Err(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* An alias that makes reading async function signatures that return promises a
|
||||
* bit easier.
|
||||
*/
|
||||
export type AsyncResult<T, E> = Promise<Result<T, E>>;
|
||||
|
||||
/**
|
||||
* This namespace allows the concrete implementations to have the same name as
|
||||
* their respective key interfaces. Callers should not need to work with the
|
||||
* namespace or its contained classes directly.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Result {
|
||||
export class Ok<T> implements Ok<T>, Unwrap<T, never>, Combinators<T, never> {
|
||||
kind = "ok" as const;
|
||||
|
||||
constructor(readonly _value: T) {}
|
||||
|
||||
isOk(this: Result<T, unknown>): this is Ok<T> {
|
||||
return this.kind === "ok";
|
||||
}
|
||||
|
||||
isErr(this: Result<T, unknown>): this is Err<never> {
|
||||
return !this.isOk();
|
||||
}
|
||||
|
||||
and<U, E>(r: Result<U, E>): Result<U, E> {
|
||||
return combinators.and<T, U, E>(this, r);
|
||||
}
|
||||
|
||||
andThen<U, E>(f: (t: T) => Result<U, E>): Result<U, E> {
|
||||
return f(this.value);
|
||||
}
|
||||
|
||||
async andThenAsync<U, E>(
|
||||
f: (t: T) => AsyncResult<U, E>,
|
||||
): AsyncResult<U, E> {
|
||||
return await f(this.value);
|
||||
}
|
||||
|
||||
or<F>(r: Result<T, F>): Result<T, F> {
|
||||
return combinators.or(this, r);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
orElse<F>(_: (e: never) => Result<T, F>): Result<T, F> {
|
||||
return this;
|
||||
}
|
||||
|
||||
async orElseAsync<F>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_: (e: never) => AsyncResult<T, F>,
|
||||
): AsyncResult<T, F> {
|
||||
return this;
|
||||
}
|
||||
|
||||
yield<U, E>(f: (t: T) => U): Result<U, E> {
|
||||
return combinators.mapSync<T, U, E>(this, f);
|
||||
}
|
||||
|
||||
async yieldAsync<U, E>(f: (t: T) => Promise<U>): AsyncResult<U, E> {
|
||||
return await combinators.mapAsync<T, U, E>(this, f);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
yieldErr<E, F>(_: (e: E) => F): Result<T, F> {
|
||||
return this;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
yieldOr<U>(_: U, f: (t: T) => U): Result<U, never> {
|
||||
return this.yield(f);
|
||||
}
|
||||
|
||||
yieldOrElse<U>(d: (e: never) => U, f: (t: T) => U): Result<U, never> {
|
||||
return this.yieldOr(d(this.error), f);
|
||||
}
|
||||
|
||||
get value(): T {
|
||||
return this._value;
|
||||
}
|
||||
|
||||
get error(): never {
|
||||
throw new Error(
|
||||
"Cannot retrieve error value from OK! Has this result been narrowed using isOk/isErr?",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Err<E>
|
||||
implements Err<E>, Unwrap<never, E>, Combinators<never, E>
|
||||
{
|
||||
kind = "err" as const;
|
||||
|
||||
constructor(readonly _error: E) {}
|
||||
|
||||
isOk(this: Result<unknown, E>): this is Ok<never> {
|
||||
return !this.isErr();
|
||||
}
|
||||
|
||||
isErr(this: Result<unknown, E>): this is Err<E> {
|
||||
return this.kind === "err";
|
||||
}
|
||||
|
||||
and<U>(r: Result<U, E>): Result<U, E> {
|
||||
return combinators.and(this, r);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
andThen<T, U>(_: (t: T) => Result<U, E>): Result<U, E> {
|
||||
return this;
|
||||
}
|
||||
|
||||
async andThenAsync<T, U>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_: (t: T) => AsyncResult<U, E>,
|
||||
): AsyncResult<U, E> {
|
||||
return this;
|
||||
}
|
||||
|
||||
or<T, F>(r: Result<T, F>): Result<T, F> {
|
||||
return combinators.or<T, E, F>(this, r);
|
||||
}
|
||||
|
||||
orElse<F>(f: (e: E) => Result<never, F>): Result<never, F> {
|
||||
return f(this.error);
|
||||
}
|
||||
|
||||
async orElseAsync<F>(
|
||||
f: (e: E) => AsyncResult<never, F>,
|
||||
): AsyncResult<never, F> {
|
||||
return await f(this.error);
|
||||
}
|
||||
|
||||
yield<T, U>(f: (t: T) => U): Result<U, E> {
|
||||
return combinators.mapSync(this, f);
|
||||
}
|
||||
|
||||
async yieldAsync<T, U>(f: (t: T) => Promise<U>): AsyncResult<U, E> {
|
||||
return await combinators.mapAsync(this, f);
|
||||
}
|
||||
|
||||
yieldErr<T, F>(f: (e: E) => F): Result<T, F> {
|
||||
return new Result.Err(f(this.error));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
yieldOr<U>(u: U, _: (t: never) => U): Result<U, E> {
|
||||
return new Result.Ok(u);
|
||||
}
|
||||
|
||||
yieldOrElse<U>(u: (e: E) => U, f: (t: never) => U): Result<U, E> {
|
||||
return this.yieldOr(u(this._error), f);
|
||||
}
|
||||
|
||||
get value(): never {
|
||||
throw new Error(
|
||||
"Cannot retrieve value from Err! Has this result been narrowed using isOk/isErr?",
|
||||
);
|
||||
}
|
||||
|
||||
get error(): E {
|
||||
return this._error;
|
||||
}
|
||||
}
|
||||
}
|
66
src/pipe.ts
Normal file
66
src/pipe.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { AsyncResult, Result } from ".";
|
||||
|
||||
/** @internal */
|
||||
export function thenPipe<T, U, E>(
|
||||
f: (t: T) => Result<U, E>,
|
||||
): (r: Result<T, E>) => Result<U, E> {
|
||||
return (r: Result<T, E>): Result<U, E> => r.andThen(f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function thenPipeAsync<T, U, E>(
|
||||
f: (t: T) => AsyncResult<U, E>,
|
||||
): (r: Result<T, E>) => AsyncResult<U, E> {
|
||||
return async (r: Result<T, E>): AsyncResult<U, E> => await r.andThenAsync(f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function yieldPipe<T, U, E>(
|
||||
f: (t: T) => U,
|
||||
): (r: Result<T, E>) => Result<U, E> {
|
||||
return (r: Result<T, E>): Result<U, E> => r.yield(f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function yieldOrPipe<T, U, E>(
|
||||
u: U,
|
||||
f: (t: T) => U,
|
||||
): (r: Result<T, E>) => Result<U, E> {
|
||||
return (r: Result<T, E>): Result<U, E> => r.yieldOr(u, f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function yieldOrElsePipe<T, U, E>(
|
||||
u: (e: E) => U,
|
||||
f: (t: T) => U,
|
||||
): (r: Result<T, E>) => Result<U, E> {
|
||||
return (r: Result<T, E>): Result<U, E> => r.yieldOrElse(u, f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function elsePipe<T, E, F>(
|
||||
f: (e: E) => Result<T, F>,
|
||||
): (r: Result<T, E>) => Result<T, F> {
|
||||
return (r: Result<T, E>): Result<T, F> => r.orElse(f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function elsePipeAsync<T, E, F>(
|
||||
f: (e: E) => AsyncResult<T, F>,
|
||||
): (r: Result<T, E>) => AsyncResult<T, F> {
|
||||
return async (r: Result<T, E>): AsyncResult<T, F> => r.orElseAsync(f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function yieldPipeAsync<T, U, E>(
|
||||
f: (t: T) => Promise<U>,
|
||||
): (r: Result<T, E>) => AsyncResult<U, E> {
|
||||
return async (r: Result<T, E>): AsyncResult<U, E> => r.yieldAsync(f);
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function errPipe<T, E, F>(
|
||||
f: (e: E) => F,
|
||||
): (r: Result<T, E>) => Result<T, F> {
|
||||
return (r: Result<T, E>): Result<T, F> => r.yieldErr(f);
|
||||
}
|
117
src/resultify.ts
Normal file
117
src/resultify.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
import { AsyncResult, Result } from ".";
|
||||
import {
|
||||
elsePipe,
|
||||
elsePipeAsync,
|
||||
errPipe,
|
||||
thenPipe,
|
||||
thenPipeAsync,
|
||||
yieldOrElsePipe,
|
||||
yieldOrPipe,
|
||||
yieldPipe,
|
||||
yieldPipeAsync,
|
||||
} from "./pipe";
|
||||
|
||||
export type Resultified<T, E> = Promise<Result<T, E>> & {
|
||||
and<U>(r: Result<U, E>): Resultified<U, E>;
|
||||
|
||||
andThen<U>(f: (t: T) => Result<U, E>): Resultified<U, E>;
|
||||
|
||||
andThenAsync<U>(f: (t: T) => AsyncResult<U, E>): Resultified<U, E>;
|
||||
|
||||
or<F>(r: Result<T, F>): Resultified<T, F>;
|
||||
|
||||
orElse<F>(f: (e: E) => Result<T, F>): Resultified<T, F>;
|
||||
|
||||
orElseAsync<F>(f: (e: E) => AsyncResult<T, F>): Resultified<T, F>;
|
||||
|
||||
yield<U>(f: (t: T) => U): Resultified<U, E>;
|
||||
|
||||
yieldAsync<U>(f: (t: T) => Promise<U>): Resultified<U, E>;
|
||||
|
||||
yieldErr<F>(f: (e: E) => F): Resultified<T, F>;
|
||||
|
||||
yieldOr<U>(u: U, f: (t: T) => U): Resultified<U, E>;
|
||||
|
||||
yieldOrElse<U>(u: (e: E) => U, f: (t: T) => U): Resultified<U, E>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Consumes a {@type Result<T, E>} or {@type Promise<Result<T, E>>} and patches
|
||||
* it so that the methods of {@type Result<T, E>} can be called on the outer
|
||||
* {@type Promise}. If the argument is a {@type Result<T, E>}, this function
|
||||
* wraps it in an immediately resolved {@type Promise}; this behavior
|
||||
* simplifies starting a call chain with a call to a synchronous, {@type
|
||||
* Result} returning function. This function will not re-patch an already
|
||||
* modified promise meaning it is safe to call again with the same promise if
|
||||
* unsure.
|
||||
*/
|
||||
export function resultify<T, E>(
|
||||
resultOrPromise: Result<T, E> | Promise<Result<T, E>>,
|
||||
): Resultified<T, E> {
|
||||
const promise = intoPromise(resultOrPromise);
|
||||
const resultified = promise as Resultified<T, E>;
|
||||
if (!resultOrPromise || !resultified) {
|
||||
throw new Error(
|
||||
"Promise was undefined or null! If this error occurs during testing, make sure your mocks return a Promise<Result>",
|
||||
);
|
||||
}
|
||||
if (resultified.andThen && typeof resultified.andThen === "function") {
|
||||
return resultified;
|
||||
}
|
||||
|
||||
resultified.and = <U>(r: Result<U, E>): Resultified<U, E> =>
|
||||
resultify(promise.then(() => r));
|
||||
|
||||
resultified.andThen = <U>(f: (t: T) => Result<U, E>): Resultified<U, E> =>
|
||||
resultify(promise.then(thenPipe(f)));
|
||||
|
||||
resultified.andThenAsync = <U>(
|
||||
f: (t: T) => AsyncResult<U, E>,
|
||||
): Resultified<U, E> => resultify(promise.then(thenPipeAsync(f)));
|
||||
|
||||
resultified.or = <F>(r: Result<T, F>): Resultified<T, F> =>
|
||||
resultify(promise.then(() => r));
|
||||
|
||||
resultified.orElse = <F>(f: (e: E) => Result<T, F>): Resultified<T, F> =>
|
||||
resultify(promise.then(elsePipe(f)));
|
||||
|
||||
resultified.orElseAsync = <F>(
|
||||
f: (e: E) => AsyncResult<T, F>,
|
||||
): Resultified<T, F> => resultify(promise.then(elsePipeAsync(f)));
|
||||
|
||||
resultified.yield = <U>(f: (t: T) => U): Resultified<U, E> =>
|
||||
resultify(promise.then(yieldPipe(f)));
|
||||
|
||||
resultified.yieldAsync = <U>(f: (t: T) => Promise<U>): Resultified<U, E> =>
|
||||
resultify(promise.then(yieldPipeAsync(f)));
|
||||
|
||||
resultified.yieldErr = <F>(f: (e: E) => F): Resultified<T, F> =>
|
||||
resultify(promise.then(errPipe(f)));
|
||||
|
||||
resultified.yieldOr = <U>(u: U, f: (t: T) => U): Resultified<U, E> =>
|
||||
resultify(promise.then(yieldOrPipe(u, f)));
|
||||
|
||||
resultified.yieldOrElse = <U>(
|
||||
u: (e: E) => U,
|
||||
f: (t: T) => U,
|
||||
): Resultified<U, E> => resultify(promise.then(yieldOrElsePipe(u, f)));
|
||||
|
||||
return resultified;
|
||||
}
|
||||
|
||||
function intoPromise<T, E>(
|
||||
resultOrPromise: Result<T, E> | Promise<Result<T, E>>,
|
||||
): Promise<Result<T, E>> {
|
||||
return isResult(resultOrPromise)
|
||||
? Promise.resolve(resultOrPromise)
|
||||
: resultOrPromise;
|
||||
}
|
||||
|
||||
function isResult<T, E>(
|
||||
resultOrPromise: Result<T, E> | Promise<Result<T, E>>,
|
||||
): resultOrPromise is Result<T, E> {
|
||||
return (
|
||||
resultOrPromise instanceof Result.Ok ||
|
||||
resultOrPromise instanceof Result.Err
|
||||
);
|
||||
}
|
43
src/unit.ts
Normal file
43
src/unit.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { Err, Ok, Result } from ".";
|
||||
|
||||
/**
|
||||
* Convert an exception throwing function into a {@type Result} returning one.
|
||||
*/
|
||||
export function trySync<T, E>(f: () => T, e: (e: unknown) => E): Result<T, E> {
|
||||
try {
|
||||
return Ok(f());
|
||||
} catch (cause) {
|
||||
return Err(e(cause));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an exception throwing async function into a {@type Result} returning
|
||||
* one.
|
||||
*/
|
||||
export async function tryAsync<T, E>(
|
||||
f: () => Promise<T>,
|
||||
e: (e: unknown) => E,
|
||||
): Promise<Result<T, E>> {
|
||||
try {
|
||||
return Ok(await f());
|
||||
} catch (cause) {
|
||||
return Err(e(cause));
|
||||
}
|
||||
}
|
||||
|
||||
export function passOrYield<E>(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
errorClass: Function,
|
||||
f: (error: unknown) => E,
|
||||
): (error: unknown) => E {
|
||||
return (error: unknown) => {
|
||||
if (
|
||||
error instanceof errorClass ||
|
||||
(error instanceof Error && error.name === errorClass.name)
|
||||
) {
|
||||
return error as E;
|
||||
}
|
||||
return f(error);
|
||||
};
|
||||
}
|
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"incremental": true,
|
||||
"isolatedModules": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"target": "esNext",
|
||||
"outDir": "./dist/",
|
||||
"rootDir": "./src/"
|
||||
},
|
||||
"exclude": ["dist"],
|
||||
"include": ["./src/**/*.ts"]
|
||||
}
|
Loading…
Reference in a new issue