diff --git a/README.md b/README.md index fa5d629..0493c57 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ function fallible(): Result { const res = fallible(); -// Using isOk helper will do this for You, but You can also +// Using isOk helper will do this for you, but you can also // access the `__kind` field and compare it with `ResultKind` enum directly if (isOk(res)) { // Typescript infers res.data's type as `number` @@ -65,7 +65,7 @@ const res: Result = fallible(); // Call `equip` with the Result of fallible function const equipped: ResultEquipped = equip(res); -// Use as You would Rust's Result +// Use as you would Rust's Result const squared: number = equipped.map(n => n * n).expect('Squared n'); // Using unwrap can cause a panic: `panicked at 'Squared n: ""'` diff --git a/package-lock.json b/package-lock.json index 862549e..8309a60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@types/jest": "^26.0.24", "clean-terminal-webpack-plugin": "^3.0.0", "jest": "^27.0.6", + "jest-fetch-mock": "^3.0.3", "terser-webpack-plugin": "^5.1.4", "ts-jest": "^27.0.4", "ts-loader": "^9.2.4", @@ -1779,6 +1780,15 @@ "safe-buffer": "~5.1.1" } }, + "node_modules/cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2976,6 +2986,16 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "27.0.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", @@ -3940,6 +3960,15 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true, + "engines": { + "node": "4.x || >=6.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4219,6 +4248,12 @@ "node": ">=0.4.0" } }, + "node_modules/promise-polyfill": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz", + "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==", + "dev": true + }, "node_modules/prompts": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", @@ -6954,6 +6989,15 @@ "safe-buffer": "~5.1.1" } }, + "cross-fetch": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.4.tgz", + "integrity": "sha512-1eAtFWdIubi6T4XPy6ei9iUFoKpUkIF971QLN8lIvvvwueI65+Nw5haMNKUwfJxabqlIIDODJKGrQ66gxC0PbQ==", + "dev": true, + "requires": { + "node-fetch": "2.6.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7843,6 +7887,16 @@ "jest-util": "^27.0.6" } }, + "jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "requires": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "jest-get-type": { "version": "27.0.6", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz", @@ -8591,6 +8645,12 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node-fetch": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", + "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "dev": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -8803,6 +8863,12 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-polyfill": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.2.0.tgz", + "integrity": "sha512-k/TC0mIcPVF6yHhUvwAp7cvL6I2fFV7TzF1DuGPI8mBh4QQazf36xCKEHKTZKRysEoTQoQdKyP25J8MPJp7j5g==", + "dev": true + }, "prompts": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", diff --git a/package.json b/package.json index fe2a18f..6a3e631 100644 --- a/package.json +++ b/package.json @@ -27,14 +27,15 @@ "@types/jest": "^26.0.24", "clean-terminal-webpack-plugin": "^3.0.0", "jest": "^27.0.6", + "jest-fetch-mock": "^3.0.3", "terser-webpack-plugin": "^5.1.4", + "ts-jest": "^27.0.4", "ts-loader": "^9.2.4", + "tslint": "^6.1.3", "typedoc": "^0.21.4", "typescript": "^4.3.5", "webpack": "^5.47.1", - "webpack-cli": "^4.7.2", - "ts-jest": "^27.0.4", - "tslint": "^6.1.3" + "webpack-cli": "^4.7.2" }, "files": [ "dist/**/*" diff --git a/src/index.ts b/src/index.ts index 49b8616..9a649f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,4 +13,7 @@ export * from './option/consts'; export * from './option/equipped'; // Wrappers for common js functions -export * from './js_wrappers'; +export * from './js_wrappers/types'; +export * from './js_wrappers/fetch'; +export * from './js_wrappers/helpers'; +export * from './js_wrappers/parse_json'; diff --git a/src/js_wrappers.spec.ts b/src/js_wrappers.spec.ts deleted file mode 100644 index 9672153..0000000 --- a/src/js_wrappers.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Result, ResultKind } from './result/types'; - -import { Ok, Err } from './result/helpers'; -import { parseJson, catchResult } from './js_wrappers'; - - -describe('js wrappers', () => { - test('catchResult', () => { - function throwsError(): void { - throw new Error('1234'); - } - - expect(throwsError).toThrowError(); - - function doesNotThrowError(): number { - return 5; - } - - expect(doesNotThrowError).not.toThrowError(); - - expect(catchResult(throwsError)).toEqual(Err('Error: 1234')); - expect(catchResult(doesNotThrowError)).toEqual(Ok(5)); - }); - - test('parseJson', () => { - const j1: string = '{'; - const res1: Result = parseJson(j1); - expect(res1.__kind).toEqual(ResultKind.Err); - expect(res1.data).toEqual('SyntaxError: Unexpected end of JSON input'); - - const j2: string = '{ "test": 2 }'; - const res2: Result = parseJson(j2); - expect(res2.__kind).toEqual(ResultKind.Ok); - expect(res2.data).toEqual({ test: 2 }); - }); -}); diff --git a/src/js_wrappers.ts b/src/js_wrappers.ts deleted file mode 100644 index 917977e..0000000 --- a/src/js_wrappers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Result } from './result/types'; -import { Ok, Err } from './result/helpers'; - - -/** - * Turns any fallible function's output (T) info Result. - */ -export function catchResult(f: () => T): Result { - try { - return Ok(f()); - } catch(e) { - return Err(String(e)); - } -} - -/** - * Parses json, returning Result. - */ -export function parseJson(json: string): Result { - return catchResult(() => JSON.parse(json)); -} diff --git a/src/js_wrappers/fetch.ts b/src/js_wrappers/fetch.ts new file mode 100644 index 0000000..792e82d --- /dev/null +++ b/src/js_wrappers/fetch.ts @@ -0,0 +1,40 @@ +import { equip } from '../helpers'; +import { Ok, Err } from '../result/helpers'; +import { catchResult, catchAsyncResult } from './helpers'; + +import { Result, ResultKind } from '../result/types'; +import { SafeResponse, SafeFetchError } from './types'; + + +export async function safeFetch( + input: RequestInfo, + init?: RequestInit, +): Promise> { + // Catching async error + const caught = await catchAsyncResult(() => fetch(input, init)); + + // Mapping the result from Response into SafeResponse + return equip(caught).map((res: Response) => { + return { + // Copies + body: res.body, + bodyUsed: res.bodyUsed, + headers: res.headers, + ok: res.ok, + redirected: res.redirected, + status: res.status, + statusText: res.statusText, + type: res.type, + url: res.url, + clone: res.clone, + + // Safetied + trailer: res.trailer, + arrayBuffer: res.arrayBuffer, + blob: res.blob, + formData: res.formData, + json: res.json, + text: res.text, + } as any as SafeResponse; + }).inner; +} diff --git a/src/js_wrappers/helpers.ts b/src/js_wrappers/helpers.ts new file mode 100644 index 0000000..32f11de --- /dev/null +++ b/src/js_wrappers/helpers.ts @@ -0,0 +1,29 @@ +import { Ok, Err } from '../result/helpers'; + +import { Result } from '../result/types'; + + +/** + * Turns any fallible function's output (T) into Result. + */ +export function catchResult(f: () => T): Result { + try { + return Ok(f()); + } catch(e) { + return Err(String(e)); + } +} + +/** + * Turns any async fallible function's output (T) into + * Promise>. + */ +export async function catchAsyncResult( + f: () => Promise, +): Promise> { + try { + return Ok(await f()); + } catch(e) { + return Err(String(e)); + } +} diff --git a/src/js_wrappers/js_wrappers.spec.ts b/src/js_wrappers/js_wrappers.spec.ts new file mode 100644 index 0000000..a99bde0 --- /dev/null +++ b/src/js_wrappers/js_wrappers.spec.ts @@ -0,0 +1,92 @@ +import { SafeResponse } from './types'; +import { Result, ResultKind } from '../result/types'; + +import { Ok, Err } from '../result/helpers'; + +import { safeFetch } from './fetch'; +import { parseJson } from './parse_json'; +import { catchResult, catchAsyncResult } from './helpers'; + +import { enableFetchMocks } from 'jest-fetch-mock'; +enableFetchMocks(); + + +describe('js wrappers > helpers', () => { + test('catchResult', () => { + function throwsError(): void { + throw new Error('1234'); + } + expect(throwsError).toThrowError(); + + function doesNotThrowError(): number { + return 5; + } + expect(doesNotThrowError).not.toThrowError(); + + expect(catchResult(throwsError)).toEqual(Err('Error: 1234')); + expect(catchResult(doesNotThrowError)).toEqual(Ok(5)); + }); + + test('catchAsyncResult', async () => { + async function throwsError(): Promise { + throw new Error('1234'); + } + expect(throwsError()).rejects.toEqual(new Error('1234')); + + async function doesNotThrowError(): Promise { + return 5; + } + expect(doesNotThrowError()).resolves.toEqual(5); + + expect(catchAsyncResult(throwsError)) + .resolves + .toEqual(Err('Error: 1234')); + expect(catchAsyncResult(doesNotThrowError)) + .resolves + .toEqual(Ok(5)); + }); +}); + +describe('js wrappers > parse_json', () => { + test('parseJson', () => { + const j1: string = '{'; + const res1: Result = parseJson(j1); + expect(res1.__kind).toEqual(ResultKind.Err); + expect(res1.data).toEqual('SyntaxError: Unexpected end of JSON input'); + + const j2: string = '{ "test": 2 }'; + const res2: Result = parseJson(j2); + expect(res2.__kind).toEqual(ResultKind.Ok); + expect(res2.data).toEqual({ test: 2 }); + }); +}); + +describe('js wrappers > fetch', () => { + it('should return an error on invalid url call', async () => { + // Using jest-fetch-mock + (fetch as any).mockAbortOnce() + + const fetched: Result = await safeFetch('asdf'); + + expect(fetched) + .toEqual(Err('ReferenceError: DOMException is not defined')); + }); + + it('should return valid json', async () => { + // Using jest-fetch-mock + (fetch as any).mockResponseOnce(JSON.stringify({ data: '2137' })); + + const fetched: Result = await safeFetch('asdf'); + + expect(fetched.__kind).toEqual(ResultKind.Ok); + + const json: Result<{ data: string }, string> + = await (fetched.data as SafeResponse).json(); + + console.log({json}); + // const fetched: Result = await safeFetch('asdf'); + // + // expect(fetched) + // .toEqual(Err('ReferenceError: DOMException is not defined')); + }); +}); diff --git a/src/js_wrappers/parse_json.ts b/src/js_wrappers/parse_json.ts new file mode 100644 index 0000000..5d1be68 --- /dev/null +++ b/src/js_wrappers/parse_json.ts @@ -0,0 +1,12 @@ +import { catchResult } from './helpers'; +import { Ok, Err } from '../result/helpers'; + +import { Result } from '../result/types'; + + +/** + * Parses json, returning Result. + */ +export function parseJson(json: string): Result { + return catchResult(() => JSON.parse(json)); +} diff --git a/src/js_wrappers/types.ts b/src/js_wrappers/types.ts new file mode 100644 index 0000000..18fe70d --- /dev/null +++ b/src/js_wrappers/types.ts @@ -0,0 +1,22 @@ +import { Result } from '../result/types'; +import { Option } from '../option/types'; + + +export interface SafeResponse + extends Omit< + Response, + 'trailer' | 'arrayBuffer' | 'blob' | 'formData' | 'json' | 'text' + > +{ + readonly trailer: Promise>; + arrayBuffer(): Promise>; + blob(): Promise>; + formData(): Promise>; + json(): Promise>; + text(): Promise>; +} + +export interface SafeFetchError { + response: Option; + message: string; +}