UNPKG

@lucaspaganini/value-objects

Version:

TypeScript first validation and class creation library

160 lines (145 loc) 5.84 kB
import { isDefined, isLeft, isNotNumber } from '../utils' import { LogicError, MaxLengthError, MinLengthError, MinSizeError, NotIntegerError, RawTypeError, VOError, } from './errors' import { makeFromRawInit } from './functions' import { ValueObject, ValueObjectContructor, VOCRawInit, VORaw } from './value-object' export interface VOArrayOptions { /** * Minimum inclusive length. * Can't be less than zero or bigger than `maxLength` */ minLength?: number /** * Maximum inclusive length. * Can't be less than zero or smaller than `minLength` */ maxLength?: number /** * Maximum inclusive errors to acumulate before throwing. * Can't be less than zero. * @default 1 */ maxErrors?: number } export interface VOArrayInstance<VO extends ValueObject<any>> { toArray(): Array<VO> valueOf(): Array<VORaw<VO>> } export interface VOArrayConstructor<VOC extends ValueObjectContructor> { new (rawInit: Array<VOCRawInit<VOC>>): VOArrayInstance<InstanceType<VOC>> } /** * Function to create an array wrapper over a given value object constructor. * Useful if you already have a class and you need an array of it. * * @template VOC Value object constructor to make an array wrapper of. * @param VOC Value object constructor to make an array wrapper of. * @param options Customizations for the returned class constructor. * @return Class constructor that accepts an array of what the given * value object constructor would accept. Calling {@link VOArrayInstance.valueOf} * calls `valueOf()` for all it's inner instances and returns an array of the results. * * @example * ```typescript * class Email extends VOString({ ... }) { * getHost(): string { ... } * } * * class EmailsArray extends VOArray(Email) {} * new EmailsArray(['me@lucaspaganini.com', 'test@example.com']); // OK * new EmailsArray([123]); // Compilation error: Expects Array<string> * new EmailsArray(['invalid-email']); // Runtime error: Value doesn't match pattern * * const emails = new EmailsArray(['me@lucaspaganini.com', 'test@example.com']); * emails.valueOf(); // ['me@lucaspaganini.com', 'test@example.com'] * emails.toArray(); // [Email, Email] * emails.toArray().map((email) => email.getHost()); // ['lucaspaganini.com', 'example.com'] * ``` * * @example * ```typescript * class Test { * constructor(shouldThrow: boolean) { * if (shouldThrow) throw Error('I was instructed to throw'); * } * } * new Test(false); // OK * new Test(true); // Runtime error: I was instructed to throw * * class TestsArray extends VOArray(Test, { * minLength: 1, * maxLength: 5, * maxErrors: 2 * }) {} * new TestsArray([false]); // OK * new TestsArray([]); // Runtime error: Too short * new TestsArray([false, false, false, false, false, false]); // Runtime error: Too long * new TestsArray([true, true, true, true]); // Runtime error: ["I was instructed to throw", "I was instructed to throw"] * ``` */ export const VOArray = <VOC extends ValueObjectContructor>( VOC: VOC, options: VOArrayOptions = {}, ): VOArrayConstructor<VOC> => { if (isDefined(options.minLength)) { if (isNotNumber(options.minLength)) throw new RawTypeError('number', typeof options.minLength, 'options.minLength') if (!Number.isInteger(options.minLength)) throw new NotIntegerError(options.minLength, 'options.minLength') if (options.minLength < 0) throw new MinSizeError(options.minLength, 0) } if (isDefined(options.maxLength)) { if (isNotNumber(options.maxLength)) throw new RawTypeError('number', typeof options.maxLength, 'options.maxLength') if (!Number.isInteger(options.maxLength)) throw new NotIntegerError(options.maxLength, 'options.maxLength') if (options.maxLength < 0) throw new MinSizeError(options.maxLength, 0) } if (isDefined(options.minLength) && isDefined(options.maxLength)) { if (options.minLength > options.maxLength) throw new LogicError('options.minLength should not be bigger than options.maxLength') } if (isDefined(options.maxErrors)) { if (isNotNumber(options.maxErrors)) throw new RawTypeError('number', typeof options.maxErrors, 'options.maxErrors') if (!Number.isInteger(options.maxErrors)) throw new NotIntegerError(options.maxErrors, 'options.maxErrors') if (options.maxErrors < 0) throw new MinSizeError(options.maxErrors, 0) } const maxErrors = options.maxErrors ?? 1 return class { private _valueObjects: Array<InstanceType<VOC>> = [] constructor(raw: Array<VOCRawInit<VOC>>) { if (!Array.isArray(raw)) throw new RawTypeError('Array<Raw>', typeof raw) if (options.minLength !== undefined && raw.length < options.minLength) throw new MinLengthError(options.minLength, raw.length) if (isDefined(options.maxLength) && raw.length > options.maxLength) throw new MaxLengthError(options.maxLength, raw.length) const errors: Array<Error> = [] const fromRaw = makeFromRawInit(VOC) for (const [_i, _raw] of Object.entries(raw)) { const index = parseInt(_i) const either = fromRaw(_raw) if (isLeft(either)) { const errorsWithIndex = either.left.map(e => { if (VOError.is(e)) e.path.push(index) return e }) errors.push(...errorsWithIndex) if (errors.length >= maxErrors) throw errors } else { this._valueObjects.push(either.right) } } if (errors.length > 0) throw errors if (raw.length !== this._valueObjects.length) throw new VOError(`Unknown error`) } valueOf(): Array<VORaw<InstanceType<VOC>>> { return this._valueObjects.map(vo => vo.valueOf()) } toArray(): Array<InstanceType<VOC>> { return (<Array<InstanceType<VOC>>>[]).concat(this._valueObjects) } } }