UNPKG

n4s

Version:

typed schema validation version of enforce

238 lines (199 loc) 6.81 kB
import { describe, expect, it } from 'vitest'; import { enforce } from '../../../n4s'; import type { RuleInstance } from '../../../utils/RuleInstance'; declare global { namespace n4s { interface EnforceMatchers { toNumber: (value: unknown) => { pass: boolean; type: number }; trimString: (value: unknown) => { pass: boolean; type: string }; } } } enforce.extend({ toNumber: (value: unknown) => { const parsed = Number(value); return Number.isNaN(parsed) ? { pass: false, type: value } : { pass: true, type: parsed }; }, trimString: (value: unknown) => { if (typeof value !== 'string') { return { pass: false, type: value }; } return { pass: true, type: value.trim() }; }, }); describe('schema parse integration', () => { it('shape parses nested values with custom coercions', () => { const schema = enforce.shape({ profile: enforce.shape({ name: enforce.trimString(), age: enforce.toNumber(), }), }); const result = schema.parse({ profile: { name: ' Jane ', age: '34' }, }); expect(result).toEqual({ profile: { name: 'Jane', age: 34 }, }); }); it('loose parses known keys while keeping extra payload fields', () => { const schema = enforce.loose({ amount: enforce.toNumber(), title: enforce.trimString(), }); const result = schema.parse({ amount: '49', title: ' invoice ', metadata: { source: 'api' }, }); expect(result).toEqual({ amount: 49, title: 'invoice', metadata: { source: 'api' }, }); }); it('partial parses only provided keys', () => { const schema = enforce.partial({ page: enforce.toNumber(), search: enforce.trimString(), }); expect(schema.parse({ page: '2' })).toEqual({ page: 2 }); expect(schema.parse({ search: ' vest ' })).toEqual({ search: 'vest' }); }); it('rejects dangerous own keys to prevent prototype pollution', () => { const schema = enforce.loose({ safe: enforce.isString(), }); const payload = JSON.parse('{"safe":"ok","__proto__":{"polluted":true}}'); const result = schema.run(payload); expect(result.pass).toBe(false); expect(result.path).toEqual(['__proto__']); expect(({} as Record<string, unknown>).polluted).toBeUndefined(); }); it('rejects dangerous schema keys', () => { const schema = enforce.shape(JSON.parse('{"__proto__":true}')); const result = schema.run({}); expect(result.pass).toBe(false); expect(result.path).toEqual(['__proto__']); }); it('isArrayOf parses array elements and preserves type transformations', () => { const schema = enforce.isArrayOf( enforce.shape({ name: enforce.trimString(), age: enforce.toNumber(), }), ); const result = schema.parse([ { name: ' Jane ', age: '34' }, { name: ' John ', age: '45' }, ]); expect(result).toEqual([ { name: 'Jane', age: 34 }, { name: 'John', age: 45 }, ]); }); it('shape parses values with built-in lazy parser chains', () => { const schema = enforce.shape({ name: enforce.isString().trim().toTitle(), age: enforce.isNumeric().toNumber().clamp(0, 120), subscribed: enforce.isString().trim().toBoolean(), tags: enforce.isArray<string>().uniq().join('|'), payload: enforce.isString().parseJSON(), nickname: enforce.isString().trim().defaultTo('N/A'), }); const result = schema.parse({ name: ' jANE DOE ', age: '180', subscribed: ' yes ', tags: ['vest', 'n4s', 'vest'], payload: '{"env":"test"}', nickname: ' ', }); expect(result).toEqual({ name: 'Jane Doe', age: 120, subscribed: true, tags: 'vest|n4s', payload: { env: 'test' }, nickname: '', }); }); it('defaultTo applies fallback for nullish values before type checks', () => { const schema = enforce.shape({ label: enforce.isString().defaultTo('N/A'), }); // @ts-expect-error - testing nullish input against string schema expect(schema.parse({ label: null })).toEqual({ label: 'N/A' }); // @ts-expect-error - testing nullish input against string schema expect(schema.parse({ label: undefined })).toEqual({ label: 'N/A' }); expect(schema.parse({ label: 'hello' })).toEqual({ label: 'hello' }); }); it('lazy propagates parse transformations from inner schema', () => { const inner = enforce.shape({ name: enforce.trimString(), age: enforce.toNumber(), }); const schema = enforce.lazy(() => inner); // @ts-expect-error - input type differs from output due to coercions const result = schema.parse({ name: ' Jane ', age: '34' }); expect(result).toEqual({ name: 'Jane', age: 34 }); }); it('tuple parses element values with custom coercions', () => { const schema = enforce.tuple(enforce.trimString(), enforce.toNumber()); const result = schema.parse([' hello ', '42']); expect(result).toEqual(['hello', 42]); }); it('tuple parses nested shape elements', () => { const schema = enforce.tuple( enforce.trimString(), enforce.shape({ name: enforce.trimString(), age: enforce.toNumber(), }), ); const result = schema.parse([' label ', { name: ' Jane ', age: '34' }]); expect(result).toEqual(['label', { name: 'Jane', age: 34 }]); }); it('tuple parses with built-in parser chains', () => { const schema = enforce.tuple( enforce.isString().trim().toTitle(), enforce.isNumeric().toNumber().clamp(0, 100), ); const result = schema.parse([' jANE DOE ', '180']); expect(result).toEqual(['Jane Doe', 100]); }); it('tuple inside shape preserves parse transformations', () => { const schema = enforce.shape({ label: enforce.trimString(), coords: enforce.tuple(enforce.toNumber(), enforce.toNumber()), }); const result = schema.parse({ label: ' origin ', coords: ['10', '20'] }); expect(result).toEqual({ label: 'origin', coords: [10, 20] }); }); it('lazy propagates parse transformations through recursive schemas', () => { const schema: RuleInstance<any> = enforce.shape({ name: enforce.trimString(), children: enforce.isArrayOf(enforce.lazy(() => schema)), }); const result = schema.parse({ name: ' Root ', children: [ { name: ' Child ', children: [{ name: ' Grandchild ', children: [] }], }, ], }); expect(result).toEqual({ name: 'Root', children: [ { name: 'Child', children: [{ name: 'Grandchild', children: [] }], }, ], }); }); });