diff --git a/apps/website/scripts/jsdoc.mjs b/apps/website/scripts/jsdoc.mjs index 769c0874..f9a5bbdf 100644 --- a/apps/website/scripts/jsdoc.mjs +++ b/apps/website/scripts/jsdoc.mjs @@ -47,6 +47,10 @@ await Promise.all( name = node.declaration.id.name; kind = 'function'; break; + case 'TSDeclareFunction': + name = node.declaration.id.name; + kind = 'function'; + break; case 'VariableDeclaration': name = node.declaration.declarations[0].id.name; kind = 'variable'; diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 8e2f3f1e..2cf61ba7 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -4,6 +4,7 @@ "license": "MIT", "scripts": { "test:run": "vitest run --typecheck", + "test:watch": "vitest --typecheck", "build": "vite build", "size": "size-limit", "publint": "node ../../tools/publint.mjs", @@ -31,7 +32,7 @@ "size-limit": [ { "path": "./dist/contracts.js", - "limit": "603 B" + "limit": "687 B" } ] } diff --git a/packages/contracts/src/contracts.test.ts b/packages/contracts/src/contracts.test.ts index 57606099..a89c5a7e 100644 --- a/packages/contracts/src/contracts.test.ts +++ b/packages/contracts/src/contracts.test.ts @@ -195,7 +195,7 @@ describe('str', () => { expect(cntrctA.isData('b')).toBeFalsy(); expect(cntrct1.getErrorMessages('b')).toMatchInlineSnapshot(` [ - "expected 1, got \\"b\\"", + "expected 1, got "b"", ] `); @@ -266,13 +266,13 @@ describe('str', () => { expect(cntrct.getErrorMessages('')).toMatchInlineSnapshot(` [ "expected boolean, got string", - "expected 0, got \\"\\"", + "expected 0, got """, ] `); }); }); - describe('rec', () => { + describe('rec, overload with fields', () => { it('empty object', () => { const cntrct = rec({}); @@ -321,6 +321,29 @@ describe('str', () => { }); }); + describe('rec, overload with types', () => { + it('empty object', () => { + const cntrct = rec(str, num); + + expect(cntrct.isData({})).toBeTruthy(); + expect(cntrct.getErrorMessages({})).toEqual([]); + }); + + it('invalid field type', () => { + const cntrct = rec(str, str); + + expect(cntrct.isData({ a: 'a' })).toBeTruthy(); + expect(cntrct.getErrorMessages({ a: 'a' })).toEqual([]); + + expect(cntrct.isData({ a: 1, b: 'b' })).toBeFalsy(); + expect(cntrct.getErrorMessages({ a: 1 })).toMatchInlineSnapshot(` + [ + "a: expected string, got number", + ] + `); + }); + }); + describe('arr', () => { it('valid', () => { const cntrctNum = arr(num); diff --git a/packages/contracts/src/index.ts b/packages/contracts/src/index.ts index 0eeb143d..5ca29903 100644 --- a/packages/contracts/src/index.ts +++ b/packages/contracts/src/index.ts @@ -123,6 +123,20 @@ export function or>>( }; } +/** + * Function that creates a _Contract_ that checks if a value is object and every property is conform to the given _Contract_. + * + * @example + * const Ids = rec(str, num); + * + * Ids.isData({ id1: 1, id2: 2 }) === true; + * Ids.isData({ id1: 1, id2: '2' }) === false; + */ +export function rec( + keys: typeof str, + values: Contract +): Contract>; + /** * Function that creates a _Contract_ that checks if a value is conform to an object with the given _Contracts_ as properties. * @@ -137,15 +151,27 @@ export function or>>( */ export function rec>>( c: C -): Contract }> { - const check = (x: unknown): x is { [key in keyof C]: UnContract } => { +): Contract }>; + +export function rec(shape: any, fieldContract?: any): any { + const check = (x: unknown) => { 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; + if (shape === str) { + for (const val of Object.values(x)) { + if (fieldContract.isData(val) === false) { + valid = false; + break; + } + } + } else { + for (const [key, val] of Object.entries(shape)) { + // @ts-expect-error + if (!val.isData((x as any)[key])) { + valid = false; + break; + } } } @@ -160,9 +186,18 @@ export function rec>>( } 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}`)); + if (shape === str) { + for (const [key, val] of Object.entries(x)) { + if (fieldContract.isData(val) === false) { + errors.push(`${key}: ${fieldContract.getErrorMessages(val)}`); + } + } + } else { + for (const [key, val] of Object.entries(shape)) { + // @ts-expect-error + const newErrors = val.getErrorMessages((x as any)[key]); + errors.push(...newErrors.map((msg: string) => `${key}: ${msg}`)); + } } return errors; diff --git a/packages/contracts/src/interop.test.ts b/packages/contracts/src/interop.test.ts index 958fc2e7..46a7759e 100644 --- a/packages/contracts/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 './contracts'; +import { rec, arr } from './index'; describe('runtypes', () => { it('supports Runtype inside', () => {