namespace Leaf { export const ValidatorErrors = ErrorDoc.build({ Invalid: { path: "path.to.invalid.field", code: null } }) export type Stringable = { toString(): string } export type ComplexData = any export class Validator { static currentReason: Stringable = null static currentCode = null static create(name: string) { return new Validator(name) } constructor(public currentRouteName: string, public readonly parent: PV = null) { //return new Proxy(this, { // get: (obj, prop) => { // let blacklist = ["name"] // let whitelist = ["currentReason", "currentCode", "elementValidator"] // if (whitelist.indexOf(prop.toString()) >= 0) return obj[prop] // return prop in obj && blacklist.indexOf(prop as string) < 0 && obj[prop] || this.field(prop as string) // } //}) } clone(parent = this.parent): this { let validator = new Validator(this.currentRouteName, parent) validator.children = this.children.map(item => item.clone(validator as any)) validator.checkers = this.checkers.map(item => item.clone()) return validator as any } private checkers: Checker[] = [] private children: Validator[] = [] field(key: K): Validator { for (let child of this.children) { if (child.currentRouteName == key) { return child as Validator } } let child = new Validator(key as string, this as any) this.children.push(child as any) return child } with(key: K, handler?: (validator: Validator) => void) { handler(this.field(key)) return this } use(validator: Validator) { this.children = validator.children.map(child => child.clone(this)) this.checkers = validator.checkers.map(item => item.clone()) return this } private elementValidator: Validator each(): Validator { this.elementValidator return null } check(data, root = data, parent = null) { try { for (let checker of this.checkers) { checker.check(data, root, parent) } } catch (e) { let path = this.getRoute().join(".") throw new ValidatorErrors.Invalid(path + " " + e.message, { path, code: e.code }) } if (this.elementValidator) { if (Array.isArray(data)) { let i = 0 for (let child of data) { this.elementValidator.currentRouteName = i.toString() this.elementValidator.check(child, root, data) i++ } } else { for (let key of Object.keys(data || {})) { this.elementValidator.currentRouteName = key this.elementValidator.check(data[key], root, data) } } } for (let child of this.children) { if (data !== null && typeof data !== "undefined") { child.check(data[child.currentRouteName], root, data) } else { child.check(null, root, data) } } } reason(reason: Stringable) { Validator.currentReason = reason return this } code(code: any) { Validator.currentCode = code return this } private addChecker(c) { c.reason = Validator.currentReason c.code = Validator.currentCode this.checkers.push(c) return this } private getRoute(index?) { let routes = [] if (this.parent) routes.push(...this.parent.getRoute()) routes.push(this.currentRouteName) if (typeof index == "number" || typeof index == "string") { routes.push(index) } return routes } string() { return this.addChecker(new StringTypeChecker()) } int() { return this.addChecker(new IntTypeChecker()) } number() { return this.addChecker(new NumberTypeChecker()) } has() { return this.addChecker(new HasChecker()) } lt(num: number) { return this.addChecker(new LtChecker(num)) } gt(num: number) { return this.addChecker(new GtChecker(num)) } lte(num: number) { return this.addChecker(new LteChecker(num)) } gte(num: number) { return this.addChecker(new GteChecker(num)) } reg(reg: RegExp) { return this.addChecker(new RegCheck(reg)) } pass(fn: (data: T, root?: R) => void) { return this.addChecker(new PassChecker(fn)) } } abstract class Checker { constructor(public readonly config: any = null) { } reason: string = null code = null abstract check(data: any, root?: any, parent?: any) clone() { let Cons = this.constructor as any let checker = new Cons() // We don't deep copy it, since no one should be able to modify it unless INTENTED. checker.config = this.config checker.reason = this.reason checker.code = this.code return checker } } class StringTypeChecker extends Checker { check(data) { if (typeof data != "string") throw new ValidatorErrors.Invalid(this.reason || "Not a string") } } class IntTypeChecker extends Checker { constructor() { super() } check(data) { if (typeof data != "number") throw new ValidatorErrors.Invalid(this.reason || "Not a int") if (Math.floor(data) !== data || isNaN(data)) throw new ValidatorErrors.Invalid(this.reason || "Not a int") } } class NumberTypeChecker extends Checker { constructor() { super() } check(data) { if (typeof data != "number") throw new ValidatorErrors.Invalid(this.reason || "Not a number") } } class HasChecker extends Checker { check(data) { if (typeof data == "undefined" || data === null) { throw new Error("Not exists") } } } class GtChecker extends Checker { config: number check(data) { if (data > this.config) return throw new ValidatorErrors.Invalid(this.reason || `${data} not greate ${this.config}`) } } class LtChecker extends Checker { config: number check(data) { if (data < this.config) return throw new ValidatorErrors.Invalid(this.reason || `${data} not less than ${this.config}`) } } class GteChecker extends Checker { config: number check(data) { if (data >= this.config) return throw new ValidatorErrors.Invalid(this.reason || `${data} not greate-or-equal than ${this.config}`) } } class LteChecker extends Checker { config: number check(data) { if (data <= this.config) return throw new ValidatorErrors.Invalid(this.reason || `${data} not less-or-equal than ${this.config}`) } } class PassChecker extends Checker { config: (data: any, root?: any, parent?: any) => void check(data, root, parent) { try { this.config(data, root, parent) } catch (e) { throw new ValidatorErrors.Invalid(e.message || this.reason) } } } class RegCheck extends Checker { config: RegExp check(data) { this.config.lastIndex = 0 if (this.config.test(data)) return throw new ValidatorErrors.Invalid(this.reason || `${data} fail to pass reg exp checks ${this.config.source}`) } } // TODO // 1. lt/lte (less than) // 2. gt/gte (greater than) // 3. eq (equal) // 4. is (===) // 5. match(reg exp) // 6. custom( given a function) // 7. string (type string check) // 8. number (type number check) // 9. exists // 10. in (one of given array) // 11. int (is int) }