@typeofweb/schema
Version:
`@typeofweb/schema` is a lightweight and extensible library for data validation with full TypeScript support!
399 lines (373 loc) • 11.2 kB
JavaScript
/**
* @typeofweb/schema@0.7.3
* Copyright (c) 2021 Type of Web - Michał Miszczyszyn
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
;
Object.defineProperty(exports, '__esModule', { value: true });
const isSchema = (val) => {
return typeof val === 'object' && val !== null && '__validate' in val && 'toString' in val;
};
const schemaToString = (schema) => {
return schema.toString();
};
const typeToPrint = (str) => str;
const objectToPrint = (str) => '{' + str + '}';
const quote = (str) => (/\s/.test(str) ? `"${str}"` : str);
const unionToPrint = (arr) => {
const str = arr.join(' | ');
if (arr.length > 1) {
return `(${str})`;
}
return str;
};
class ValidationError extends Error {
constructor(schema, value) {
const expected = schemaToString(schema);
const got = typeof value === 'function' ? String(value) : JSON.stringify(value);
const details = {
kind: 'TYPE_MISMATCH',
got,
expected,
};
super(`Invalid type! Expected ${details.expected} but got ${details.got}!`);
this.details = details;
this.name = 'ValidationError';
Error.captureStackTrace(this);
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
function pipe(schema, ...rest) {
return rest.reduce((acc, mod) => mod(acc), typeof schema === 'function' ? schema() : schema);
}
const λ = pipe;
const left = (value) => ({ _t: 'left', value });
const right = (value) => ({ _t: 'right', value });
const nextValid = (value) => ({ _t: 'nextValid', value });
const nextNotValid = (value) => ({
_t: 'nextNotValid',
value,
});
const refinementToolkit = {
right: right,
left: left,
nextValid,
nextNotValid,
};
const refine = (refinement, toString) => (schema) => {
return {
...schema,
toString() {
return unionToPrint(
[
schema === null || schema === void 0 ? void 0 : schema.toString(),
toString === null || toString === void 0 ? void 0 : toString(),
].filter(Boolean),
);
},
__validate(val) {
// eslint-disable-next-line functional/no-this-expression
const innerResult = refinement.call(this, val, refinementToolkit);
if (
(innerResult === null || innerResult === void 0 ? void 0 : innerResult._t) === 'left' ||
(innerResult === null || innerResult === void 0 ? void 0 : innerResult._t) === 'right'
) {
return innerResult;
}
if (!schema) {
return innerResult;
}
return schema.__validate(innerResult.value);
},
};
};
/* eslint-disable functional/no-loop-statement */
const array = (...validators) => {
return refine(
function (values, t) {
if (!Array.isArray(values)) {
return t.left(values);
}
let isError = false;
const result = new Array(values.length);
valuesLoop: for (let i = 0; i < values.length; ++i) {
const value = values[i];
for (let k = 0; k < validators.length; ++k) {
const validator = validators[k];
const r = validator.__validate(value);
if (r._t === 'right' || r._t === 'nextValid') {
result[i] = r.value;
continue valuesLoop;
}
}
result[i] = new ValidationError(this, values);
isError = true;
continue;
}
if (isError) {
return t.left(result);
}
return t.nextValid(result);
},
() => {
const str = validators.map((s) => schemaToString(s)).join(' | ');
return validators.length > 1 ? typeToPrint(`(${str})[]`) : typeToPrint(`${str}[]`);
},
);
};
const boolean = refine(
(value, t) => {
if (typeof value !== 'boolean') {
return t.left(value);
}
return t.nextValid(value);
},
() => typeToPrint('boolean'),
);
const isDate = (d) => Object.prototype.toString.call(d) === '[object Date]';
const simplifiedISODateStringRegex = /^[+-]?\d{4}/;
const isISODateString = (value) => simplifiedISODateStringRegex.test(value);
const date = refine(
(value, t) => {
const parsedValue = parseDate(value);
if (!isDate(parsedValue) || Number.isNaN(Number(parsedValue))) {
return t.left(parsedValue);
}
return t.nextValid(parsedValue);
},
() => typeToPrint('Date'),
);
function parseDate(value) {
if (typeof value === 'string' && isISODateString(value)) {
return new Date(value);
}
return value;
}
const number = refine(
(value, t) => {
const parsedValue = parseNumber(value);
if (typeof parsedValue !== 'number' || Number.isNaN(parsedValue)) {
return t.left(parsedValue);
}
return t.nextValid(parsedValue);
},
() => typeToPrint('number'),
);
function parseNumber(value) {
if (typeof value === 'string') {
if (value.trim() === '') {
return value;
}
return Number(value);
}
return value;
}
/* eslint-disable functional/no-loop-statement */
const object = (schemasObject, options) => {
return refine(
function (obj, t) {
if (typeof obj !== 'object' || obj === null) {
return t.left(obj);
}
const object = obj;
const validators = schemasObject;
const allowUnknownKeys = !!(options === null || options === void 0
? void 0
: options.allowUnknownKeys);
let isError = false;
const result = {};
for (const key in object) {
if (!Object.prototype.hasOwnProperty.call(object, key)) {
continue;
}
const value = object[key];
const validator = validators[key];
if (validator) {
const r = validator.__validate(value);
result[key] = r.value;
isError || (isError = r._t === 'left');
continue;
} else {
if (allowUnknownKeys) {
result[key] = value;
continue;
} else {
isError = true;
result[key] = new ValidationError(this, object);
continue;
}
}
}
for (const key in validators) {
if (!Object.prototype.hasOwnProperty.call(validators, key)) {
continue;
}
if (key in result) {
continue;
}
const validator = validators[key];
const value = object[key];
const r = validator.__validate(value);
isError || (isError = r._t === 'left');
}
if (isError) {
return t.left(result);
}
return t.nextValid(result);
},
() => {
const entries = Object.entries(schemasObject).map(([key, val]) => [key, schemaToString(val)]);
if (entries.length === 0) {
return objectToPrint('');
}
return objectToPrint(
' ' + entries.map(([key, val]) => quote(key) + ': ' + val).join(', ') + ' ',
);
},
);
};
/* eslint-disable functional/no-loop-statement */
// `U extends (Primitives)[]` and `[...U]` is a trick to force TypeScript to narrow the type correctly
// thanks to this, there's no need for "as const": oneOf(['a', 'b']) works as oneOf(['a', 'b'] as const)
const oneOf = (validatorsOrLiterals) => {
return refine(
function (value, t) {
for (let i = 0; i < validatorsOrLiterals.length; ++i) {
const valueOrSchema = validatorsOrLiterals[i];
if (isSchema(valueOrSchema)) {
const r = valueOrSchema.__validate(value);
if (r._t === 'right') {
return t.right(r.value);
} else if (r._t === 'nextValid') {
return t.nextValid(r.value);
}
continue;
} else {
if (value === valueOrSchema) {
return t.right(valueOrSchema);
}
}
}
return t.left(value);
},
() => {
const str = validatorsOrLiterals
.map((s) => (isSchema(s) ? schemaToString(s) : JSON.stringify(s)))
.join(' | ');
return validatorsOrLiterals.length > 1 ? `(${str})` : str;
},
);
};
const string = refine(
(value, t) => {
const parsedValue = parseString(value);
if (typeof parsedValue !== 'string') {
return t.left(parsedValue);
}
return t.nextValid(parsedValue);
},
() => typeToPrint('string'),
);
function parseString(value) {
if (isDate(value)) {
return value.toISOString();
}
return value;
}
/* eslint-disable functional/no-loop-statement */
const tuple = (validatorsOrLiterals) => {
return refine(
function (values, t) {
if (!Array.isArray(values) || values.length !== validatorsOrLiterals.length) {
return t.left(values);
}
let isError = false;
const result = new Array(values.length);
for (let i = 0; i < values.length; ++i) {
const valueOrSchema = validatorsOrLiterals[i];
const value = values[i];
if (isSchema(valueOrSchema)) {
const r = valueOrSchema.__validate(value);
result[i] = r.value;
isError || (isError = r._t === 'left');
continue;
} else {
if (valueOrSchema === value) {
result[i] = value;
continue;
} else {
result[i] = new ValidationError(this, validatorsOrLiterals);
isError = true;
continue;
}
}
}
if (isError) {
return t.left(result);
}
return t.nextValid(result);
},
() =>
'[' +
validatorsOrLiterals
.map((s) => (isSchema(s) ? schemaToString(s) : JSON.stringify(s)))
.join(', ') +
']',
);
};
const unknown = refine(
(value, t) => {
return t.nextValid(value);
},
() => typeToPrint('unknown'),
);
const validate = (schema) => (value) => {
const result = schema.__validate(value);
if (result._t === 'right' || result._t === 'nextValid') {
return result.value;
} else {
// throw result.value;
throw new ValidationError(schema, value);
}
};
const nil = refine(
(value, t) => (value === null || value === undefined ? t.right(value) : t.nextNotValid(value)),
() => `undefined | null`,
);
const nullable = refine(
(value, t) => (value === null ? t.right(null) : t.nextNotValid(value)),
() => typeToPrint('null'),
);
const optional = refine(
(value, t) => (value === undefined ? t.right(undefined) : t.nextNotValid(value)),
() => typeToPrint('undefined'),
);
const minArrayLength = (minLength) =>
refine((value, t) => {
return value.length >= minLength ? t.nextValid(value) : t.left(value);
});
const minStringLength = (minLength) =>
refine((value, t) => (value.length >= minLength ? t.nextValid(value) : t.left(value)));
exports.ValidationError = ValidationError;
exports.array = array;
exports.boolean = boolean;
exports.date = date;
exports.isSchema = isSchema;
exports.minArrayLength = minArrayLength;
exports.minStringLength = minStringLength;
exports.nil = nil;
exports.nullable = nullable;
exports.number = number;
exports.object = object;
exports.oneOf = oneOf;
exports.optional = optional;
exports.pipe = pipe;
exports.refine = refine;
exports.string = string;
exports.tuple = tuple;
exports.unknown = unknown;
exports.validate = validate;
exports.λ = λ;
//# sourceMappingURL=index.common.js.map