import Schema from "./Schema" import { SchemaJSON, ValidationError, ValidationResult } from "./types" export default abstract class SchemaItem { private invalidError = 'the field is invalid' /** * list of attributes for custom works */ public readonly attributes: Array = [] public attrs(...attributes: Array) { this.attributes.concat(attributes) return this } public setInvalidError(err: string): this { this.invalidError = err return this } public clone(): this { return Schema.fromJSON(this.toJSON()) as this } /** * schemas implementing unwrap can return their child component (mostly the value) * * ex: s.record(s.number(), s.string()) returns s.string() * * es2: s.nullable(s.string()) returns s.string() */ public unwrap?(): SchemaItem /** * Pre process the variable for various reasons * * It will execute every pre-process sequentially in order of being added * * note: The type of the final Pre-Process MUST be valid */ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents public preProcess: Array<(input: unknown) => Type | unknown> = [] /** * post process the variable after it being validated * * it will execute each post-processes sequentially * * note: the type of the final post-process MUST be valid */ public postProcess: Array<(input: Type) => Type> = [] /** * keep public ? */ public validations: Array<{ fn: (input: Type) => boolean error?: string | undefined }> = [] public savedCalls: Array<{ name: string, args: Array | IArguments | undefined }> = [] private readonly items?: Array public constructor(items?: Array | IArguments) { if (items && items.length > 0) { this.items = Array.isArray(items) ? items : Array.from(items) } } public parse(input: unknown, options?: { fast?: boolean }): ValidationResult { // pre process the variable for (const preProcess of this.preProcess) { input = preProcess(input) } // validate that the pre process handled correctly the variable if (!this.isOfType(input)) { return { valid: false, errors: [{ message: 'invalid type' }] } } // run validation checks const errors: Array = [] for (const validation of this.validations) { if (!validation.fn(input)) { // add the error to the list errors.push({ message: validation.error ?? this.invalidError }) // if the system should be fast, stop checking for other errors if (options?.fast) { return { valid: false, object: input as (Type extends object ? Partial : Type) | undefined, errors: errors } } } } // return with errors if (errors.length > 0) { return { valid: false, object: input as (Type extends object ? Partial : Type) | undefined, errors: errors } } // post-process the variable for (const postProcess of this.postProcess) { input = postProcess(input as Type) } // validate that the pre process handled correctly the variable if (!this.isOfType(input)) { throw new Error('Post process error occured :(') } return { valid: true, object: input } } public toJSON(): SchemaJSON { return { i: this.constructor.name, a: this.attributes.length > 0 ? this.attributes : undefined, c: this.items?.map((it) => it instanceof SchemaItem ? it.toJSON() : it), f: this.savedCalls .map((it) => (it.args ? { n: it.name, a: Array.from(it.args) } : { n: it.name })) } } protected addValidation(fn: ((input: Type) => boolean) | { fn: (input: Type) => boolean, error?: string }, error?: string) { this.validations.push(typeof fn === 'function' ? { fn, error } : fn) return this } protected addPreProcess(fn: (input: unknown) => Type | unknown) { this.preProcess.push(fn) return this } protected addPostProcess(fn: (input: Type) => Type) { this.postProcess.push(fn) return this } public abstract isOfType(input: unknown): input is Type }