UNPKG

mauss

Version:

practical functions and reusable configurations

198 lines (197 loc) 6.67 kB
import { Rejection } from './error.js'; export function define(builder) { const schema = builder({ optional, boolean, number, string, literal, date, array, record, }); return (input) => { if (typeof schema === 'function') { return schema(input); } const errors = []; const result = check({ input, schema, errors }); if (errors.length) throw new Rejection(errors); return result; }; } function check({ path = [], errors, input, schema }) { if (typeof input !== 'object' || input == null) { const type = typeof input === 'object' ? 'null' : typeof input; return void errors.push({ expected: 'object', path: path.length ? path : ['<root>'], message: `[UnexpectedInput] Received "${type}"`, }); } const result = {}; for (const key in schema) { const current = [...path, key]; const value = input[key]; const validate = schema[key]; try { if (typeof validate === 'function') { result[key] = validate(value); } else if (typeof validate === 'object') { result[key] = check({ path: current, errors, input: value, schema: validate, }); } else { errors.push({ expected: 'definition', path: current, message: `[InvalidSchema] Received "${typeof validate}"`, }); } } catch (e) { if (e && typeof e.expected === 'string' && typeof e.message === 'string') { errors.push({ expected: e.expected, path: current, message: e.message }); } else if (e instanceof Rejection) errors.push(...e.issues); else throw e; } } return result; } export function optional(validator, fallback) { if (typeof validator === 'function') { return (input) => (input === void 0 ? fallback : validator(input)); } return (input) => { if (input === void 0) return undefined; const errors = []; const result = check({ input, schema: validator, errors }); if (errors.length) throw new Rejection(errors); return result; }; } // --- primitive validators --- export function boolean(transform) { return (input) => { if (typeof input !== 'boolean') { throw { expected: 'boolean', message: `[UnexpectedInput] Received "${typeof input}"` }; } return transform ? transform(input) : input; }; } export function number(transform) { return (input) => { switch (typeof input) { case 'number': break; case 'string': { if (input.trim() === '') { throw { expected: 'number', message: '[UnexpectedInput] Received empty string' }; } input = Number(input); break; } case 'boolean': { input = input ? 1 : 0; break; } case 'bigint': { if (input > Number.MAX_SAFE_INTEGER || input < Number.MIN_SAFE_INTEGER) { throw { expected: 'number', message: '[UnsafeCast] BigInt value exceeds safe integer range', }; } input = Number(input); break; } default: { throw { expected: 'number', message: `[UnexpectedInput] Received "${typeof input}"` }; } } if (Number.isNaN(input)) { throw { expected: 'number', message: '[UnexpectedInput] Received "NaN"' }; } return transform ? transform(input) : input; }; } export function string(transform) { return (input) => { if (typeof input !== 'string') { throw { expected: 'string', message: `[UnexpectedInput] Received "${typeof input}"` }; } return transform ? transform(input) : input; }; } export function literal(...values) { return (input) => { if (values.length === 0) { throw { expected: 'literal', message: '[InvalidSchema] No values provided' }; } if (typeof input !== 'string') { throw { expected: 'literal', message: `[UnexpectedInput] Received "${typeof input}"` }; } if (!values.includes(input)) { throw { expected: 'literal', message: `[InvalidInput] "${input}" is not in [${values.join(', ')}]`, }; } return input; }; } // --- string converters --- export function date(transform) { return (input) => { const d = input instanceof Date ? new Date(input.getTime()) : new Date(input); if (Number.isNaN(d.getTime())) { throw { expected: 'date', message: `[InvalidInput] Received "Invalid Date" from ${input}` }; } return transform ? transform(d) : d; }; } export function array(item, transform) { return (input) => { if (!Array.isArray(input)) { const type = typeof input === 'object' ? 'non-array object' : typeof input; throw { expected: 'array', message: `[UnexpectedInput] Received "${type}"` }; } const result = input.map((v) => { if (typeof item === 'function') return item(v); const errors = []; const result = check({ input: v, schema: item, errors }); if (errors.length) throw new Rejection(errors); return result; }); return transform ? transform(result) : result; }; } export function record(value, transform) { return (input) => { if (typeof input !== 'object' || input == null) { const type = typeof input === 'object' ? 'null or undefined' : typeof input; throw { expected: 'record', message: `[UnexpectedInput] Received "${type}"` }; } if (Array.isArray(input)) { throw { expected: 'record', message: '[InvalidInput] Received an array' }; } const result = {}; for (const key in input) { result[key] = value(input[key]); } return transform ? transform(result) : result; }; }