This commit is contained in:
Anton Nesterov 2024-10-16 07:02:31 +02:00
commit 849b917d50
No known key found for this signature in database
GPG key ID: 59121E8AE2851FB5
8 changed files with 460 additions and 0 deletions

175
.gitignore vendored Normal file
View file

@ -0,0 +1,175 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

70
README.md Normal file
View file

@ -0,0 +1,70 @@
# Grip
Simplified result/error handling for JavaScript.
Grip always returns a consistent call result ready to be handled.
It makes the control flow similar to that of Golang, but doesn't force you to make additional null checks or create transitional variables to hold error results.
## Install
```bash
bun add github:nesterow/grip
```
## Usage
The `grip` function accepts a function or a promise and returns a result with return value and status.
The result can be hadled as either an object or a tuple.
```javascript
import { grip } from '@nesterow/grip';
```
## Handle result as an object:
The result can be handled as an object: `{value, status, Ok(), Fail(), Of(type)}`
```javascript
const fetchResult = await grip(
fetch('https://api.example.com')
);
if (fetchResult.Fail()) {
handleErrorProperly();
return;
}
const jsonResult = await grip(
res.value.json()
);
if (jsonResult.Of(SyntaxError)) {
handleJsonParseError();
return;
}
```
## Handle result as a tuple:
The result can also be received as a tuple if you want to handle errors in Go'ish style:
```javascript
const [response, fetchStatus] = await grip(
fetch('https://api.example.com')
);
if (fetchStatus.Fail()) {
handleErrorProperly();
return;
}
const [json, parseStatus] = await grip(
response.json()
);
if (parseStatus.Of(SyntaxError)) {
handleJsonParseError();
return;
}
```
## License
MIT

BIN
bun.lockb Executable file

Binary file not shown.

65
grip.test.ts Normal file
View file

@ -0,0 +1,65 @@
import { test, expect } from "bun:test";
import { grip } from "./grip";
test("Promise", async () => {
const [result, status] = await grip(Promise.resolve("ok"));
expect(result).toBe("ok");
expect(status.Ok()).toBe(true);
});
test("Promise.reject", async () => {
const [result, status] = await grip(() =>
Promise.reject(new Error("not ok")),
);
expect(status.Ok()).toBe(false);
expect(status.message).toBe("not ok");
expect(result === null).toBe(true);
});
test("() => Promise", async () => {
const [result, status] = await grip(() => Promise.resolve("ok"));
expect(result).toBe("ok");
expect(status.Ok()).toBe(true);
});
test("() => value", () => {
const [result, status] = grip(() => "ok");
expect(result).toBe("ok");
expect(status.Ok()).toBe(true);
});
test("() => throw", async () => {
const [_, status] = grip(() => {
if (1) throw "not ok";
});
expect(status.Ok()).toBe(false);
expect(status.message).toBe("not ok");
});
test("Result { 0, 1, value, Ok(), Fail() }", async () => {
const result = grip(() => {
if (1) throw "not ok";
});
expect(result.Ok()).toBe(false);
expect(result.value === null).toBe(true);
expect(result.status.message).toBe("not ok");
});
test("fetch err", async () => {
const [result, status] = await grip(fetch("https://localhost:30012"));
expect(status.Ok()).toBe(false);
expect(result === null).toBe(true);
expect(status.Of(Error)).toBe(true);
});
test("fetch json", async () => {
const [result, fetchStatus] = await grip(() =>
fetch("https://google.com/404"),
);
expect(fetchStatus.Ok()).toBe(true);
expect(result.ok).toBe(false);
const [json, jsonStatus] = await grip(result.json());
expect(jsonStatus.Ok()).toBe(false);
expect(jsonStatus.Of(SyntaxError)).toBe(true);
expect(json === null).toBe(true);
});

107
grip.ts Normal file
View file

@ -0,0 +1,107 @@
interface Status extends Error {
Ok(): boolean;
Fail(): boolean;
Of(cls: any): boolean;
}
export class Err extends Error {
Ok() {
return false;
}
Fail() {
return true;
}
Of(cls: any) {
return this.cause instanceof cls || this instanceof cls;
}
static fromCatch(error: any) {
const e = new Err(typeof error === "string" ? error : error.message);
e.cause = error;
e.stack = error.stack;
return e;
}
}
export class Ok extends Error {
Ok() {
return true;
}
Fail() {
return false;
}
Of(cls: any) {
return this instanceof cls;
}
}
interface IResult<T> {
0: T;
1: Status;
value: T;
status: Status;
Of(cls: any): boolean;
Ok(): boolean;
Fail(): boolean;
}
class Result<T> extends Array<T | Status> implements IResult<T> {
0: T;
1: Status;
constructor(result: T, status: Status) {
super(2);
this[0] = result;
this[1] = status;
}
get value() {
return this[0];
}
get status() {
return this[1];
}
Ok() {
return (this[1] as Status).Ok();
}
Fail() {
return (this[1] as Status).Fail();
}
Of(impl: any) {
return (this[1] as Status).Of(impl);
}
}
type Unwrap<T> =
T extends Promise<infer U>
? U
: T extends (...args: any) => Promise<infer U>
? U
: T extends (...args: any) => infer U
? U
: T;
export type SafeResult<T> =
T extends Promise<any>
? Promise<Result<Unwrap<T>>>
: T extends () => Promise<any>
? Promise<Result<Unwrap<T>>>
: Result<Unwrap<T>>;
export function grip<T>(action: T): SafeResult<T> {
if (action instanceof Promise) {
return promise<T>(action) as SafeResult<T>;
}
try {
const result = (action as any)();
if (result instanceof Promise) {
return promise<T>(result) as SafeResult<T>;
}
return new Result<T>(result, new Ok()) as SafeResult<T>;
} catch (err: any) {
return new Result<T>(null as any, Err.fromCatch(err)) as SafeResult<T>;
}
}
const promise = <T>(result: Promise<T>) => {
return result
.then((res) => new Result(res, new Ok()))
.catch((err) => new Result(null, Err.fromCatch(err))) as Promise<Result<T>>;
};

1
mod.ts Normal file
View file

@ -0,0 +1 @@
export * from "./grip.ts";

15
package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "@nesterow/follow",
"module": "grip.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"scripts": {
"bundle": "bun build ./grip.ts --outdir ./dist",
"test": "bun test"
}
}

27
tsconfig.json Normal file
View file

@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}