import { ValidationError, ValidationErrorBuilder } from '../errors' import { AbstractValidator, makeValidatorFactory } from '../types' import { ConstantValidator } from '../validators/constant' import { ObjectValidator } from '../validators/object' import type { ValidationOptions } from '..' import type { Schema, Validator } from '../types' const KEYS: Exclude[] = [ 'href', 'origin', 'protocol', 'username', 'password', 'host', 'hostname', 'port', 'pathname', 'search', 'hash', ] const OPTIONS: ValidationOptions = { stripAdditionalProperties: false, stripForbiddenProperties: false, stripOptionalNulls: false, } /** Constraints to validate a `URL` with. */ export interface URLConstraints { /** Constraint to validate the `href` component of the `URL`. */ href?: string | Validator, /** Constraint to validate the `origin` component of the `URL`. */ origin?: string | Validator, /** Constraint to validate the `protocol` component of the `URL`. */ protocol?: string | Validator, /** Constraint to validate the `username` component of the `URL`. */ username?: string | Validator, /** Constraint to validate the `password` component of the `URL`. */ password?: string | Validator, /** Constraint to validate the `host` (`hostname:port`) component of the `URL`. */ host?: string | Validator, /** Constraint to validate the `hostname` component of the `URL`. */ hostname?: string | Validator, /** Constraint to validate the `port` component of the `URL`. */ port?: string | Validator, /** Constraint to validate the `pathname` component of the `URL`. */ pathname?: string | Validator, /** Constraint to validate the `search` component of the `URL` as a string. */ search?: string | Validator, /** Constraint to validate the `hash` component of the `URL`. */ hash?: string | Validator, /** * Schema used to validate the `searchParams` component of the `URL`. * * The `searchParams` will be normalized in a `Record`, where * only the _first_ value associated with a search parameter will be checked. */ searchParams?: Schema, } /** A `Validator` validating URLs and converting them to `URL` instances. */ export class URLValidator extends AbstractValidator { readonly href?: Validator readonly origin?: Validator readonly protocol?: Validator readonly username?: Validator readonly password?: Validator readonly host?: Validator readonly hostname?: Validator readonly port?: Validator readonly pathname?: Validator readonly search?: Validator readonly hash?: Validator readonly searchParams?: ObjectValidator constructor(constraints: URLConstraints = {}) { super() for (const key of KEYS) { const constraint = constraints[key] if (typeof constraint === 'string') { this[key] = new ConstantValidator(constraint) } else if (constraint) { this[key] = constraint } } if (constraints.searchParams) { this.searchParams = new ObjectValidator(constraints.searchParams) } } validate(value: unknown): URL { let url: URL try { url = value instanceof URL ? value : new URL(value as any) } catch { throw new ValidationError('Value could not be converted to a "URL"') } const builder = new ValidationErrorBuilder() for (const key of KEYS) { const validator = this[key] if (validator) { try { validator.validate(url[key], OPTIONS) } catch (error) { builder.record(error, key) } } } if (this.searchParams) { const parameters: Record = {} url.searchParams.forEach((value, key) => parameters[key] = value) try { this.searchParams.validate(parameters, OPTIONS) } catch (error) { builder.record(error, 'searchParams') } } return builder.assert(url) } } export function urlFactory(constraints: URLConstraints): URLValidator { return new URLValidator(constraints) } /** Validate URLs and convert them to `URL` instances. */ export const url = makeValidatorFactory(new URLValidator(), urlFactory)