mauss
Version:
practical functions and reusable configurations
198 lines (197 loc) • 6.67 kB
JavaScript
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;
};
}