first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
/**
* CEL (Common Expression Language) parsing and evaluation helpers.
*
* Only handles the subset of CEL actually used in routing rule conditions:
* equality/inequality, startsWith, contains, in-list, header access, and
* simple numeric comparisons.
*/
/**
* Evaluate a single normalised CEL clause against a resolved variable map.
* Only handles simple equality/inequality patterns (field == "v", "v" == field,
* field != "v", "v" != field). Returns null when too complex to evaluate.
*/
export function evalChainCondition(cond: string, vars: Record<string, string>): boolean | null {
const s = cond.trim();
let m: RegExpMatchArray | null;
// Simple equality: field == "v" or "v" == field
m = s.match(/^(\w+)\s*==\s*["']([^"']*)["']$/);
if (m && m[1] in vars) return vars[m[1]] === m[2];
m = s.match(/^["']([^"']*)['"]\s*==\s*(\w+)$/);
if (m && m[2] in vars) return vars[m[2]] === m[1];
// Inequality: field != "v" or "v" != field
m = s.match(/^(\w+)\s*!=\s*["']([^"']*)["']$/);
if (m && m[1] in vars) return vars[m[1]] !== m[2];
m = s.match(/^["']([^"']*)['"]\s*!=\s*(\w+)$/);
if (m && m[2] in vars) return vars[m[2]] !== m[1];
// startsWith: field.startsWith("prefix")
m = s.match(/^(\w+)\.startsWith\(["']([^"']*)["']\)$/);
if (m && m[1] in vars) return vars[m[1]].startsWith(m[2]);
// contains: field.contains("sub")
m = s.match(/^(\w+)\.contains\(["']([^"']*)["']\)$/);
if (m && m[1] in vars) return vars[m[1]].includes(m[2]);
// in list: field in ["a","b","c"]
m = s.match(/^(\w+)\s+in\s+\[([^\]]*)\]$/);
if (m && m[1] in vars) {
const items = m[2].split(",").map((x) => x.trim().replace(/^["']|["']$/g, ""));
return items.includes(vars[m[1]]);
}
// headers["key"] == "value"
m = s.match(/^headers\[["']([^"']*)["']\]\s*==\s*["']([^"']*)["']$/);
if (m) {
const hVal = vars[`headers.${m[1]}`] ?? vars[`header_${m[1]}`];
if (hVal !== undefined) return hVal === m[2];
}
// Numeric comparisons: field >= n, field <= n, field > n, field < n
m = s.match(/^(\w+)\s*(>=|<=|>|<)\s*(\d+(?:\.\d+)?)$/);
if (m && m[1] in vars) {
const lv = parseFloat(vars[m[1]]);
const rv = parseFloat(m[3]);
if (!isNaN(lv)) {
if (m[2] === ">") return lv > rv;
if (m[2] === "<") return lv < rv;
if (m[2] === ">=") return lv >= rv;
if (m[2] === "<=") return lv <= rv;
}
}
return null; // too complex — skip
}
function isWrappedInParens(s: string): boolean {
if (!s.startsWith("(") || !s.endsWith(")")) return false;
let d = 0;
for (let i = 0; i < s.length; i++) {
if (s[i] === "(") d++;
else if (s[i] === ")") d--;
if (d === 0 && i < s.length - 1) return false;
}
return true;
}
function splitOn(expr: string, op: "&&" | "||"): string[] {
const trimmed = expr.trim();
const s = isWrappedInParens(trimmed) ? trimmed.slice(1, -1) : trimmed;
const parts: string[] = [];
let depth = 0,
current = "";
for (let i = 0; i < s.length; i++) {
const ch = s[i];
if (ch === "(" || ch === "[") depth++;
else if (ch === ")" || ch === "]") depth--;
else if (depth === 0 && s.slice(i, i + 2) === op) {
const p = current.trim();
if (p) parts.push(p);
current = "";
i++;
continue;
}
current += ch;
}
const last = current.trim();
if (last) parts.push(last);
if (parts.length < 2) return [expr.trim()];
return parts;
}
/** Cartesian product of two arrays of string arrays. */
function cartesian(a: string[][], b: string[][]): string[][] {
const result: string[][] = [];
for (const x of a) for (const y of b) result.push([...x, ...y]);
return result;
}
/** Expand a CEL string into one or more condition lists, fanning out on OR.
* Handles nested disjunctions such as `a && (b || c)` → [["a","b"],["a","c"]].
*/
export function expandCEL(cel: string): string[][] {
const trimmed = cel?.trim() || "";
if (!trimmed) return [[]];
// OR has lower precedence than AND → split on || first (outer level)
const orBranches = splitOn(trimmed, "||");
const result: string[][] = [];
for (const branch of orBranches) {
const andParts = splitOn(branch.trim(), "&&")
.map((p) => p.trim())
.filter(Boolean);
if (!andParts.length) {
result.push([branch.trim()]);
continue;
}
// For each AND part, check if it is a parenthesized OR — expand recursively
// and Cartesian-product with the accumulated combinations so far.
let combinations: string[][] = [[]];
for (const part of andParts) {
if (isWrappedInParens(part)) {
const inner = part.slice(1, -1).trim();
const innerBranches = splitOn(inner, "||");
if (innerBranches.length > 1) {
const innerExpanded = innerBranches.flatMap((b) => expandCEL(b.trim()));
combinations = cartesian(combinations, innerExpanded);
continue;
}
}
combinations = combinations.map((c) => [...c, part]);
}
result.push(...combinations);
}
return result.length ? result : [[]];
}
/**
* Normalize a CEL condition token for trie key comparison.
* Collapses whitespace around operators so "a == b" and "a==b" are the same key.
*/
export function normalizeCond(cond: string): string {
return cond
.trim()
.replace(/\s*(==|!=|>=|<=|>|<)\s*/g, (_, op) => ` ${op} `)
.replace(/\s+/g, " ");
}