init
This commit is contained in:
commit
849b917d50
175
.gitignore
vendored
Normal file
175
.gitignore
vendored
Normal 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
70
README.md
Normal 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
|
65
grip.test.ts
Normal file
65
grip.test.ts
Normal 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
107
grip.ts
Normal 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>>;
|
||||||
|
};
|
15
package.json
Normal file
15
package.json
Normal 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
27
tsconfig.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue