From 527e9b435fa1c4406a8839e9f1521bd27ee10121 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Jul 2024 10:36:11 +0700 Subject: [PATCH 01/26] Migrate contarcts implementation from FF repo --- package.json | 3 + packages/contract/CHANGELOG.md | 1 + packages/contract/README.md | 3 + packages/contract/package.json | 37 +++ packages/contract/src/contract.test.ts | 422 +++++++++++++++++++++++++ packages/contract/src/contract.ts | 144 +++++++++ packages/contract/src/index.ts | 1 + packages/contract/src/interop.test.ts | 40 +++ packages/contract/src/protocol.ts | 11 + packages/contract/tsconfig.json | 11 + packages/contract/vite.config.js | 19 ++ pnpm-lock.yaml | 31 ++ tsconfig.base.json | 3 +- 13 files changed, 725 insertions(+), 1 deletion(-) create mode 100644 packages/contract/CHANGELOG.md create mode 100644 packages/contract/README.md create mode 100644 packages/contract/package.json create mode 100644 packages/contract/src/contract.test.ts create mode 100644 packages/contract/src/contract.ts create mode 100644 packages/contract/src/index.ts create mode 100644 packages/contract/src/interop.test.ts create mode 100644 packages/contract/src/protocol.ts create mode 100644 packages/contract/tsconfig.json create mode 100644 packages/contract/vite.config.js diff --git a/package.json b/package.json index 61a81a08..b0faa18c 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "@arethetypeswrong/cli": "^0.15.3", "@changesets/cli": "^2.24.1", "@farfetched/core": "^0.8.13", + "@farfetched/runtypes": "^0.12.4", + "@farfetched/superstruct": "^0.12.4", "@playwright/test": "^1.32.2", "@reduxjs/toolkit": "^2.0.1", "@size-limit/file": "^7.0.8", @@ -27,6 +29,7 @@ "redux-saga": "^1.2.3", "runtypes": "^6.7.0", "size-limit": "^7.0.8", + "superstruct": "^2.0.2", "typescript": "5.1.6", "vite": "4.4.9", "vite-plugin-dts": "^3.8.3", diff --git a/packages/contract/CHANGELOG.md b/packages/contract/CHANGELOG.md new file mode 100644 index 00000000..eb3f7643 --- /dev/null +++ b/packages/contract/CHANGELOG.md @@ -0,0 +1 @@ +# @withease/contract diff --git a/packages/contract/README.md b/packages/contract/README.md new file mode 100644 index 00000000..5a80c5e0 --- /dev/null +++ b/packages/contract/README.md @@ -0,0 +1,3 @@ +# @withease/web-api + +Read documentation [here](https://withease.effector.dev/web-api/). diff --git a/packages/contract/package.json b/packages/contract/package.json new file mode 100644 index 00000000..03beb00a --- /dev/null +++ b/packages/contract/package.json @@ -0,0 +1,37 @@ +{ + "name": "@withease/contract", + "version": "0.0.1", + "license": "MIT", + "scripts": { + "test:run": "vitest run --typecheck", + "build": "vite build", + "size": "size-limit", + "publint": "node ../../tools/publint.mjs", + "typelint": "attw --pack" + }, + "type": "module", + "files": [ + "dist" + ], + "main": "./dist/contract.cjs", + "module": "./dist/contract.js", + "types": "./dist/contract.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/contract.d.ts", + "default": "./dist/contract.js" + }, + "require": { + "types": "./dist/contract.d.cts", + "default": "./dist/contract.cjs" + } + } + }, + "size-limit": [ + { + "path": "./dist/contract.js", + "limit": "603 B" + } + ] +} diff --git a/packages/contract/src/contract.test.ts b/packages/contract/src/contract.test.ts new file mode 100644 index 00000000..fab9a275 --- /dev/null +++ b/packages/contract/src/contract.test.ts @@ -0,0 +1,422 @@ +import { describe, it, test, expect } from 'vitest'; + +import { bool, num, str, rec, or, val, arr } from './contract'; + +describe('bool', () => { + it('valid', () => { + expect(bool.isData(true)).toBeTruthy(); + expect(bool.getErrorMessages(true)).toEqual([]); + + expect(bool.isData(false)).toBeTruthy(); + expect(bool.getErrorMessages(false)).toEqual([]); + }); + + it('invalid', () => { + expect(bool.isData(null)).toBeFalsy(); + expect(bool.getErrorMessages(null)).toMatchInlineSnapshot(` + [ + "expected boolean, got null", + ] + `); + + expect(bool.isData(undefined)).toBeFalsy(); + expect(bool.getErrorMessages(undefined)).toMatchInlineSnapshot( + ` + [ + "expected boolean, got undefined", + ] + ` + ); + + expect(bool.isData(0)).toBeFalsy(); + expect(bool.getErrorMessages(0)).toMatchInlineSnapshot(` + [ + "expected boolean, got number", + ] + `); + + expect(bool.isData(1)).toBeFalsy(); + expect(bool.getErrorMessages(1)).toMatchInlineSnapshot(` + [ + "expected boolean, got number", + ] + `); + + expect(bool.isData('')).toBeFalsy(); + expect(bool.getErrorMessages('')).toMatchInlineSnapshot(` + [ + "expected boolean, got string", + ] + `); + + expect(bool.isData('a')).toBeFalsy(); + expect(bool.getErrorMessages('a')).toMatchInlineSnapshot(` + [ + "expected boolean, got string", + ] + `); + }); +}); + +describe('num', () => { + it('valid', () => { + expect(num.isData(0)).toBeTruthy(); + expect(num.getErrorMessages(0)).toEqual([]); + + expect(num.isData(1)).toBeTruthy(); + expect(num.getErrorMessages(1)).toEqual([]); + + expect(num.isData(-1)).toBeTruthy(); + expect(num.getErrorMessages(-1)).toEqual([]); + }); + + it('invalid', () => { + expect(num.isData(null)).toBeFalsy(); + expect(num.getErrorMessages(null)).toMatchInlineSnapshot(` + [ + "expected number, got null", + ] + `); + + expect(num.isData(undefined)).toBeFalsy(); + expect(num.getErrorMessages(undefined)).toMatchInlineSnapshot( + ` + [ + "expected number, got undefined", + ] + ` + ); + + expect(num.isData('')).toBeFalsy(); + expect(num.getErrorMessages('')).toMatchInlineSnapshot(` + [ + "expected number, got string", + ] + `); + + expect(num.isData('a')).toBeFalsy(); + expect(num.getErrorMessages('a')).toMatchInlineSnapshot(` + [ + "expected number, got string", + ] + `); + + expect(num.isData(true)).toBeFalsy(); + expect(num.getErrorMessages(true)).toMatchInlineSnapshot(` + [ + "expected number, got boolean", + ] + `); + + expect(num.isData(false)).toBeFalsy(); + expect(num.getErrorMessages(false)).toMatchInlineSnapshot(` + [ + "expected number, got boolean", + ] + `); + }); +}); + +describe('str', () => { + it('valid', () => { + expect(str.isData('')).toBeTruthy(); + expect(str.getErrorMessages('')).toEqual([]); + + expect(str.isData('a')).toBeTruthy(); + expect(str.getErrorMessages('a')).toEqual([]); + + expect(str.isData('abc')).toBeTruthy(); + expect(str.getErrorMessages('abc')).toEqual([]); + }); + + it('invalid', () => { + expect(str.isData(null)).toBeFalsy(); + expect(str.getErrorMessages(null)).toMatchInlineSnapshot(` + [ + "expected string, got null", + ] + `); + + expect(str.isData(undefined)).toBeFalsy(); + expect(str.getErrorMessages(undefined)).toMatchInlineSnapshot( + ` + [ + "expected string, got undefined", + ] + ` + ); + + expect(str.isData(0)).toBeFalsy(); + expect(str.getErrorMessages(0)).toMatchInlineSnapshot(` + [ + "expected string, got number", + ] + `); + + expect(str.isData(1)).toBeFalsy(); + expect(str.getErrorMessages(1)).toMatchInlineSnapshot(` + [ + "expected string, got number", + ] + `); + + expect(str.isData(true)).toBeFalsy(); + expect(str.getErrorMessages(true)).toMatchInlineSnapshot(` + [ + "expected string, got boolean", + ] + `); + + expect(str.isData(false)).toBeFalsy(); + expect(str.getErrorMessages(false)).toMatchInlineSnapshot(` + [ + "expected string, got boolean", + ] + `); + }); + + describe('val', () => { + const cntrctA = val('a'); + const cntrct1 = val(1); + const cntrctTrue = val(true); + + it('valid', () => { + expect(cntrctA.isData('a')).toBeTruthy(); + expect(cntrctA.getErrorMessages('a')).toEqual([]); + + expect(cntrct1.isData(1)).toBeTruthy(); + expect(cntrct1.getErrorMessages(1)).toEqual([]); + + expect(cntrctTrue.isData(true)).toBeTruthy(); + expect(cntrctTrue.getErrorMessages(true)).toEqual([]); + }); + + it('invalid', () => { + expect(cntrctA.isData('b')).toBeFalsy(); + expect(cntrct1.getErrorMessages('b')).toMatchInlineSnapshot(` + [ + "expected 1, got \\"b\\"", + ] + `); + + expect(cntrct1.isData(2)).toBeFalsy(); + expect(cntrct1.getErrorMessages(2)).toMatchInlineSnapshot(` + [ + "expected 1, got 2", + ] + `); + + expect(cntrctTrue.isData(false)).toBeFalsy(); + expect(cntrctTrue.getErrorMessages(false)).toMatchInlineSnapshot(` + [ + "expected true, got false", + ] + `); + }); + }); + + describe('nullable', () => { + /* nullable is or(c, val(null)) because it is more explicit */ + const nullableBool = or(bool, val(null)); + + it('valid', () => { + expect(nullableBool.isData(true)).toBeTruthy(); + expect(nullableBool.getErrorMessages(true)).toEqual([]); + + expect(nullableBool.isData(null)).toBeTruthy(); + expect(nullableBool.getErrorMessages(null)).toEqual([]); + }); + + it('invalid', () => { + expect(nullableBool.isData(undefined)).toBeFalsy(); + expect(nullableBool.getErrorMessages(undefined)).toMatchInlineSnapshot( + ` + [ + "expected boolean, got undefined", + "expected null, got undefined", + ] + ` + ); + }); + }); + + describe('or', () => { + const cntrct = or(bool, val(0)); + it('valid', () => { + expect(cntrct.isData(0)).toBeTruthy(); + expect(cntrct.getErrorMessages(0)).toEqual([]); + + expect(cntrct.isData(true)).toBeTruthy(); + expect(cntrct.getErrorMessages(true)).toEqual([]); + + expect(cntrct.isData(false)).toBeTruthy(); + expect(cntrct.getErrorMessages(false)).toEqual([]); + }); + + it('invalid', () => { + expect(cntrct.isData(1)).toBeFalsy(); + expect(cntrct.getErrorMessages(1)).toMatchInlineSnapshot(` + [ + "expected boolean, got number", + "expected 0, got 1", + ] + `); + + expect(cntrct.isData('')).toBeFalsy(); + expect(cntrct.getErrorMessages('')).toMatchInlineSnapshot(` + [ + "expected boolean, got string", + "expected 0, got \\"\\"", + ] + `); + }); + }); + + describe('rec', () => { + it('empty object', () => { + const cntrct = rec({}); + + expect(cntrct.isData({})).toBeTruthy(); + expect(cntrct.getErrorMessages({})).toEqual([]); + + /* extra keys are allowed */ + expect(cntrct.isData({ a: 1 })).toBeTruthy(); + expect(cntrct.getErrorMessages({ a: 1 })).toEqual([]); + }); + + it('object with bool', () => { + const cntrct = rec({ enabled: bool }); + + expect(cntrct.isData({ enabled: true })).toBeTruthy(); + expect(cntrct.getErrorMessages({ enabled: true })).toEqual([]); + + expect(cntrct.isData({ enabled: false })).toBeTruthy(); + expect(cntrct.getErrorMessages({ enabled: false })).toEqual([]); + + expect(cntrct.isData({})).toBeFalsy(); + expect(cntrct.getErrorMessages({})).toMatchInlineSnapshot(` + [ + "enabled: expected boolean, got undefined", + ] + `); + + expect(cntrct.isData({ enabled: 'true' })).toBeFalsy(); + expect(cntrct.getErrorMessages({ enabled: 'true' })) + .toMatchInlineSnapshot(` + [ + "enabled: expected boolean, got string", + ] + `); + + expect(cntrct.isData(1)).toBeFalsy(); + expect(cntrct.getErrorMessages(1)).toMatchInlineSnapshot(` + [ + "expected object, got number", + ] + `); + }); + + it('optional field edge case', () => { + expect(rec({ name: or(str, val(undefined)) }).isData({})).toBeTruthy(); + }); + }); + + describe('arr', () => { + it('valid', () => { + const cntrctNum = arr(num); + + expect(cntrctNum.isData([])).toBeTruthy(); + expect(cntrctNum.getErrorMessages([])).toEqual([]); + + expect(cntrctNum.isData([1])).toBeTruthy(); + expect(cntrctNum.getErrorMessages([1])).toEqual([]); + + const cntrctBool = arr(bool); + + expect(cntrctBool.isData([])).toBeTruthy(); + expect(cntrctBool.getErrorMessages([])).toEqual([]); + + expect(cntrctBool.isData([true, false])).toBeTruthy(); + expect(cntrctBool.getErrorMessages([true, false])).toEqual([]); + }); + + it('invalid', () => { + const cntrctNum = arr(num); + + expect(cntrctNum.isData([true])).toBeFalsy(); + expect(cntrctNum.getErrorMessages([true])).toMatchInlineSnapshot(` + [ + "0: expected number, got boolean", + ] + `); + + expect(cntrctNum.isData([1, 'a'])).toBeFalsy(); + expect(cntrctNum.getErrorMessages([1, 'a'])).toMatchInlineSnapshot(` + [ + "1: expected number, got string", + ] + `); + + expect(cntrctNum.isData(true)).toBeFalsy(); + expect(cntrctNum.getErrorMessages(true)).toMatchInlineSnapshot(` + [ + "expected array, got boolean", + ] + `); + + const cntrctBool = arr(bool); + + expect(cntrctBool.isData([1])).toBeFalsy(); + expect(cntrctBool.getErrorMessages([1])).toMatchInlineSnapshot(` + [ + "0: expected boolean, got number", + ] + `); + + expect(cntrctBool.isData([true, 1])).toBeFalsy(); + expect(cntrctBool.getErrorMessages([true, 1])).toMatchInlineSnapshot( + ` + [ + "1: expected boolean, got number", + ] + ` + ); + }); + }); +}); + +describe('complex nested', () => { + test('format errors for nested objects', () => { + const cntrct = rec({ + user: rec({ name: str }), + }); + + expect(cntrct.isData({ user: { name: 'a' } })).toBeTruthy(); + expect(cntrct.getErrorMessages({ user: { name: 'a' } })).toEqual([]); + + expect(cntrct.isData({ user: { name: 1 } })).toBeFalsy(); + expect(cntrct.getErrorMessages({ user: { name: 1 } })) + .toMatchInlineSnapshot(` + [ + "user: name: expected string, got number", + ] + `); + }); + + test('supports objects in arrays', () => { + const cntrct = arr(rec({ name: str })); + + expect(cntrct.isData([])).toBeTruthy(); + expect(cntrct.getErrorMessages([])).toEqual([]); + + expect(cntrct.isData([{ name: 'a' }])).toBeTruthy(); + expect(cntrct.getErrorMessages([{ name: 'a' }])).toEqual([]); + + expect(cntrct.isData([{ name: 1 }])).toBeFalsy(); + expect(cntrct.getErrorMessages([{ name: 1 }])).toMatchInlineSnapshot(` + [ + "0: name: expected string, got number", + ] + `); + }); +}); diff --git a/packages/contract/src/contract.ts b/packages/contract/src/contract.ts new file mode 100644 index 00000000..da60f598 --- /dev/null +++ b/packages/contract/src/contract.ts @@ -0,0 +1,144 @@ +import { type Contract } from './protocol'; + +export type UnContract = T extends Contract ? U : never; + +export const bool: Contract = createSimpleContract( + (x: unknown): x is boolean => { + return typeof x === 'boolean'; + }, + 'boolean' +); + +export const num: Contract = createSimpleContract( + (x: unknown): x is number => { + return typeof x === 'number'; + }, + 'number' +); + +export const str: Contract = createSimpleContract( + (x: unknown): x is string => { + return typeof x === 'string'; + }, + 'string' +); + +export function val( + value: T +): Contract { + const check = (x: unknown): x is T => { + return x === value; + }; + + return { + isData: check, + getErrorMessages: createGetErrorMessages(check, (actual) => [ + `expected ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`, + ]), + }; +} + +export function or>>( + ...contracts: T +): Contract> { + const check = (x: unknown): x is ContarctValue => + contracts.some((c) => c.isData(x)); + + return { + isData: check, + getErrorMessages: createGetErrorMessages(check, (x) => { + return contracts.flatMap((c) => c.getErrorMessages(x)); + }), + }; +} + +export function rec>>( + c: C +): Contract }> { + const check = ( + x: unknown + ): x is { [key in keyof C]: ContarctValue } => { + if (typeof x !== 'object' || x === null) return false; + + let valid = true; + for (const [key, val] of Object.entries(c)) { + if (!val.isData((x as any)[key])) { + valid = false; + break; + } + } + + return valid; + }; + + return { + isData: check, + getErrorMessages: createGetErrorMessages(check, (x) => { + if (typeof x !== 'object' || x === null) { + return [`expected object, got ${typeOf(x)}`]; + } + const errors = [] as string[]; + + for (const [key, val] of Object.entries(c)) { + const newErrors = val.getErrorMessages((x as any)[key]); + errors.push(...newErrors.map((msg) => `${key}: ${msg}`)); + } + + return errors; + }), + }; +} + +export function arr(c: Contract): Contract { + const check = (x: unknown): x is V[] => + Array.isArray(x) && x.every((v) => c.isData(v)); + + return { + isData: check, + getErrorMessages: createGetErrorMessages(check, (x) => { + if (!Array.isArray(x)) { + return [`expected array, got ${typeOf(x)}`]; + } + + return x.flatMap((v, idx) => + c.getErrorMessages(v).map((message) => `${idx}: ${message}`) + ); + }), + }; +} + +// -- types + +export type ContarctValue> = + C extends Contract ? T : never; + +// -- utils + +function createSimpleContract( + check: (x: unknown) => x is T, + exepctedType: string +): Contract { + return { + isData: check, + getErrorMessages: createGetErrorMessages(check, (actual) => [ + `expected ${exepctedType}, got ${typeOf(actual)}`, + ]), + }; +} + +function createGetErrorMessages( + check: (v: unknown) => boolean, + fn: (v: unknown) => string[] +): (v: unknown) => string[] { + return (v) => { + if (check(v)) { + return []; + } + + return fn(v); + }; +} + +function typeOf(x: unknown): string { + return x === null ? 'null' : typeof x; +} diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts new file mode 100644 index 00000000..9d2b01fd --- /dev/null +++ b/packages/contract/src/index.ts @@ -0,0 +1 @@ +export { type UnContract, bool, str, num, rec, arr, val, or } from './contract'; diff --git a/packages/contract/src/interop.test.ts b/packages/contract/src/interop.test.ts new file mode 100644 index 00000000..b59c8a0d --- /dev/null +++ b/packages/contract/src/interop.test.ts @@ -0,0 +1,40 @@ +import { describe, it, expect } from 'vitest'; + +import { Array, String } from 'runtypes'; +import { object, string } from 'superstruct'; +import { runtypeContract } from '@farfetched/runtypes'; +import { superstructContract } from '@farfetched/superstruct'; + +import { rec, arr } from './contract'; + +describe('runtypes', () => { + it('supports Runtype inside', () => { + const cntrct = rec({ name: runtypeContract(Array(String)) }); + + expect(cntrct.isData({ name: ['foo'] })).toBe(true); + expect(cntrct.getErrorMessages({ name: ['foo'] })).toEqual([]); + + expect(cntrct.isData({ name: [1] })).toBe(false); + expect(cntrct.getErrorMessages({ name: [1] })).toMatchInlineSnapshot(` + [ + "name: 0: Expected string, but was number", + ] + `); + }); +}); + +describe('superstruct', () => { + it('supports Superstruct inside', () => { + const cntrct = arr(superstructContract(object({ name: string() }))); + + expect(cntrct.isData([{ name: 'foo' }])).toBe(true); + expect(cntrct.getErrorMessages([{ name: 'foo' }])).toEqual([]); + + expect(cntrct.isData([{ name: 1 }])).toBe(false); + expect(cntrct.getErrorMessages([{ name: 1 }])).toMatchInlineSnapshot(` + [ + "0: name: Expected a string, but received: 1", + ] + `); + }); +}); diff --git a/packages/contract/src/protocol.ts b/packages/contract/src/protocol.ts new file mode 100644 index 00000000..d0803312 --- /dev/null +++ b/packages/contract/src/protocol.ts @@ -0,0 +1,11 @@ +export type Contract = { + /** + * Checks if Raw is Data + */ + isData: (prepared: Raw) => prepared is Data; + /** + * - empty array is dedicated for valid response + * - array of string with validation errors for invalidDataError + */ + getErrorMessages: (prepared: Raw) => string[]; +}; diff --git a/packages/contract/tsconfig.json b/packages/contract/tsconfig.json new file mode 100644 index 00000000..65b1ff2c --- /dev/null +++ b/packages/contract/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "types": ["node"], + "outDir": "dist", + "rootDir": "src", + "baseUrl": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/contract/vite.config.js b/packages/contract/vite.config.js new file mode 100644 index 00000000..b44ff98a --- /dev/null +++ b/packages/contract/vite.config.js @@ -0,0 +1,19 @@ +import tsconfigPaths from 'vite-tsconfig-paths'; +import dts from '../../tools/vite/types'; + +export default { + test: { + typecheck: { + ignoreSourceErrors: true, + }, + }, + plugins: [tsconfigPaths(), dts()], + build: { + lib: { + entry: 'src/index.ts', + name: '@withease/contract', + fileName: 'contract', + formats: ['es', 'cjs'], + }, + }, +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b742f4db..c5f10ee6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,8 @@ importers: '@arethetypeswrong/cli': ^0.15.3 '@changesets/cli': ^2.24.1 '@farfetched/core': ^0.8.13 + '@farfetched/runtypes': ^0.12.4 + '@farfetched/superstruct': ^0.12.4 '@playwright/test': ^1.32.2 '@reduxjs/toolkit': ^2.0.1 '@size-limit/file': ^7.0.8 @@ -25,6 +27,7 @@ importers: runtypes: ^6.7.0 sandpack-vue3: ^3.1.7 size-limit: ^7.0.8 + superstruct: ^2.0.2 typescript: 5.1.6 vite: 4.4.9 vite-plugin-dts: ^3.8.3 @@ -40,6 +43,8 @@ importers: '@arethetypeswrong/cli': 0.15.3 '@changesets/cli': 2.24.1 '@farfetched/core': 0.8.13_effector@23.0.0 + '@farfetched/runtypes': 0.12.4_x3t3da54h2pudzi4xrass7othq + '@farfetched/superstruct': 0.12.4_geb4xrowurf3cfiqaitxl4xbju '@playwright/test': 1.32.2 '@reduxjs/toolkit': 2.0.1 '@size-limit/file': 7.0.8_size-limit@7.0.8 @@ -56,6 +61,7 @@ importers: redux-saga: 1.2.3 runtypes: 6.7.0 size-limit: 7.0.8 + superstruct: 2.0.2 typescript: 5.1.6 vite: 4.4.9_@types+node@20.12.7 vite-plugin-dts: 3.8.3_em34en7sabsx6ywz24kghffsne @@ -1288,6 +1294,26 @@ packages: effector: 23.0.0 dev: true + /@farfetched/runtypes/0.12.4_x3t3da54h2pudzi4xrass7othq: + resolution: {integrity: sha512-x7lCiDzX25aSY15TrFtUaUMwJUsClcWphEMoGuPGo0akclCFJw9x2tki/W4vXJomcg6HFjaMbv11w/F1XUMbUA==} + peerDependencies: + '@farfetched/core': 0.12.4 + runtypes: ^6.6.0 + dependencies: + '@farfetched/core': 0.8.13_effector@23.0.0 + runtypes: 6.7.0 + dev: true + + /@farfetched/superstruct/0.12.4_geb4xrowurf3cfiqaitxl4xbju: + resolution: {integrity: sha512-2yBhvSdYFCbMa8h7x7+V0eYgwKV+5QfGz54ixdj8030ng4yEzrI2P7UJgm9xTjn1PxSl2EalsyzShRJ/drDBzw==} + peerDependencies: + '@farfetched/core': 0.12.4 + superstruct: ^1.0.3 + dependencies: + '@farfetched/core': 0.8.13_effector@23.0.0 + superstruct: 2.0.2 + dev: true + /@jest/schemas/29.6.3: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4585,6 +4611,11 @@ packages: resolution: {integrity: sha512-Ca5ib8HrFn+f+0n4N4ScTIA9iTOQ7MaGS1ylHcoVqW9J7w2w8PzN6g9gKmTYgGEBH8e120+RCmhpje6jC5uGWA==} dev: false + /superstruct/2.0.2: + resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} + engines: {node: '>=14.0.0'} + dev: true + /supports-color/5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} diff --git a/tsconfig.base.json b/tsconfig.base.json index 1e70b46a..fd233ce0 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -16,7 +16,8 @@ "@withease/factories": ["packages/factories/src/index.ts"], "@withease/i18next": ["packages/i18next/src/index.ts"], "@withease/redux": ["packages/redux/src/index.ts"], - "@withease/web-api": ["packages/web-api/src/index.ts"] + "@withease/web-api": ["packages/web-api/src/index.ts"], + "@withease/contract": ["packages/contract/src/index.ts"] } }, "exclude": ["node_modules"] From d1fb8703e0c6a454370abe139a7ad6d5f195885c Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Jul 2024 12:18:56 +0700 Subject: [PATCH 02/26] Correct naming --- apps/website/docs/.vitepress/config.mjs | 4 ++++ apps/website/docs/contracts/index.md | 0 packages/contract/CHANGELOG.md | 1 - packages/contract/README.md | 3 --- packages/contract/src/index.ts | 1 - packages/contracts/CHANGELOG.md | 1 + packages/contracts/README.md | 3 +++ packages/{contract => contracts}/package.json | 18 +++++++++--------- .../src/contracts.test.ts} | 2 +- .../src/contract.ts => contracts/src/index.ts} | 0 .../src/interop.test.ts | 2 +- .../{contract => contracts}/src/protocol.ts | 0 packages/{contract => contracts}/tsconfig.json | 0 .../{contract => contracts}/vite.config.js | 4 ++-- tsconfig.base.json | 2 +- 15 files changed, 22 insertions(+), 19 deletions(-) create mode 100644 apps/website/docs/contracts/index.md delete mode 100644 packages/contract/CHANGELOG.md delete mode 100644 packages/contract/README.md delete mode 100644 packages/contract/src/index.ts create mode 100644 packages/contracts/CHANGELOG.md create mode 100644 packages/contracts/README.md rename packages/{contract => contracts}/package.json (55%) rename packages/{contract/src/contract.test.ts => contracts/src/contracts.test.ts} (99%) rename packages/{contract/src/contract.ts => contracts/src/index.ts} (100%) rename packages/{contract => contracts}/src/interop.test.ts (96%) rename packages/{contract => contracts}/src/protocol.ts (100%) rename packages/{contract => contracts}/tsconfig.json (100%) rename packages/{contract => contracts}/vite.config.js (83%) diff --git a/apps/website/docs/.vitepress/config.mjs b/apps/website/docs/.vitepress/config.mjs index be997375..b3fd5101 100644 --- a/apps/website/docs/.vitepress/config.mjs +++ b/apps/website/docs/.vitepress/config.mjs @@ -48,6 +48,7 @@ export default defineConfig({ { text: 'redux', link: '/redux/' }, { text: 'web-api', link: '/web-api/' }, { text: 'factories', link: '/factories/' }, + { text: 'contracts', link: '/contracts/' }, ], }, { text: 'Magazine', link: '/magazine/' }, @@ -114,6 +115,9 @@ export default defineConfig({ { text: 'Motivation', link: '/factories/motivation' }, { text: 'Important Caveats', link: '/factories/important_caveats' }, ]), + ...createSidebar('contracts', [ + { text: 'Get Started', link: '/contracts/' }, + ]), '/magazine/': [ { text: 'Architecture', diff --git a/apps/website/docs/contracts/index.md b/apps/website/docs/contracts/index.md new file mode 100644 index 00000000..e69de29b diff --git a/packages/contract/CHANGELOG.md b/packages/contract/CHANGELOG.md deleted file mode 100644 index eb3f7643..00000000 --- a/packages/contract/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -# @withease/contract diff --git a/packages/contract/README.md b/packages/contract/README.md deleted file mode 100644 index 5a80c5e0..00000000 --- a/packages/contract/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @withease/web-api - -Read documentation [here](https://withease.effector.dev/web-api/). diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts deleted file mode 100644 index 9d2b01fd..00000000 --- a/packages/contract/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { type UnContract, bool, str, num, rec, arr, val, or } from './contract'; diff --git a/packages/contracts/CHANGELOG.md b/packages/contracts/CHANGELOG.md new file mode 100644 index 00000000..724991ef --- /dev/null +++ b/packages/contracts/CHANGELOG.md @@ -0,0 +1 @@ +# @withease/contracts diff --git a/packages/contracts/README.md b/packages/contracts/README.md new file mode 100644 index 00000000..8bc8be06 --- /dev/null +++ b/packages/contracts/README.md @@ -0,0 +1,3 @@ +# @withease/contracts + +Read documentation [here](https://withease.effector.dev/contracts/). diff --git a/packages/contract/package.json b/packages/contracts/package.json similarity index 55% rename from packages/contract/package.json rename to packages/contracts/package.json index 03beb00a..8e2f3f1e 100644 --- a/packages/contract/package.json +++ b/packages/contracts/package.json @@ -1,5 +1,5 @@ { - "name": "@withease/contract", + "name": "@withease/contracts", "version": "0.0.1", "license": "MIT", "scripts": { @@ -13,24 +13,24 @@ "files": [ "dist" ], - "main": "./dist/contract.cjs", - "module": "./dist/contract.js", - "types": "./dist/contract.d.ts", + "main": "./dist/contracts.cjs", + "module": "./dist/contracts.js", + "types": "./dist/contracts.d.ts", "exports": { ".": { "import": { - "types": "./dist/contract.d.ts", - "default": "./dist/contract.js" + "types": "./dist/contracts.d.ts", + "default": "./dist/contracts.js" }, "require": { - "types": "./dist/contract.d.cts", - "default": "./dist/contract.cjs" + "types": "./dist/contracts.d.cts", + "default": "./dist/contracts.cjs" } } }, "size-limit": [ { - "path": "./dist/contract.js", + "path": "./dist/contracts.js", "limit": "603 B" } ] diff --git a/packages/contract/src/contract.test.ts b/packages/contracts/src/contracts.test.ts similarity index 99% rename from packages/contract/src/contract.test.ts rename to packages/contracts/src/contracts.test.ts index fab9a275..2af9e433 100644 --- a/packages/contract/src/contract.test.ts +++ b/packages/contracts/src/contracts.test.ts @@ -1,6 +1,6 @@ import { describe, it, test, expect } from 'vitest'; -import { bool, num, str, rec, or, val, arr } from './contract'; +import { bool, num, str, rec, or, val, arr } from './contracts'; describe('bool', () => { it('valid', () => { diff --git a/packages/contract/src/contract.ts b/packages/contracts/src/index.ts similarity index 100% rename from packages/contract/src/contract.ts rename to packages/contracts/src/index.ts diff --git a/packages/contract/src/interop.test.ts b/packages/contracts/src/interop.test.ts similarity index 96% rename from packages/contract/src/interop.test.ts rename to packages/contracts/src/interop.test.ts index b59c8a0d..958fc2e7 100644 --- a/packages/contract/src/interop.test.ts +++ b/packages/contracts/src/interop.test.ts @@ -5,7 +5,7 @@ import { object, string } from 'superstruct'; import { runtypeContract } from '@farfetched/runtypes'; import { superstructContract } from '@farfetched/superstruct'; -import { rec, arr } from './contract'; +import { rec, arr } from './contracts'; describe('runtypes', () => { it('supports Runtype inside', () => { diff --git a/packages/contract/src/protocol.ts b/packages/contracts/src/protocol.ts similarity index 100% rename from packages/contract/src/protocol.ts rename to packages/contracts/src/protocol.ts diff --git a/packages/contract/tsconfig.json b/packages/contracts/tsconfig.json similarity index 100% rename from packages/contract/tsconfig.json rename to packages/contracts/tsconfig.json diff --git a/packages/contract/vite.config.js b/packages/contracts/vite.config.js similarity index 83% rename from packages/contract/vite.config.js rename to packages/contracts/vite.config.js index b44ff98a..b5117dbb 100644 --- a/packages/contract/vite.config.js +++ b/packages/contracts/vite.config.js @@ -11,8 +11,8 @@ export default { build: { lib: { entry: 'src/index.ts', - name: '@withease/contract', - fileName: 'contract', + name: '@withease/contracts', + fileName: 'contracts', formats: ['es', 'cjs'], }, }, diff --git a/tsconfig.base.json b/tsconfig.base.json index fd233ce0..aefd3c0d 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -17,7 +17,7 @@ "@withease/i18next": ["packages/i18next/src/index.ts"], "@withease/redux": ["packages/redux/src/index.ts"], "@withease/web-api": ["packages/web-api/src/index.ts"], - "@withease/contract": ["packages/contract/src/index.ts"] + "@withease/contracts": ["packages/contracts/src/index.ts"] } }, "exclude": ["node_modules"] From 1d13c0da19868545e0a2f3ca408aa430fbe7c6d3 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Jul 2024 14:11:16 +0700 Subject: [PATCH 03/26] Docs --- apps/website/docs/.vitepress/config.mjs | 1 + .../docs/.vitepress/theme/LiveDemo.vue | 9 +- apps/website/docs/contracts/api.md | 4 + .../docs/contracts/array_numbers.live.vue | 32 ++++ apps/website/docs/contracts/index.md | 145 ++++++++++++++++++ apps/website/docs/contracts/size_chart.vue | 56 +++++++ apps/website/docs/contracts/sizes.data.ts | 44 ++++++ apps/website/docs/protocols/contract.md | 1 + package.json | 6 +- pnpm-lock.yaml | 39 +++++ 10 files changed, 330 insertions(+), 7 deletions(-) create mode 100644 apps/website/docs/contracts/api.md create mode 100644 apps/website/docs/contracts/array_numbers.live.vue create mode 100644 apps/website/docs/contracts/size_chart.vue create mode 100644 apps/website/docs/contracts/sizes.data.ts diff --git a/apps/website/docs/.vitepress/config.mjs b/apps/website/docs/.vitepress/config.mjs index b3fd5101..99468af6 100644 --- a/apps/website/docs/.vitepress/config.mjs +++ b/apps/website/docs/.vitepress/config.mjs @@ -117,6 +117,7 @@ export default defineConfig({ ]), ...createSidebar('contracts', [ { text: 'Get Started', link: '/contracts/' }, + { text: 'APIs', link: '/contracts/api' }, ]), '/magazine/': [ { diff --git a/apps/website/docs/.vitepress/theme/LiveDemo.vue b/apps/website/docs/.vitepress/theme/LiveDemo.vue index 84863f3b..5e730251 100644 --- a/apps/website/docs/.vitepress/theme/LiveDemo.vue +++ b/apps/website/docs/.vitepress/theme/LiveDemo.vue @@ -3,6 +3,7 @@ import { Sandpack } from 'sandpack-vue3'; import repositoryPackageJson from '../../../../../package.json'; import webApiRaw from '../../../../../packages/web-api/dist/web-api.js?raw'; +import contractsRaw from '../../../../../packages/contracts/dist/contracts.js?raw'; const repositoryVersions = { ...repositoryPackageJson.dependencies, @@ -14,6 +15,7 @@ const props = defineProps(['demoFile']); const files = { '/src/App.vue': props.demoFile, ...localPackage({ name: 'web-api', content: webApiRaw }), + ...localPackage({ name: 'contracts', content: contractsRaw }) }; const customSetup = { @@ -41,10 +43,5 @@ function localPackage({ name, content }) { diff --git a/apps/website/docs/contracts/api.md b/apps/website/docs/contracts/api.md new file mode 100644 index 00000000..691d534c --- /dev/null +++ b/apps/website/docs/contracts/api.md @@ -0,0 +1,4 @@ +# APIs + +Full list of available APIs. + diff --git a/apps/website/docs/contracts/array_numbers.live.vue b/apps/website/docs/contracts/array_numbers.live.vue new file mode 100644 index 00000000..9cfdc913 --- /dev/null +++ b/apps/website/docs/contracts/array_numbers.live.vue @@ -0,0 +1,32 @@ + + + diff --git a/apps/website/docs/contracts/index.md b/apps/website/docs/contracts/index.md index e69de29b..5c11687b 100644 --- a/apps/website/docs/contracts/index.md +++ b/apps/website/docs/contracts/index.md @@ -0,0 +1,145 @@ + + +# contracts + +Extremely small library (less than **{{maxSize}}** controlled by CI) for creating [_Contracts_](/protocols/contract) that allows you to introduce data validation on edges of the application with no performance compromises. + +## Installation + +First, you need to install package: + +::: code-group + +```sh [pnpm] +pnpm install @withease/contracts +``` + +```sh [yarn] +yarn add @withease/contracts +``` + +```sh [npm] +npm install @withease/contracts +``` + +::: + +## Creating a _Contract_ + +`@withease/contracts` exports bunch of utilities that can be used to create a _Contract_, read the full API reference [here](/contracts/api). Any of the utilities returns a _Contract_ object, that accepts something `unknown` and checks if it is something concrete defined by the used utility. + + + +## Usage of a _Contract_ + +`@withease/contracts` is designed to be compatible with Effector's ecosystem without additional interop, so most of the time you can pass created [_Contract_](/protocols/contract) to other Effector's libraries as is. + +### Farfetched + +[Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contracts). + +```ts +import { createJsonQuery } from '@farfetched/core'; +import { rec, str, arr, val, or } from '@withease/contracts'; + +const characterQuery = createJsonQuery({ + params: declareParams<{ id: number }>(), + request: { + method: 'GET', + url: ({ id }) => `https://rickandmortyapi.com/api/character/${id}`, + }, + response: { + // after receiving data from the server + // check if it is conforms the Contract to ensure + // API does not return something unexpected + contract: rec({ + id: str, + name: str, + status: Status, + species: str, + type: str, + gender: Gender, + origin: rec({ name: str, url: str }), + location: rec({ name: str, url: str }), + image: or(val('Female'), val('Male'), val('Genderless')), + episode: arr(str), + }), + }, +}); +``` + +### effector-storage + +[`effector-storage`](https://github.com/yumauri/effector-storage) is a small module for Effector to sync stores with different storages (local storage, session storage, async storage, IndexedDB, cookies, server side storage, etc). + +Since data is stored in an external storage it is important to validate it before using it in the application. + +```ts +import { createStore } from 'effector'; +import { persist } from 'effector-storage'; +import { num } from '@withease/contracts'; + +const $counter = createStore(0); + +persist({ + store: $counter, + key: 'counter', + // after reading value from a storage check if a value is number + // to avoid pushing invalid data to the Store + contract: num, +}); +``` + +## Integration with other libraries + +Since `@withease/contracts` is compatible [_Contract_](/protocols/contract) protocol it can be used with any library that supports it. + +For instance, you can define a part of a [_Contract_](/protocols/contract) with [Zod](https://zod.dev/) and combine it with `@withease/contracts`: + +```ts +import { z } from 'zod'; +import { arr, rec } from '@withease/contracts'; +import { zodContract } from '@farfetched/zod'; + +const User = z.object({ + name: z.string(), +}); + +const MyContract = arr( + rec({ + // 👇 easily integrate Zod via compatibility layer + users: zodContract(User), + }) +); +``` + +The full list of libraries that support _Contract_ protocol can be found [here](/protocols/contract). + +## Differences from other libraries + +
+It is extremely small and we mean it 👇 + + + +::: tip +Data fetched directly from https://esm.run/ and updates on every commit. +::: + +
+
+It is significantly smaller than other libraries for creating _Contracts_. +
diff --git a/apps/website/docs/contracts/size_chart.vue b/apps/website/docs/contracts/size_chart.vue new file mode 100644 index 00000000..eb8cafd2 --- /dev/null +++ b/apps/website/docs/contracts/size_chart.vue @@ -0,0 +1,56 @@ + + + diff --git a/apps/website/docs/contracts/sizes.data.ts b/apps/website/docs/contracts/sizes.data.ts new file mode 100644 index 00000000..c72630ec --- /dev/null +++ b/apps/website/docs/contracts/sizes.data.ts @@ -0,0 +1,44 @@ +export default { + async load() { + try { + const [ + zodSize, + runtypesSize, + ioTsSize, + fpTsSize, + superstructSize, + typedContractsSize, + valibotSize, + ] = await Promise.all([ + definePackageSize('zod', 'lib/index.js'), + definePackageSize('runtypes', 'lib/index.js'), + definePackageSize('io-ts', 'lib/index.js'), + definePackageSize('fp-ts', 'lib/index.js'), + definePackageSize('superstruct', 'dist/index.mjs'), + definePackageSize('typed-contracts', 'lib/bundle.js'), + definePackageSize('valibot', './dist/index.js'), + ]); + + return [ + { name: 'Zod', size: zodSize }, + { name: 'runtypes', size: runtypesSize }, + { name: 'io-ts + fp-ts', size: ioTsSize + fpTsSize }, + { name: 'superstruct', size: superstructSize }, + { name: 'typed-contracts', size: typedContractsSize }, + { name: 'valibot', size: valibotSize }, + ]; + } catch (error) { + return null; + } + }, +}; + +async function definePackageSize(packageName, moduleName) { + const response = await fetch( + `https://esm.run/${packageName}@latest/${moduleName}` + ); + + const encodedSize = Number(response.headers.get('content-length') ?? 0); + + return encodedSize; +} diff --git a/apps/website/docs/protocols/contract.md b/apps/website/docs/protocols/contract.md index 1ea14101..3a96610f 100644 --- a/apps/website/docs/protocols/contract.md +++ b/apps/website/docs/protocols/contract.md @@ -11,6 +11,7 @@ A rule to statically validate received data. Any object following the strict API ::: tip Packages that provide integration for creating _Contract_ +- [`@withease/contracts`](/contracts/) - [`@farfetched/runtypes`](https://farfetched.pages.dev/api/contracts/runtypes.html) - [`@farfetched/zod`](https://farfetched.pages.dev/api/contracts/zod.html) - [`@farfetched/io-ts`](https://farfetched.pages.dev/api/contracts/io-ts.html) diff --git a/package.json b/package.json index b0faa18c..22ba2436 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@reduxjs/toolkit": "^2.0.1", "@size-limit/file": "^7.0.8", "@types/node": "^20.12.7", + "chart.js": "^4.4.3", "effector": "23.0.0", "effector-vue": "23.0.0", "glob": "^8.0.3", @@ -37,10 +38,13 @@ "vitepress": "1.2.3", "vitepress-plugin-rss": "^0.2.8", "vitest": "~1.5.0", - "vue": "3.4.27" + "vue": "3.4.27", + "vue-chartjs": "^5.3.1" }, "dependencies": { "@algolia/client-search": "^4.14.3", + "bytes": "^3.1.2", + "pretty-bytes": "^6.1.1", "sandpack-vue3": "^3.1.7" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5f10ee6..82cd8356 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,8 @@ importers: '@reduxjs/toolkit': ^2.0.1 '@size-limit/file': ^7.0.8 '@types/node': ^20.12.7 + bytes: ^3.1.2 + chart.js: ^4.4.3 effector: 23.0.0 effector-vue: 23.0.0 glob: ^8.0.3 @@ -21,6 +23,7 @@ importers: markdown: ^0.5.0 playwright: ^1.32.2 prettier: ^2.6.2 + pretty-bytes: ^6.1.1 publint: ^0.2.7 redux: ^5.0.0 redux-saga: ^1.2.3 @@ -36,8 +39,11 @@ importers: vitepress-plugin-rss: ^0.2.8 vitest: ~1.5.0 vue: 3.4.27 + vue-chartjs: ^5.3.1 dependencies: '@algolia/client-search': 4.14.3 + bytes: 3.1.2 + pretty-bytes: 6.1.1 sandpack-vue3: 3.1.7_vue@3.4.27 devDependencies: '@arethetypeswrong/cli': 0.15.3 @@ -49,6 +55,7 @@ importers: '@reduxjs/toolkit': 2.0.1 '@size-limit/file': 7.0.8_size-limit@7.0.8 '@types/node': 20.12.7 + chart.js: 4.4.3 effector: 23.0.0 effector-vue: 23.0.0_effector@23.0.0+vue@3.4.27 glob: 8.1.0 @@ -70,6 +77,7 @@ importers: vitepress-plugin-rss: 0.2.8_vitepress@1.2.3 vitest: 1.5.0_@types+node@20.12.7 vue: 3.4.27_typescript@5.1.6 + vue-chartjs: 5.3.1_chart.js@4.4.3+vue@3.4.27 apps/web-api-demo: specifiers: @@ -1324,6 +1332,10 @@ packages: /@jridgewell/sourcemap-codec/1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@kurkle/color/0.3.2: + resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} + dev: true + /@lezer/common/1.0.4: resolution: {integrity: sha512-lZHlk8p67x4aIDtJl6UQrXSOP6oi7dQR3W/geFVrENdA1JDaAJWldnVqVjPMJupbTKbzDfFcePfKttqVidS/dg==} dev: false @@ -2358,6 +2370,11 @@ packages: engines: {node: '>= 0.8'} dev: true + /bytes/3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + /cac/6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2436,6 +2453,13 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /chart.js/4.4.3: + resolution: {integrity: sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==} + engines: {pnpm: '>=8'} + dependencies: + '@kurkle/color': 0.3.2 + dev: true + /check-error/1.0.3: resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: @@ -4045,6 +4069,11 @@ packages: hasBin: true dev: true + /pretty-bytes/6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + dev: false + /pretty-format/29.7.0: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -5126,6 +5155,16 @@ packages: - terser dev: true + /vue-chartjs/5.3.1_chart.js@4.4.3+vue@3.4.27: + resolution: {integrity: sha512-rZjqcHBxKiHrBl0CIvcOlVEBwRhpWAVf6rDU3vUfa7HuSRmGtCslc0Oc8m16oAVuk0erzc1FCtH1VCriHsrz+A==} + peerDependencies: + chart.js: ^4.1.1 + vue: ^3.0.0-0 || ^2.7.0 + dependencies: + chart.js: 4.4.3 + vue: 3.4.27_typescript@5.1.6 + dev: true + /vue-demi/0.14.8_vue@3.4.27: resolution: {integrity: sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==} engines: {node: '>=12'} From ef9d855370abcbd977832bd09cfcb287b9fc24b0 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Jul 2024 14:11:48 +0700 Subject: [PATCH 04/26] Prettier --- .../docs/.vitepress/theme/LiveDemo.vue | 9 ++- .../docs/contracts/array_numbers.live.vue | 46 ++++++------ apps/website/docs/contracts/size_chart.vue | 71 ++++++++++--------- 3 files changed, 67 insertions(+), 59 deletions(-) diff --git a/apps/website/docs/.vitepress/theme/LiveDemo.vue b/apps/website/docs/.vitepress/theme/LiveDemo.vue index 5e730251..6d1adf18 100644 --- a/apps/website/docs/.vitepress/theme/LiveDemo.vue +++ b/apps/website/docs/.vitepress/theme/LiveDemo.vue @@ -15,7 +15,7 @@ const props = defineProps(['demoFile']); const files = { '/src/App.vue': props.demoFile, ...localPackage({ name: 'web-api', content: webApiRaw }), - ...localPackage({ name: 'contracts', content: contractsRaw }) + ...localPackage({ name: 'contracts', content: contractsRaw }), }; const customSetup = { @@ -43,5 +43,10 @@ function localPackage({ name, content }) { diff --git a/apps/website/docs/contracts/array_numbers.live.vue b/apps/website/docs/contracts/array_numbers.live.vue index 9cfdc913..a5094e16 100644 --- a/apps/website/docs/contracts/array_numbers.live.vue +++ b/apps/website/docs/contracts/array_numbers.live.vue @@ -5,28 +5,28 @@ const contract = arr(num); diff --git a/apps/website/docs/contracts/size_chart.vue b/apps/website/docs/contracts/size_chart.vue index eb8cafd2..e0c54dd7 100644 --- a/apps/website/docs/contracts/size_chart.vue +++ b/apps/website/docs/contracts/size_chart.vue @@ -2,55 +2,58 @@ import { computed, defineProps } from 'vue'; import { Bar } from 'vue-chartjs'; import { - Chart as ChartJS, - Title, - Tooltip, - BarElement, - CategoryScale, - LinearScale, + Chart as ChartJS, + Title, + Tooltip, + BarElement, + CategoryScale, + LinearScale, } from 'chart.js'; import prettyBytes from 'pretty-bytes'; ChartJS.register(Title, Tooltip, BarElement, CategoryScale, LinearScale); const props = defineProps({ - sizes: { - type: Array, - required: true, - }, + sizes: { + type: Array, + required: true, + }, }); const chartData = computed(() => { - const sortedSizes = props.sizes.toSorted((a, b) => a.size - b.size); - return { - labels: sortedSizes.map((item) => item.name), - datasets: [ - { data: sortedSizes.map((item) => item.size), backgroundColor: '#3451b2' }, - ], - } + const sortedSizes = props.sizes.toSorted((a, b) => a.size - b.size); + return { + labels: sortedSizes.map((item) => item.name), + datasets: [ + { + data: sortedSizes.map((item) => item.size), + backgroundColor: '#3451b2', + }, + ], + }; }); const chartOptions = { - responsive: true, - scales: { - y: { - beginAtZero: true, - ticks: { - callback: (value) => - typeof value === 'number' ? prettyBytes(value) : value, - }, - }, + responsive: true, + scales: { + y: { + beginAtZero: true, + ticks: { + callback: (value) => + typeof value === 'number' ? prettyBytes(value) : value, + }, }, - plugins: { - tooltip: { - callbacks: { - label: (item) => prettyBytes(item.parsed.y), - title: ([item]) => item?.label, - }, - }, + }, + plugins: { + tooltip: { + callbacks: { + label: (item) => prettyBytes(item.parsed.y), + title: ([item]) => item?.label, + }, }, + }, }; From b5e9b05e27e84f594ff57441c54fef53bde83d84 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Jul 2024 15:34:57 +0700 Subject: [PATCH 05/26] Fix typo --- apps/website/docs/contracts/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/website/docs/contracts/index.md b/apps/website/docs/contracts/index.md index 5c11687b..bb0086b3 100644 --- a/apps/website/docs/contracts/index.md +++ b/apps/website/docs/contracts/index.md @@ -49,7 +49,7 @@ npm install @withease/contracts ### Farfetched -[Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contracts). +[Farfetched](https://ff.effector.dev) is the advanced data fetching tool for web applications based of Effector. It suggests to ensure that data received from the server is conforms desired [_Contract_](/protocols/contract). ```ts import { createJsonQuery } from '@farfetched/core'; From aa9d2008e691f68e5df5d4765753edb1a6dec2df Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Mon, 29 Jul 2024 15:37:37 +0700 Subject: [PATCH 06/26] Do not use future --- apps/website/docs/contracts/size_chart.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/website/docs/contracts/size_chart.vue b/apps/website/docs/contracts/size_chart.vue index e0c54dd7..5e3b1d26 100644 --- a/apps/website/docs/contracts/size_chart.vue +++ b/apps/website/docs/contracts/size_chart.vue @@ -20,7 +20,9 @@ const props = defineProps({ }, }); const chartData = computed(() => { - const sortedSizes = props.sizes.toSorted((a, b) => a.size - b.size); + const sortedSizes = [...props.sizes] + sortedSizes.sort((a, b) => a.size - b.size); + return { labels: sortedSizes.map((item) => item.name), datasets: [ From fd58e761cda17018ab8645f312682f9c25b8e187 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Tue, 30 Jul 2024 06:28:58 +0700 Subject: [PATCH 07/26] More docs --- apps/website/docs/contracts/index.md | 32 ++++++++++++++++++++++++++++ apps/website/docs/index.md | 5 +++++ 2 files changed, 37 insertions(+) diff --git a/apps/website/docs/contracts/index.md b/apps/website/docs/contracts/index.md index bb0086b3..d63752af 100644 --- a/apps/website/docs/contracts/index.md +++ b/apps/website/docs/contracts/index.md @@ -43,6 +43,23 @@ npm install @withease/contracts +## Extracting types from a _Contract_ + +`@withease/contracts` provides a special type `UnContract` that can be used to extract a type from a _Contract_. + +```ts +import { type UnContract, rec, str, num } from '@withease/contracts'; + +const UserContract = rec({ + id: num, + name: str, + email: str, +}); + +// type User = { id: number, name: string, email: string } +type User = UnContract; +``` + ## Usage of a _Contract_ `@withease/contracts` is designed to be compatible with Effector's ecosystem without additional interop, so most of the time you can pass created [_Contract_](/protocols/contract) to other Effector's libraries as is. @@ -133,6 +150,9 @@ The full list of libraries that support _Contract_ protocol can be found [here](
It is extremely small and we mean it 👇 +
+
+ ::: tip @@ -143,3 +163,15 @@ Data fetched directly from https://esm.run/ and updates on every commit.
It is significantly smaller than other libraries for creating _Contracts_.
+ +Of course smaller size is comes with some trade-offs, but we believe that in most cases it is worth it. `@withease/contracts` covers most of the common cases but does not try to be a silver bullet for all possible cases. It does not aim to have the following features from other libraries: + +- Branded types ([like in Runtypes](https://github.com/runtypes/runtypes?tab=readme-ov-file#branded-types)) +- Advanced string-validators ([like IP-validation in Zod](https://zod.dev/?id=ip-addresses)) +- Promise schemas ([like in Zod](https://zod.dev/?id=promise)) +- Error i18n ([like in Valibot](https://valibot.dev/guides/internationalization/)) +- ...and many other features that are not needed in _most_ of the cases + +::: tip Q: What if I started a project with `@withease/contracts` and then realized that I need some of the features that are not covered by it? +A: No worries! You can easily integrate `@withease/contracts` with other libraries that have the features you need. Check out the [Integration with other libraries](#integration-with-other-libraries) section for more details. +::: diff --git a/apps/website/docs/index.md b/apps/website/docs/index.md index af688b40..670c7de5 100644 --- a/apps/website/docs/index.md +++ b/apps/website/docs/index.md @@ -32,6 +32,11 @@ features: details: Web API bindings — network status, tab visibility, and more link: /web-api/ linkText: Get Started + - icon: 📄 + title: contracts + details: Extremely small library to validate data from external sources + link: /contracts/ + linkText: Get Started - icon: 👩‍🏭 title: factories details: Set of helpers to create factories in your application From 9de370b19820f23173aff9f9bdd28892dfcfae33 Mon Sep 17 00:00:00 2001 From: Igor Kamyshev Date: Tue, 30 Jul 2024 11:18:48 +0700 Subject: [PATCH 08/26] Auto generate APIs page --- apps/website/.gitignore | 3 +- apps/website/docs/contracts/api.md | 4 - apps/website/docs/contracts/size_chart.vue | 2 +- apps/website/package.json | 2 +- .../cli.mjs => scripts/changelog.mjs} | 0 apps/website/scripts/jsdoc.mjs | 112 ++++++++++++++++++ package.json | 3 + packages/contracts/src/contracts.test.ts | 2 +- packages/contracts/src/index.ts | 39 ++++-- packages/factories/src/invoke.ts | 1 + pnpm-lock.yaml | 21 +++- 11 files changed, 165 insertions(+), 24 deletions(-) delete mode 100644 apps/website/docs/contracts/api.md rename apps/website/{changelog/cli.mjs => scripts/changelog.mjs} (100%) create mode 100644 apps/website/scripts/jsdoc.mjs diff --git a/apps/website/.gitignore b/apps/website/.gitignore index 81d4b609..57854de4 100644 --- a/apps/website/.gitignore +++ b/apps/website/.gitignore @@ -1,2 +1,3 @@ docs/.vitepress/cache -CHANGELOG.md \ No newline at end of file +CHANGELOG.md +api.md \ No newline at end of file diff --git a/apps/website/docs/contracts/api.md b/apps/website/docs/contracts/api.md deleted file mode 100644 index 691d534c..00000000 --- a/apps/website/docs/contracts/api.md +++ /dev/null @@ -1,4 +0,0 @@ -# APIs - -Full list of available APIs. - diff --git a/apps/website/docs/contracts/size_chart.vue b/apps/website/docs/contracts/size_chart.vue index 5e3b1d26..9b8d78ca 100644 --- a/apps/website/docs/contracts/size_chart.vue +++ b/apps/website/docs/contracts/size_chart.vue @@ -1,5 +1,5 @@