UNPKG

@metamask/superstruct

Version:

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

202 lines 6.83 kB
/** * Check if a value is an iterator. * * @param value - The value to check. * @returns Whether the value is an iterator. */ function isIterable(value) { return isObject(value) && typeof value[Symbol.iterator] === 'function'; } /** * Check if a value is a plain object. * * @param value - The value to check. * @returns Whether the value is a plain object. */ export function isObject(value) { return typeof value === 'object' && value !== null; } /** * Check if a value is a plain object. * * @param value - The value to check. * @returns Whether the value is a plain object. */ export function isPlainObject(value) { if (Object.prototype.toString.call(value) !== '[object Object]') { return false; } const prototype = Object.getPrototypeOf(value); return prototype === null || prototype === Object.prototype; } /** * Return a value as a printable string. * * @param value - The value to print. * @returns The value as a string. */ export function print(value) { if (typeof value === 'symbol') { return value.toString(); } // eslint-disable-next-line @typescript-eslint/restrict-template-expressions return typeof value === 'string' ? JSON.stringify(value) : `${value}`; } /** * Shift (remove and return) the first value from the `input` iterator. * Like `Array.prototype.shift()` but for an `Iterator`. * * @param input - The iterator to shift. * @returns The first value of the iterator, or `undefined` if the iterator is * empty. */ export function shiftIterator(input) { const { done, value } = input.next(); return done ? undefined : value; } /** * Convert a single validation result to a failure. * * @param result - The result to convert. * @param context - The context of the validation. * @param struct - The struct being validated. * @param value - The value being validated. * @returns A failure if the result is a failure, or `undefined` if the result * is a success. */ export function toFailure(result, context, struct, value) { if (result === true) { return undefined; } else if (result === false) { // eslint-disable-next-line no-param-reassign result = {}; } else if (typeof result === 'string') { // eslint-disable-next-line no-param-reassign result = { message: result }; } const { path, branch } = context; const { type } = struct; const { refinement, message = `Expected a value of type \`${type}\`${refinement ? ` with refinement \`${refinement}\`` : ''}, but received: \`${print(value)}\``, } = result; return { value, type, refinement, key: path[path.length - 1], path, branch, ...result, message, }; } /** * Convert a validation result to an iterable of failures. * * @param result - The result to convert. * @param context - The context of the validation. * @param struct - The struct being validated. * @param value - The value being validated. * @yields The failures. * @returns An iterable of failures. */ export function* toFailures(result, context, struct, value) { if (!isIterable(result)) { // eslint-disable-next-line no-param-reassign result = [result]; } for (const validationResult of result) { const failure = toFailure(validationResult, context, struct, value); if (failure) { yield failure; } } } /** * Check a value against a struct, traversing deeply into nested values, and * returning an iterator of failures or success. * * @param value - The value to check. * @param struct - The struct to check against. * @param options - Optional settings. * @param options.path - The path to the value in the input data. * @param options.branch - The branch of the value in the input data. * @param options.coerce - Whether to coerce the value before validating it. * @param options.mask - Whether to mask the value before validating it. * @param options.message - An optional message to include in the error. * @yields An iterator of failures or success. * @returns An iterator of failures or success. */ export function* run(value, struct, options = {}) { const { path = [], branch = [value], coerce = false, mask = false } = options; const context = { path, branch }; if (coerce) { // eslint-disable-next-line no-param-reassign value = struct.coercer(value, context); if (mask && struct.type !== 'type' && isObject(struct.schema) && isObject(value) && !Array.isArray(value)) { for (const key in value) { if (struct.schema[key] === undefined) { delete value[key]; } } } } let status = 'valid'; for (const failure of struct.validator(value, context)) { failure.explanation = options.message; status = 'not_valid'; yield [failure, undefined]; } // eslint-disable-next-line prefer-const for (let [innerKey, innerValue, innerStruct] of struct.entries(value, context)) { const iterable = run(innerValue, innerStruct, { path: innerKey === undefined ? path : [...path, innerKey], branch: innerKey === undefined ? branch : [...branch, innerValue], coerce, mask, message: options.message, }); for (const result of iterable) { if (result[0]) { status = result[0].refinement === null || result[0].refinement === undefined ? 'not_valid' : 'not_refined'; yield [result[0], undefined]; } else if (coerce) { innerValue = result[1]; if (innerKey === undefined) { // eslint-disable-next-line no-param-reassign value = innerValue; } else if (value instanceof Map) { value.set(innerKey, innerValue); } else if (value instanceof Set) { value.add(innerValue); } else if (isObject(value)) { if (innerValue !== undefined || innerKey in value) { value[innerKey] = innerValue; } } } } } if (status !== 'not_valid') { for (const failure of struct.refiner(value, context)) { failure.explanation = options.message; status = 'not_refined'; yield [failure, undefined]; } } if (status === 'valid') { yield [undefined, value]; } } //# sourceMappingURL=utils.mjs.map