UNPKG

@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
/** * @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. */ 'use strict'; 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