|  | @@ -0,0 +1,199 @@
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * IMPORTED EXTERNAL LIB - DO NOT MODIFY
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Validation specification.
 | 
	
		
			
				|  |  | + * Given an object type, a specification outline is derived to ensure that validation is defined for all scalar
 | 
	
		
			
				|  |  | + * properties.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * If a property is array-type, a single validator must be specified which receives the entire array.
 | 
	
		
			
				|  |  | + * If a property is otherwise object-type, the validation spec extends deeply into it.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * Optional properties must still be specified; the `optional()` function can be used to allow them to be skipped if
 | 
	
		
			
				|  |  | + * the value is undefined.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export type Spec<T> =
 | 
	
		
			
				|  |  | +  T extends Array<unknown>
 | 
	
		
			
				|  |  | +    ? ValidateFn
 | 
	
		
			
				|  |  | +    : T extends object
 | 
	
		
			
				|  |  | +      ? { [P in keyof Required<T>]: Spec<T[P]> }
 | 
	
		
			
				|  |  | +      : ValidateFn
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validation error thrown by `validate()`. Provides the failed parameter along with the error message. */
 | 
	
		
			
				|  |  | +export class ValidateError extends Error {
 | 
	
		
			
				|  |  | +  param: string
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +  constructor(param: string, message: string) {
 | 
	
		
			
				|  |  | +    super(message)
 | 
	
		
			
				|  |  | +    this.name = 'ValidateError'
 | 
	
		
			
				|  |  | +    this.param = param
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * A validation function takes any input and throws an error if it does not satisfy the relevant condition.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * It may return `true` signifying that further validation is not required; see `seq()` for more detail.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * When used in conjunction with `validate()` to parse an object, an `origInput` is also passed giving the entire
 | 
	
		
			
				|  |  | + * data object.
 | 
	
		
			
				|  |  | + * This can be used to validate one property based on the value of another, though as the type of the parent input
 | 
	
		
			
				|  |  | + * is `unknown` knowledge of the structure has to be asserted by the caller.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export type ValidateFn = (input: unknown, origInput?: unknown) => true | void
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate input is Boolean. */
 | 
	
		
			
				|  |  | +export const bool: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  if (typeof input !== 'boolean') throw new Error('must be boolean')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * FQDN format expression.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * Based on https://stackoverflow.com/a/62917037/1717753
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +const domainRegexp = /^((?=[a-z0-9-_]{1,63}\.)(xn--)?[a-z0-9_]+(-[a-z0-9_]+)*\.)+[a-z]{2,63}$/
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Validate input is a fully-qualified domain name.
 | 
	
		
			
				|  |  | + * Implicitly validates `str()` for convenience.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export const domain: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  str(input)
 | 
	
		
			
				|  |  | +  if (!domainRegexp.test(input as string)) throw new Error('invalid domain')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Email format expression. */
 | 
	
		
			
				|  |  | +const emailRegexp = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Validate input is an email address.
 | 
	
		
			
				|  |  | + * Implicitly validates `str()` for convenience.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export const email: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  str(input)
 | 
	
		
			
				|  |  | +  if (!emailRegexp.test(input as string)) throw new Error('invalid email address')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate input is exactly equal to a comparison value. */
 | 
	
		
			
				|  |  | +export const eq = <T>(cmp: T): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if (input !== cmp) throw new Error(`must be ${cmp}`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate exact length of string. */
 | 
	
		
			
				|  |  | +export const exactLength = (n: number): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if ((input as string).length !== n) throw new Error(`must contain exactly ${n} characters`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Lowercase hexadecimal expression. */
 | 
	
		
			
				|  |  | +const hexRegexp = /^[a-f0-9]*$/
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Validate string is lowercase hexadecimal.
 | 
	
		
			
				|  |  | + * Implicitly validates `str()` for convenience.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export const hex: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  str(input)
 | 
	
		
			
				|  |  | +  if (!hexRegexp.test(input as string)) throw new Error('invalid characters')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Validate input is an integer.
 | 
	
		
			
				|  |  | + * Implicitly validates `numeric()` for convenience.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export const integer: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  numeric(input)
 | 
	
		
			
				|  |  | +  if ((input as number).toString().indexOf('.') > -1) throw new Error('must be an integer')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate maximum number. */
 | 
	
		
			
				|  |  | +export const max = (n: number): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if (input as number > n) throw new Error(`must be no more than ${n}`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate maximum length of string. */
 | 
	
		
			
				|  |  | +export const maxLength = (n: number): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if ((input as string).length > n) throw new Error(`must be no longer than ${n} characters`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate minimum number. */
 | 
	
		
			
				|  |  | +export const min = (n: number): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if (input as number < n) throw new Error(`must be no less than ${n}`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate minimum length of string. */
 | 
	
		
			
				|  |  | +export const minLength = (n: number): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if ((input as string).length < n) throw new Error(`must be no shorter than ${n} characters`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate input is numeric. */
 | 
	
		
			
				|  |  | +export const numeric: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  if (typeof input !== 'number') throw new Error('must be a number')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate input is one of a range of options. */
 | 
	
		
			
				|  |  | +export const oneOf = <T>(range: T[]): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if (!range.includes(input as T)) throw new Error(`must be one of: ${range.join(', ')}`)
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Special non-validator that returns true if the input is null or undefined, and never throws an error.
 | 
	
		
			
				|  |  | + * Normally used with `seq()`.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export const optional: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  if (input === null || input === undefined) return true
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate string against regular expression. */
 | 
	
		
			
				|  |  | +export const regexp = (re: RegExp): ValidateFn => input => {
 | 
	
		
			
				|  |  | +  if (!re.test(input as string)) throw new Error('invalid characters')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Sequentially runs multiple validation functions.
 | 
	
		
			
				|  |  | + * If a function returns true, for example `optional()`, subsequent validation is ignored.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export const seq = (...fs: ValidateFn[]): ValidateFn => (input, origInput) => {
 | 
	
		
			
				|  |  | +  for (let i = 0; i < fs.length; i++) {
 | 
	
		
			
				|  |  | +    if (fs[i](input, origInput)) return
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/** Validate input is a string. */
 | 
	
		
			
				|  |  | +export const str: ValidateFn = input => {
 | 
	
		
			
				|  |  | +  if (typeof input !== 'string') throw new Error('must be a string')
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * Read an unknown input and assert it matches an object specification.
 | 
	
		
			
				|  |  | + * This function immediately throws a ValidateError if any validation fails.
 | 
	
		
			
				|  |  | + * Otherwise, a typed copy of the input is returned.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * This method does not support validation across multiple properties or asynchronous validation.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * @todo Remove usage of `any` type.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +export const validate = <T extends object>(spec: Spec<Required<T>>, parent = '') =>
 | 
	
		
			
				|  |  | +  (input: unknown, origInput?: unknown): T => {
 | 
	
		
			
				|  |  | +    type V = keyof Spec<Required<T>>
 | 
	
		
			
				|  |  | +    type I = keyof T
 | 
	
		
			
				|  |  | +    if (typeof input !== 'object' || input === null) throw new Error('no data')
 | 
	
		
			
				|  |  | +    return Object.keys(spec).reduce((v, k) => {
 | 
	
		
			
				|  |  | +      const f = spec[k as V]
 | 
	
		
			
				|  |  | +      const value = (input as T)[k as I]
 | 
	
		
			
				|  |  | +      if (typeof f === 'function') {
 | 
	
		
			
				|  |  | +        try {
 | 
	
		
			
				|  |  | +          f(value, origInput || input)
 | 
	
		
			
				|  |  | +          v[k as I] = value
 | 
	
		
			
				|  |  | +        } catch (err) {
 | 
	
		
			
				|  |  | +          const param = parent ? `${parent}.${k}` : k
 | 
	
		
			
				|  |  | +          throw new ValidateError(param, (err as Error).message)
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +      } else {
 | 
	
		
			
				|  |  | +        // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
	
		
			
				|  |  | +        v[k as I] = validate(f as any, k as string)(value as any, origInput || input) as any
 | 
	
		
			
				|  |  | +      }
 | 
	
		
			
				|  |  | +      return v
 | 
	
		
			
				|  |  | +    }, <T>{})
 | 
	
		
			
				|  |  | +  }
 |