import { assertValidation, ValidationErrorBuilder } from '../errors' import { registry } from '../registry' import { AbstractValidator, makeValidatorFactory } from '../types' import { getValidator } from '../utilities' import type { InferInputSchema, InferSchema, InferValidation, Schema, TupleRestParameter, Validation, ValidationOptions, Validator, } from '../types' /* ========================================================================== * * OBJECT VALIDATOR * * ========================================================================== */ /** A `Validator` validating any `object`. */ export class AnyObjectValidator extends AbstractValidator> { validate(value: unknown): Record { assertValidation(typeof value === 'object', 'Value is not an "object"') assertValidation(value !== null, 'Value is "null"') return value } } /** A `Validator` validating `object`s according to a `Schema`. */ export class ObjectValidator extends AbstractValidator, InferInputSchema> { readonly schema: Readonly validators = new Map() additionalProperties?: Validator constructor(schema: S) { super() const { [Symbol.justusAdditionalValidator]: additional, ...properties } = schema if (additional) this.additionalProperties = additional for (const key of Object.keys(properties)) { this.validators.set(key, getValidator(properties[key])) } this.schema = schema } validate(value: unknown, options: ValidationOptions = {}): InferSchema { assertValidation(typeof value === 'object', 'Value is not an "object"') assertValidation(value !== null, 'Value is "null"') const { stripAdditionalProperties, stripOptionalNulls, partialValidation } = options const record: { [ k in string | number | symbol ]?: unknown } = value const builder = new ValidationErrorBuilder() const clone: Record = {} for (const [ key, validator ] of this.validators.entries()) { const optional = (!! validator.optional) || (!! partialValidation) const original = record[key] // strip any optional "null" value if told to do so if (stripOptionalNulls && optional && (original === null)) { continue } // if we have no value, then we have few possibilities: // - we are performing a partial validation, so we ignore // - the (optional) validator provides a valid value // - the validator is optional, so we can simply ignore // - the validator is not optional, so the property is missing if (original === undefined) { if (partialValidation) continue try { // try to validate, the validator _might_ be giving us a value const validated = validator.validate(original, options) // put the validated value in the clone, unless optional and undefined if (! (optional && (validated === undefined))) clone[key] = validated } catch { if (optional) continue // original was undefined, so we can skip! builder.record('Required property missing', key) } continue } // here value was _not_ undefined, so we have to validate it normally try { const validated = validator.validate(original, options) // put the validated value in the clone, unless optional and undefined if (! (optional && (validated === undefined))) clone[key] = validated } catch (error) { builder.record(error, key) } } // process additional keys, as all keys NOT defined in the schema const additionalKeys = Object.keys(record).filter((k) => !this.validators.has(k)) const additional = this.additionalProperties if (additional) { additionalKeys.forEach((key) => { if (record[key] === undefined) return try { clone[key] = additional.validate(record[key], options) } catch (error) { builder.record(error, key) } }) } else if (! stripAdditionalProperties) { additionalKeys.forEach((key) => { if (record[key] !== undefined) builder.record('Unknown property', key) }) } return builder.assert(clone as InferSchema) } } export function objectValidatorFactory(schema: S): S & { [Symbol.iterator](): Generator, InferInputSchema>> } { const validator = new ObjectValidator(schema) function* iterator(): Generator> { yield { [Symbol.justusRestValidator]: validator } } return Object.defineProperties(schema, { [Symbol.justusValidator]: { value: validator, enumerable: false }, [Symbol.iterator]: { value: iterator, enumerable: false }, }) as any } /** Validate `object`s. */ export const object = makeValidatorFactory(new AnyObjectValidator(), objectValidatorFactory) /** Validate `Object`s containing only the specified elements. */ export function objectOf(validation: V): Validator>> { return new ObjectValidator({ [Symbol.justusAdditionalValidator]: getValidator(validation) }) } // Register our "object" validator registry.set('object', ObjectValidator)