UNPKG

scim2-filter

Version:
163 lines (155 loc) 4.46 kB
import { Filter, Compare, NotFilter, Suffix, ValuePath } from "."; type TokenType = "Number" | "Quoted" | "Blacket" | "Word" | "EOT"; const EOT = { type: "EOT" as TokenType, literal: "" }; export type Token = { type: TokenType; literal: string; }; export function tokenizer(f: string): Token[] { const ret: Token[] = []; let rest = f; const patterns = /^(?:(\s+)|(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)|("(?:[^"]|\\.|\n)*")|([[\]()])|(\w[-\w\._:\/%]*))/; let n; while ((n = patterns.exec(rest))) { if (n[1] || n[0].length === 0) { // } else if (n[2]) { ret.push({ literal: n[2], type: "Number" }); } else if (n[3]) { ret.push({ literal: n[3], type: "Quoted" }); } else if (n[4]) { ret.push({ literal: n[4], type: "Blacket" }); } else if (n[5]) { ret.push({ literal: n[5], type: "Word" }); } rest = rest.substring(n.index + n[0].length); } if (rest.length !== 0) { throw new Error(`unexpected token ${rest}`); } ret.push(EOT); return ret; } export class Tokens implements TokenList { i: number; private current: Token | undefined; getList() { return this.list.map((a, i) => i == this.i ? `[${a.literal}]` : a.literal ); } peek(): Token { return this.current || EOT; } constructor(private list: Token[]) { this.i = 0; this.current = this.list[this.i]; } forward(): TokenList { this.current = this.list[++this.i]; return this; } shift(): Token { const c = this.peek(); this.forward(); return c; } } interface TokenList { peek(): Token; forward(): TokenList; shift(): Token; } const cops = new Set(["eq", "ne", "co", "sw", "ew", "gt", "lt", "ge", "le"]); const sops = new Set(["pr"]); export function parseFilter(list: TokenList): Filter { return parseInxif(parseExpression(list), list, Precedence.LOWEST); } export function parseExpression(list: TokenList): Filter { const t = list.shift(); if (t.literal == "(") { const filter = parseFilter(list); const close = list.shift(); if (close.literal !== ")") { throw new Error( `Unexpected token [${close.literal}(${close.type})] expected ')'` ); } return filter; } else if (t.literal.toLowerCase() == "not") { return { op: "not", filter: parseFilter(list) } as NotFilter; } else if (t.type == "Word") { return readValFilter(t, list); } else { throw new Error(`Unexpected token ${t.literal} (${t.type})`); } } enum Precedence{ LOWEST = 1, OR = 2, AND = 3 } const PRECEDENCE : { [key:string]: Precedence }= { 'or':Precedence.OR, 'and':Precedence.AND } function parseInxif(left: Filter, list: TokenList, precede: Precedence): Filter { const op = list.peek().literal.toLowerCase(); const p = PRECEDENCE[op]; if(!p || precede >= p){ return left; } const filters = [left]; while (list.peek().literal.toLowerCase() === op) { let r = parseExpression(list.forward()); const rr = list.peek().literal.toLowerCase(); if(PRECEDENCE[rr] > p){ r = parseInxif(r, list, p); } filters.push(r); } return parseInxif({ op , filters } as Filter, list, precede); } function readValFilter(left: Token, list: TokenList): Filter { if (left.type !== "Word") { throw new Error(`Unexpected token ${left.literal} expected Word`); } const attrPath = left.literal; const t = list.shift(); const op = t.literal.toLowerCase(); if (cops.has(op)) { var compValue = parseCompValue(list); return { op, attrPath, compValue } as Compare; } else if (sops.has(op)) { return { op, attrPath } as Suffix; } else if (op == "[") { const valFilter = parseFilter(list); const close = list.shift(); if (close.literal !== "]") { throw new Error(`Unexpected token ${close.literal} expected ']'`); } return { op: "[]", attrPath, valFilter } as ValuePath; } else { throw new Error( `Unexpected token ${attrPath} ${t.literal} as valFilter operator` ); } } function parseCompValue(list: TokenList): Compare["compValue"] { const t = list.shift(); try { const v = JSON.parse(t.literal); if ( v === null || typeof v == "string" || typeof v == "number" || typeof v == "boolean" ) { return v; } else { throw new Error(`${t.literal} is ${typeof v} (un supported value)`); } } catch (e) { throw new Error(`[${t.literal}(${t.type})] is not json`); } }