n4s
Version:
typed schema validation version of enforce
238 lines (199 loc) • 6.81 kB
text/typescript
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: [] }],
},
],
});
});
});