refactor: replace array-based expressions with typed class hierarchy

Replace the old Lisp-like array expression representation
(["AND", ["PARAM", ...], ...]) with a class hierarchy rooted in
an abstract Expression class (Literal, Parameter, Binary, Unary,
FunctionCall, Conditional). This gives compile-time type safety
to expression construction, evaluation, and pattern matching.

Key changes:
- New lib/common/expression.ts defines the class hierarchy with
  map/evaluate/toString methods
- Rewrite parser as hand-rolled recursive descent, removing the
  parsimmon dependency
- Extend Path with a colon separator to unify parameter identity
  and attributes into a single address space (e.g.
  "Device.WiFi.SSID.1.Name:writable"). The colon field tracks
  how many trailing segments are attribute qualifiers. PathSet
  rewritten with separate param/attr segment indexes
- Flatten device attribute representation in the UI to
  colon-delimited keys mirroring the new path syntax
This commit is contained in:
Zaid Humaimidi committed 2026-03-12 14:58:20 -07:00
1 parent 75908e624f
commit 6f3ce6f7c8
71 files changed
+4045 -3449

No files matched your search

+72 -56
View File
@@ -1,15 +1,12 @@
import * as vm from "node:vm";
import * as crypto from "node:crypto";
import * as config from "../config.ts";
import { collections } from "../db/db.ts";
import { convertOldPrecondition } from "../db/util.ts";
import * as logger from "../logger.ts";
import * as scheduling from "../scheduling.ts";
import * as expression from "../common/expression/util.ts";
import { parse } from "../common/expression/parser.ts";
import Expression, { Value } from "../common/expression.ts";
import {
Preset,
Expression,
Provisions,
VirtualParameters,
Files,
@@ -93,10 +90,10 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
}
const events = preset["events"] || {};
let precondition = true as Expression;
let precondition: Expression = new Expression.Literal(true);
if (preset["precondition"]) {
try {
precondition = parse(preset["precondition"]);
precondition = Expression.parse(preset["precondition"]);
} catch {
precondition = convertOldPrecondition(
JSON.parse(preset["precondition"]),
@@ -104,7 +101,7 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
}
// Simplify expression
precondition = expression.evaluate(precondition);
precondition = precondition.evaluate((e) => e);
}
const _provisions: Preset["provisions"] = [];
@@ -113,23 +110,42 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
for (const c of preset.configurations) {
switch (c.type) {
case "age":
_provisions.push(["refresh", c.name, +c.age]);
_provisions.push([
"refresh",
new Expression.Literal(c.name),
new Expression.Literal(+c.age),
]);
break;
case "value":
_provisions.push(["value", c.name, c.value]);
_provisions.push([
"value",
new Expression.Literal(c.name),
new Expression.Literal(c.value),
]);
break;
case "add_tag":
_provisions.push(["tag", c.tag, true]);
_provisions.push([
"tag",
new Expression.Literal(c.tag),
new Expression.Literal(true),
]);
break;
case "delete_tag":
_provisions.push(["tag", c.tag, false]);
_provisions.push([
"tag",
new Expression.Literal(c.tag),
new Expression.Literal(false),
]);
break;
case "provision":
_provisions.push([c.name, ...(c.args || [])]);
_provisions.push([
c.name,
...(c.args || []).map((a) => Expression.parse(a)),
]);
break;
case "add_object":
@@ -139,11 +155,19 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 1]);
_provisions.push([
"instances",
new Expression.Literal(p),
new Expression.Literal(1),
]);
for (const k in obj) {
if (!k.startsWith("_") && !(obj["_keys"].indexOf(k) !== -1))
_provisions.push(["value", `${p}.${k}`, obj[k]]);
_provisions.push([
"value",
new Expression.Literal(`${p}.${k}`),
new Expression.Literal(obj[k]),
]);
}
}
}
@@ -157,7 +181,11 @@ async function fetchPresets(): Promise<[string, Preset[]]> {
.map((k) => `${k}:${JSON.stringify(obj[k])}`)
.join(",");
const p = `${c.name}.[${alias}]`;
_provisions.push(["instances", p, 0]);
_provisions.push([
"instances",
new Expression.Literal(p),
new Expression.Literal(0),
]);
}
}
@@ -251,7 +279,7 @@ async function fetchConfig(): Promise<[string, Config]> {
for (const c of conf) {
// Evaluate expressions to simplify them
_config[c._id] = expression.evaluate(parse(c.value));
_config[c._id] = Expression.parse(c.value).evaluate((e) => e);
}
return [h, _config];
@@ -303,49 +331,37 @@ export function getFiles(revision: string): Files {
}
export function getConfig(
snapshotKey: string,
revision: string,
key: string,
context: Record<string, unknown>,
now: number,
cb?: (e: Expression) => Expression,
): string | number | boolean | null {
const snapshot = localCache.get(snapshotKey);
dflt: string,
fn: (e: Expression) => Expression.Literal,
): string;
export function getConfig(
revision: string,
key: string,
dflt: number,
fn: (e: Expression) => Expression.Literal,
): number;
export function getConfig(
revision: string,
key: string,
dflt: boolean,
fn: (e: Expression) => Expression.Literal,
): boolean;
export function getConfig(
revision: string,
key: string,
dflt: Value,
fn: (e: Expression) => Expression.Literal,
): Value {
const snapshot = localCache.get(revision);
if (!snapshot) throw new Error("Cache snapshot does not exist");
const oldOpts = {
"cwmp.downloadTimeout": "DOWNLOAD_TIMEOUT",
"cwmp.debug": "DEBUG",
"cwmp.retryDelay": "RETRY_DELAY",
"cwmp.sessionTimeout": "SESSION_TIMEOUT",
"cwmp.connectionRequestTimeout": "CONNECTION_REQUEST_TIMEOUT",
"cwmp.gpnNextLevel": "GPN_NEXT_LEVEL",
"cwmp.gpvBatchSize": "GPV_BATCH_SIZE",
"cwmp.cookiesPath": "COOKIES_PATH",
"cwmp.datetimeMilliseconds": "DATETIME_MILLISECONDS",
"cwmp.booleanLiteral": "BOOLEAN_LITERAL",
"cwmp.connectionRequestAllowBasicAuth":
"CONNECTION_REQUEST_ALLOW_BASIC_AUTH",
"cwmp.maxCommitIterations": "MAX_COMMIT_ITERATIONS",
"cwmp.deviceOnlineThreshold": "DEVICE_ONLINE_THRESHOLD",
"cwmp.udpConnectionRequestPort": "UDP_CONNECTION_REQUEST_PORT",
};
if (!(key in snapshot.config)) {
if (key in oldOpts) {
let id;
if (context?.["id"]) {
id = context["id"];
} else if (cb) {
id = cb(["PARAM", "DeviceID.ID"]);
if (Array.isArray(id)) id = null;
}
return config.get(oldOpts[key], id);
}
return null;
}
const v = expression.evaluate(snapshot.config[key], context, now, cb);
return Array.isArray(v) ? null : v;
const e = snapshot.config[key];
if (!e) return dflt;
const v = e.evaluate(fn).value;
if (typeof v !== typeof dflt) return dflt;
return v;
}
export function getConfigExpression(