import * as Leaf from "leaf-ts" import Criteria = Type.Topology.Condition.Criteria import Matches = Type.Topology.Condition.Matches import Match = Type.Topology.Condition.Match import CriteriaTemplate = Type.Topology.Condition.CriteriaTemplate import * as CoreLogic from "./coreLogic" export namespace Condition { export function empty(): Type.Topology.Condition.Criteria { return { ors: [{ ands: [], tag: "matches" }], tag: "criteria", } } export function never(): Type.Topology.Condition.CriteriaTemplate { return { ors: [{ ands: [], tag: "matches" }], tag: "criteriaTemplate", } } export function realize(template: CriteriaTemplate, data: T | T[]): Criteria { if (!template) return null return Condition.build(template, data) } export function and(...criterias: T[]): T { let tag = "criteria" // All null let allNone = true for (let item of criterias) { if (item) { allNone = false } } if (allNone) return null criterias = criterias.filter(item => !!item) if (criterias.length == 0) return Condition.empty() as any as T for (let cri of criterias) { if (cri.tag == "criteriaTemplate") { tag = "criteriaTemplate" break } } if (criterias.length == 1) return criterias[0] let result: T = { ors: [], tag: tag as any, } as any let first = criterias.shift() let second = criterias.shift() for (let firstOr of first.ors) { for (let secondOr of second.ors) { result.ors.push({ ands: [...firstOr.ands, ...secondOr.ands] }) } } return and(result, ...criterias) } export function criteriaToTemplateByPath(cri: Criteria): CriteriaTemplate { let template = Leaf.Util.clone(cri) as CriteriaTemplate template.tag = "criteriaTemplate" for (let ands of template.ors) { for (let match of ands.ands) { if (!match.path) { throw new Error(`Fail to convert criteria to template with match.path of null`) } delete match.value match.reference = match.path } } return template } export function criteriaToValue(cri: Criteria): any { let obj = {} for (let ands of cri.ors) { for (let match of ands.ands) { if (!match.path) { return match.value } obj[match.path] = match.value } } return CoreLogic.reconstructObjectMap(obj) } export function or(criterias: Criteria[]): Criteria { let result: Criteria = { ors: [], tag: "criteria" } if (criterias.length == 0) { return result } criterias = criterias.filter(item => !!item) if (criterias.length == 0) return null for (let item of criterias) { result.ors.push(...item.ors) } return result } const numericType = ["lt", "gt", "lte", "gte"] export function build(cri: CriteriaTemplate, obj: any = {}): Criteria { let criteria = Leaf.Util.clone(cri) as Criteria criteria.ors = criteria.ors.filter(conds => { conds.ands.map(cond => { if (cond.reference) { let value = Leaf.Util.getObjectValue(obj, cond.reference) if (!Leaf.Util.isEmptyValue(value)) { if (numericType.indexOf(cond.type) >= 0 && typeof value !== "number") { throw new Error(`Invalid condition: ${cond.reference} must be a number`) } else if (cond.type == "reg" && !(value instanceof RegExp)) { throw new Error(`Invalid condition: ${cond.reference} must be a RegExp`) } else if (cond.type == "in" && !(Array.isArray(value))) { value = [value] } } if (!Leaf.Util.isEmptyValue(value)) { cond.value = value } if (cond.type == "eq" && Array.isArray(cond.value)) { return { type: "in", value: cond.value, optional: cond.optional } } } if (Leaf.Util.isEmptyValue(cond.value)) { if (cond.optional) { return null } console.error("Invalid condition", cond) throw new Error(`Critical condition ${cond.reference} of type $${cond.type} missing value`) } return cond }).filter(item => !!item) if (conds.ands.length == 0) return false return true }) criteria.tag = "criteria" return criteria } export function test(condition: Criteria | Matches | Match, data: any): boolean { if (!condition) return true if (condition.tag == "criteria") return testCriteria(condition, data) if (condition.tag == "matches") return testMatches(condition, data) if (condition.tag == "match") return testMatch(condition, data) if (condition["tag"] == "criteriaTemplate") { throw new Error("Can't test criteria template") } return false } export function testCriteria(cri: Criteria, data: any): boolean { if (!cri) return true for (let item of cri.ors) { if (testMatches(item, data)) return true } return false } export function testMatches(matches: Matches, data: any): boolean { for (let item of matches.ands) { if (!testMatch(item, data)) { return false } } return true } export function testMatch(match: Match, data: any): boolean { let value if (match.path) { value = Leaf.Util.getObjectValue(data, match.path || null) } else { value = data } if (match.type == "eq") { return value === match.value } else if (match.type == "lte") { return value <= match.value } else if (match.type == "gte") { return value >= match.value } else if (match.type == "lt") { return value < match.value } else if (match.type == "gt") { return value > match.value } else if (match.type == "in") { return match.value.indexOf(value) >= 0 } else if (match.type == "reg") { return match.value.test(value) } else if (match.type == "wildcard") { throw new Error("Wildcard not supported") } return false } export function subInplace(criteria: Criteria | CriteriaTemplate, path: string) { for (let ors of criteria.ors) { for (let match of ors.ands) { if (!match.path) match.path = path else match.path = path + "." + match.path } } } } function prepare(target, key: string, descriptor: PropertyDescriptor) { let fn = descriptor.value descriptor.value = function () { let obj = this if (obj.isSealed) throw new Error("Can't modify criteria after sealed") obj.ensureCurrentConditions() let result = fn.apply(this, arguments) obj.currentOptional = false return result } } export class CriteriaBuilder { tb = new CriteriaTemplateBuilder sub(path: string) { this.tb.seal() Condition.subInplace(this.tb.criteria, path) } path(path: string) { this.tb.path(path) return this } lt(value: number) { this.tb.lt(null, value) return this } gt(value: number) { this.tb.gt(null, value) return this } lte(value: number) { this.tb.lte(null, value) return this } gte(value: number) { this.tb.gte(null, value) return this } eq(value: number | string | boolean) { this.tb.eq(null, value) return this } in(value: string[] | number[]) { this.tb.in(null, value) return this } reg(value: RegExp) { this.tb.reg(null, value) return this } build() { this.tb.seal() let criteria = Leaf.Util.clone(this.tb.criteria) as Type.Topology.Condition.Criteria criteria.tag = "criteria" return criteria } } export class CriteriaTemplateBuilder { currentPath: string = null currentConditions: Type.Topology.Condition.Matches = null criteria: Type.Topology.Condition.CriteriaTemplate = { ors: [], tag: "criteriaTemplate" } currentOptional = false optional() { this.currentOptional = true return this } getTemplate() { this.seal() return this.criteria } private ensureCurrentConditions() { if (!this.currentConditions) { this.currentConditions = this.createEmptyConditions() } } private createEmptyConditions() { return { ands: [] } } private isEmptyMatches(conditions: Type.Topology.Condition.Matches) { return !conditions || !conditions.ands || conditions.ands.length == 0 } path(path: string) { this.currentPath = path return this } @prepare gt(reference: string, value?: number) { this.currentConditions.ands.push({ path: this.currentPath, type: "gt", reference, value, optional: this.currentOptional }) return this } @prepare lt(reference: string, value?: number) { this.currentConditions.ands.push({ path: this.currentPath, type: "lt", reference, value, optional: this.currentOptional }) return this } @prepare lte(reference: string, value?: number) { this.currentConditions.ands.push({ path: this.currentPath, type: "lte", reference, value, optional: this.currentOptional }) return this } @prepare gte(reference: string, value?: number) { this.currentConditions.ands.push({ path: this.currentPath, type: "gte", reference, value, optional: this.currentOptional }) return this } @prepare eq(reference: string, value?: number | string | boolean) { this.currentConditions.ands.push({ path: this.currentPath, type: "eq", reference, value, optional: this.currentOptional }) return this } @prepare in(reference, value?: string[] | number[]) { this.currentConditions.ands.push({ path: this.currentPath, type: "in", reference, value, optional: this.currentOptional }) return this } or() { if (this.currentConditions) this.criteria.ors.push(this.currentConditions) this.currentConditions = null } @prepare reg(reference: string, value?: RegExp) { this.currentConditions.ands.push({ path: this.currentPath, type: "reg", reference, value, optional: this.currentOptional }) return this } @prepare wildcard() { throw new Error("Not implemented") } isSealed = false seal() { if (this.currentConditions) this.criteria.ors.push(this.currentConditions) this.currentConditions = null this.isSealed = true return this } private numericType = ["lt", "gt", "lte", "gte"] build(obj: any): Type.Topology.Condition.Criteria { if (!this.isSealed) { this.seal() } let criteria = Leaf.Util.clone(this.criteria) as Type.Topology.Condition.Criteria if (!obj) { return criteria } for (let conds of criteria.ors) { conds.ands.filter(cond => { if (cond.reference) { let value = Leaf.Util.getObjectValue(obj, cond.reference) if (this.numericType.indexOf(cond.type) >= 0 && typeof value !== "number") { throw new Error(`Invalid condition: ${cond.reference} must be a number`) } else if (cond.type == "reg" && !(value instanceof RegExp)) { throw new Error(`Invalid condition: ${cond.reference} must be a RegExp`) } cond.value = value || cond.value } if (Leaf.Util.isEmptyValue(cond.value)) { if (cond.optional) { return false } throw new Error(`Critical condition ${cond.reference} of type $${cond.type} missing value`) } return true }) for (let cond of conds.ands) { } } criteria.tag = "criteria" return criteria } }