This repository has been archived on 2021-06-02. You can view files and clone it, but cannot push or open issues or pull requests.
RealtimeDB-OLD/src/rules/parser.ts

350 lines
8.7 KiB
TypeScript

import { Token } from "./tokenise";
export interface Node {
type: string;
idx: number;
}
export interface PathStatement extends Node {
type: "path";
segments: (string | { type: "variable"; name: string })[];
}
export interface ValueStatement extends Node {
type: "value";
isNull: boolean;
isTrue: boolean;
isFalse: boolean;
isNumber: boolean;
isString: boolean;
isVariable: boolean;
value?: any;
}
export type Operators = "&&" | "||" | "==" | "<=" | ">=" | "!=" | ">" | "<";
export interface Expression extends Node {
type: "expression";
left: ValueStatement | Expression;
operator: Operators;
right: ValueStatement | Expression;
}
export type Operations = "read" | "write" | "list"; // | "update" | "create" | "delete" | "list";
export interface AllowStatement extends Node {
type: "permission";
operations: Operations[];
condition: Expression | ValueStatement;
}
export interface MatchStatement extends Node {
type: "match";
path: PathStatement;
matches: MatchStatement[];
rules: AllowStatement[];
}
export interface ServiceStatement extends Node {
type: "service";
name: string;
matches: MatchStatement[];
}
export class ParserError extends Error {
token: Token;
constructor(message: string, token: Token) {
super(message);
this.token = token;
}
}
export default function parse(tokens: Token[]) {
const tokenIterator = tokens[Symbol.iterator]();
let currentToken: Token = tokenIterator.next().value;
let nextToken: Token = tokenIterator.next().value;
const eatToken = (value?: string) => {
if (value && value !== currentToken.value) {
throw new ParserError(
`Unexpected token value, expected '${value}', received '${currentToken.value}'`,
currentToken
);
}
let idx = currentToken.startIdx;
currentToken = nextToken;
nextToken = tokenIterator.next().value;
return idx;
};
const eatText = (): [string, number] => {
checkTypes("text");
let val = currentToken.value;
let idx = currentToken.startIdx;
eatToken();
return [val, idx];
};
const eatNumber = (): number => {
checkTypes("number");
let val = Number(currentToken.value);
if (Number.isNaN(val)) {
throw new ParserError(
`Value cannot be parsed as number! ${currentToken.value}`,
currentToken
);
}
eatToken();
return val;
};
const checkTypes = (...types: string[]) => {
if (types.indexOf(currentToken.type) < 0) {
throw new ParserError(
`Unexpected token value, expected ${types.join(" | ")}, received '${
currentToken.value
}'`,
currentToken
);
}
};
const parsePathStatement = (): PathStatement => {
const segments: (string | { name: string; type: "variable" })[] = [];
const idx = currentToken.startIdx;
let next = currentToken.type === "slash";
while (next) {
eatToken("/");
if (currentToken.type === "curly_open" && nextToken.type === "text") {
eatToken("{");
const [name] = eatText();
segments.push({
type: "variable",
name,
});
eatToken("}");
} else if (currentToken.type === "text") {
const [name] = eatText();
segments.push(name);
}
next = currentToken.type === "slash";
}
return {
type: "path",
idx,
segments,
};
};
const parseValue = (): ValueStatement => {
const idx = currentToken.startIdx;
let isTrue = false;
let isFalse = false;
let isNull = false;
let isVariable = false;
let isNumber = false;
let isString = false;
let value: any = undefined;
if (currentToken.type === "keyword") {
if (currentToken.value === "true") isTrue = true;
else if (currentToken.value === "false") isFalse = true;
else if (currentToken.value === "null") isNull = true;
else {
throw new ParserError(
`Invalid keyword at this position ${currentToken.value}`,
currentToken
);
}
eatToken();
} else if (currentToken.type === "string") {
isString = true;
value = currentToken.value.slice(1, currentToken.value.length - 1);
eatToken();
} else if (currentToken.type === "number") {
isNumber = true;
value = eatNumber();
} else if (currentToken.type === "text") {
isVariable = true;
[value] = eatText();
} else {
throw new ParserError(
`Expected value got ${currentToken.type}`,
currentToken
);
}
return {
type: "value",
isFalse,
isNull,
isNumber,
isString,
isTrue,
isVariable,
value,
idx,
};
};
const parseCondition = (): Expression | ValueStatement => {
// let running = true;
let res: Expression | ValueStatement;
let left: Expression | ValueStatement | undefined;
// while (running) {
const idx = currentToken.startIdx;
if (!left) {
if (currentToken.type === "bracket_open") {
eatToken("(");
left = parseCondition();
eatToken(")");
} else {
left = parseValue();
}
}
if (currentToken.type === "comparison_operator") {
const operator = currentToken.value;
eatToken();
let right: Expression | ValueStatement;
let ct = currentToken; //Quick hack because of TypeScript
if (ct.type === "bracket_open") {
eatToken("(");
right = parseCondition();
eatToken(")");
} else {
right = parseValue();
}
res = {
type: "expression",
left,
right,
operator: operator as Operators,
idx,
};
} else if (currentToken.type === "logic_operator") {
const operator = currentToken.value;
eatToken();
const right = parseCondition();
res = {
type: "expression",
left,
operator: operator as Operators,
right,
idx,
};
} else {
res = left;
}
// let ct = currentToken;
// if (
// ct.type === "comparison_operator" ||
// ct.type === "logic_operator"
// ) {
// left = res;
// } else {
// running = false;
// }
// }
return res;
};
const parsePermissionStatement = (): AllowStatement => {
const idx = eatToken("allow");
const operations: Operations[] = [];
let next = currentToken.type !== "colon";
while (next) {
const [operation] = eatText();
operations.push(operation as Operations);
if (currentToken.type === "comma") {
next = true;
eatToken(",");
} else {
next = false;
}
}
eatToken(":");
eatToken("if");
const condition = parseCondition();
eatToken(";");
return {
type: "permission",
idx,
operations,
condition,
};
};
const parseMatchStatement = (): MatchStatement => {
const idx = eatToken("match");
const path = parsePathStatement();
eatToken("{");
const matches: MatchStatement[] = [];
const permissions: AllowStatement[] = [];
while (currentToken.type !== "curly_close") {
if (currentToken.value === "match") {
matches.push(parseMatchStatement());
} else if (currentToken.value === "allow") {
permissions.push(parsePermissionStatement());
} else {
throw new ParserError(
`Unexpected token value, expected 'match' or 'allow', received '${currentToken.value}'`,
currentToken
);
}
}
eatToken("}");
return {
type: "match",
path,
idx,
matches,
rules: permissions,
};
};
const parseServiceStatement = (): ServiceStatement => {
const idx = eatToken("service");
let [name] = eatText();
eatToken("{");
const matches: MatchStatement[] = [];
while (currentToken.value === "match") {
matches.push(parseMatchStatement());
}
eatToken("}");
return {
type: "service",
name: name,
idx,
matches,
};
};
const nodes: ServiceStatement[] = [];
while (currentToken) {
nodes.push(parseServiceStatement());
}
return nodes;
}