schema/SchemaItem.ts

165 lines
4.0 KiB
TypeScript

import Schema from "./Schema"
import { SchemaJSON, ValidationError, ValidationResult } from "./types"
export default abstract class SchemaItem<Type = any> {
private invalidError = 'the field is invalid'
/**
* list of attributes for custom works
*/
public readonly attributes: Array<string> = []
public attrs(...attributes: Array<string>) {
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<string> | IArguments | undefined }> = []
private readonly items?: Array<unknown>
public constructor(items?: Array<unknown> | IArguments) {
if (items && items.length > 0) {
this.items = Array.isArray(items) ? items : Array.from(items)
}
}
public parse(input: unknown, options?: { fast?: boolean }): ValidationResult<Type> {
// 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<ValidationError> = []
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> : Type) | undefined,
errors: errors
}
}
}
}
// return with errors
if (errors.length > 0) {
return {
valid: false,
object: input as (Type extends object ? Partial<Type> : 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
}