UNPKG

@metamask/superstruct

Version:

A simple and composable way to validate data in JavaScript (and TypeScript).

501 lines 16 kB
import { ExactOptionalStruct, Struct } from "../struct.mjs"; import { print, run, isObject } from "../utils.mjs"; import { define } from "./utilities.mjs"; /** * Ensure that any value passes validation. * * @returns A struct that will always pass validation. */ export function any() { return define('any', () => true); } /** * Ensure that a value is an array and that its elements are of a specific type. * * Note: If you omit the element struct, the arrays elements will not be * iterated at all. This can be helpful for cases where performance is critical, * and it is preferred to using `array(any())`. * * @param Element - The struct to validate each element in the array against. * @returns A new struct that will only accept arrays of the given type. */ export function array(Element) { return new Struct({ type: 'array', schema: Element, *entries(value) { if (Element && Array.isArray(value)) { for (const [index, arrayValue] of value.entries()) { yield [index, arrayValue, Element]; } } }, coercer(value) { return Array.isArray(value) ? value.slice() : value; }, validator(value) { return (Array.isArray(value) || `Expected an array value, but received: ${print(value)}`); }, }); } /** * Ensure that a value is a bigint. * * @returns A new struct that will only accept bigints. */ export function bigint() { return define('bigint', (value) => { return typeof value === 'bigint'; }); } /** * Ensure that a value is a boolean. * * @returns A new struct that will only accept booleans. */ export function boolean() { return define('boolean', (value) => { return typeof value === 'boolean'; }); } /** * Ensure that a value is a valid `Date`. * * Note: this also ensures that the value is *not* an invalid `Date` object, * which can occur when parsing a date fails but still returns a `Date`. * * @returns A new struct that will only accept valid `Date` objects. */ export function date() { return define('date', (value) => { return ((value instanceof Date && !isNaN(value.getTime())) || `Expected a valid \`Date\` object, but received: ${print(value)}`); }); } /** * Ensure that a value is one of a set of potential values. * * Note: after creating the struct, you can access the definition of the * potential values as `struct.schema`. * * @param values - The potential values that the input can be. * @returns A new struct that will only accept the given values. */ export function enums(values) { const schema = {}; const description = values.map((value) => print(value)).join(); for (const key of values) { schema[key] = key; } return new Struct({ type: 'enums', schema, validator(value) { return (values.includes(value) || `Expected one of \`${description}\`, but received: ${print(value)}`); }, }); } /** * Ensure that a value is a function. * * @returns A new struct that will only accept functions. */ // eslint-disable-next-line @typescript-eslint/ban-types export function func() { return define('func', (value) => { return (typeof value === 'function' || `Expected a function, but received: ${print(value)}`); }); } /** * Ensure that a value is an instance of a specific class. * * @param Class - The class that the value must be an instance of. * @returns A new struct that will only accept instances of the given class. */ export function instance(Class) { return define('instance', (value) => { return (value instanceof Class || `Expected a \`${Class.name}\` instance, but received: ${print(value)}`); }); } /** * Ensure that a value is an integer. * * @returns A new struct that will only accept integers. */ export function integer() { return define('integer', (value) => { return ((typeof value === 'number' && !isNaN(value) && Number.isInteger(value)) || `Expected an integer, but received: ${print(value)}`); }); } /** * Ensure that a value matches all of a set of types. * * @param Structs - The set of structs that the value must match. * @returns A new struct that will only accept values that match all of the * given structs. */ export function intersection(Structs) { return new Struct({ type: 'intersection', schema: null, *entries(value, context) { for (const { entries } of Structs) { yield* entries(value, context); } }, *validator(value, context) { for (const { validator } of Structs) { yield* validator(value, context); } }, *refiner(value, context) { for (const { refiner } of Structs) { yield* refiner(value, context); } }, }); } /** * Ensure that a value is an exact value, using `===` for comparison. * * @param constant - The exact value that the input must be. * @returns A new struct that will only accept the exact given value. */ export function literal(constant) { const description = print(constant); const valueType = typeof constant; return new Struct({ type: 'literal', schema: valueType === 'string' || valueType === 'number' || valueType === 'boolean' ? constant : null, validator(value) { return (value === constant || `Expected the literal \`${description}\`, but received: ${print(value)}`); }, }); } /** * Ensure that a value is a `Map` object, and that its keys and values are of * specific types. * * @param Key - The struct to validate each key in the map against. * @param Value - The struct to validate each value in the map against. * @returns A new struct that will only accept `Map` objects. */ export function map(Key, Value) { return new Struct({ type: 'map', schema: null, *entries(value) { if (Key && Value && value instanceof Map) { for (const [mapKey, mapValue] of value.entries()) { yield [mapKey, mapKey, Key]; yield [mapKey, mapValue, Value]; } } }, coercer(value) { return value instanceof Map ? new Map(value) : value; }, validator(value) { return (value instanceof Map || `Expected a \`Map\` object, but received: ${print(value)}`); }, }); } /** * Ensure that no value ever passes validation. * * @returns A new struct that will never pass validation. */ export function never() { return define('never', () => false); } /** * Augment an existing struct to allow `null` values. * * @param struct - The struct to augment. * @returns A new struct that will accept `null` values. */ export function nullable(struct) { return new Struct({ ...struct, validator: (value, ctx) => value === null || struct.validator(value, ctx), refiner: (value, ctx) => value === null || struct.refiner(value, ctx), }); } /** * Ensure that a value is a number. * * @returns A new struct that will only accept numbers. */ export function number() { return define('number', (value) => { return ((typeof value === 'number' && !isNaN(value)) || `Expected a number, but received: ${print(value)}`); }); } /** * Ensure that a value is an object, that it has a known set of properties, * and that its properties are of specific types. * * Note: Unrecognized properties will fail validation. * * @param schema - An object that defines the structure of the object. * @returns A new struct that will only accept objects. */ export function object(schema) { const knowns = schema ? Object.keys(schema) : []; const Never = never(); return new Struct({ type: 'object', schema: schema ?? null, *entries(value) { if (schema && isObject(value)) { const unknowns = new Set(Object.keys(value)); for (const key of knowns) { unknowns.delete(key); const propertySchema = schema[key]; if (ExactOptionalStruct.isExactOptional(propertySchema) && !Object.prototype.hasOwnProperty.call(value, key)) { continue; } yield [key, value[key], schema[key]]; } for (const key of unknowns) { yield [key, value[key], Never]; } } }, validator(value) { return (isObject(value) || `Expected an object, but received: ${print(value)}`); }, coercer(value) { return isObject(value) ? { ...value } : value; }, }); } /** * Augment a struct to allow `undefined` values. * * @param struct - The struct to augment. * @returns A new struct that will accept `undefined` values. */ export function optional(struct) { return new Struct({ ...struct, validator: (value, ctx) => value === undefined || struct.validator(value, ctx), refiner: (value, ctx) => value === undefined || struct.refiner(value, ctx), }); } /** * Augment a struct such that, if it is the property of an object, it is exactly optional. * In other words, it is either present with the correct type, or not present at all. * * NOTE: Only intended for use with `object()` structs. * * @param struct - The struct to augment. * @returns A new struct that can be used to create exactly optional properties of `object()` * structs. */ export function exactOptional(struct) { return new ExactOptionalStruct(struct); } /** * Ensure that a value is an object with keys and values of specific types, but * without ensuring any specific shape of properties. * * Like TypeScript's `Record` utility. */ /** * Ensure that a value is an object with keys and values of specific types, but * without ensuring any specific shape of properties. * * @param Key - The struct to validate each key in the record against. * @param Value - The struct to validate each value in the record against. * @returns A new struct that will only accept objects. */ export function record(Key, Value) { return new Struct({ type: 'record', schema: null, *entries(value) { if (isObject(value)) { // eslint-disable-next-line guard-for-in for (const objectKey in value) { const objectValue = value[objectKey]; yield [objectKey, objectKey, Key]; yield [objectKey, objectValue, Value]; } } }, validator(value) { return (isObject(value) || `Expected an object, but received: ${print(value)}`); }, }); } /** * Ensure that a value is a `RegExp`. * * Note: this does not test the value against the regular expression! For that * you need to use the `pattern()` refinement. * * @returns A new struct that will only accept `RegExp` objects. */ export function regexp() { return define('regexp', (value) => { return value instanceof RegExp; }); } /** * Ensure that a value is a `Set` object, and that its elements are of a * specific type. * * @param Element - The struct to validate each element in the set against. * @returns A new struct that will only accept `Set` objects. */ export function set(Element) { return new Struct({ type: 'set', schema: null, *entries(value) { if (Element && value instanceof Set) { for (const setValue of value) { yield [setValue, setValue, Element]; } } }, coercer(value) { return value instanceof Set ? new Set(value) : value; }, validator(value) { return (value instanceof Set || `Expected a \`Set\` object, but received: ${print(value)}`); }, }); } /** * Ensure that a value is a string. * * @returns A new struct that will only accept strings. */ export function string() { return define('string', (value) => { return (typeof value === 'string' || `Expected a string, but received: ${print(value)}`); }); } /** * Ensure that a value is a tuple of a specific length, and that each of its * elements is of a specific type. * * @param Structs - The set of structs that the value must match. * @returns A new struct that will only accept tuples of the given types. */ export function tuple(Structs) { const Never = never(); return new Struct({ type: 'tuple', schema: null, *entries(value) { if (Array.isArray(value)) { const length = Math.max(Structs.length, value.length); for (let i = 0; i < length; i++) { yield [i, value[i], Structs[i] || Never]; } } }, validator(value) { return (Array.isArray(value) || `Expected an array, but received: ${print(value)}`); }, }); } /** * Ensure that a value has a set of known properties of specific types. * * Note: Unrecognized properties are allowed and untouched. This is similar to * how TypeScript's structural typing works. * * @param schema - An object that defines the structure of the object. * @returns A new struct that will only accept objects. */ export function type(schema) { const keys = Object.keys(schema); return new Struct({ type: 'type', schema, *entries(value) { if (isObject(value)) { for (const k of keys) { yield [k, value[k], schema[k]]; } } }, validator(value) { return (isObject(value) || `Expected an object, but received: ${print(value)}`); }, coercer(value) { return isObject(value) ? { ...value } : value; }, }); } /** * Ensure that a value matches one of a set of types. * * @param Structs - The set of structs that the value must match. * @returns A new struct that will only accept values that match one of the * given structs. */ export function union(Structs) { const description = Structs.map((struct) => struct.type).join(' | '); return new Struct({ type: 'union', schema: null, coercer(value) { for (const InnerStruct of Structs) { const [error, coerced] = InnerStruct.validate(value, { coerce: true }); if (!error) { return coerced; } } return value; }, validator(value, ctx) { const failures = []; for (const InnerStruct of Structs) { const [...tuples] = run(value, InnerStruct, ctx); const [first] = tuples; if (!first?.[0]) { return []; } for (const [failure] of tuples) { if (failure) { failures.push(failure); } } } return [ `Expected the value to satisfy a union of \`${description}\`, but received: ${print(value)}`, ...failures, ]; }, }); } /** * Ensure that any value passes validation, without widening its type to `any`. * * @returns A struct that will always pass validation. */ export function unknown() { return define('unknown', () => true); } //# sourceMappingURL=types.mjs.map