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:
1 parent
75908e624f
commit
6f3ce6f7c8
71 files changed
+4045
-3449
No files matched your search
@@ -1,26 +1,41 @@
|
||||
import { ClosureComponent } from "mithril";
|
||||
import { m } from "../components.ts";
|
||||
import * as taskQueue from "../task-queue.ts";
|
||||
import { parse } from "../../lib/common/expression/parser.ts";
|
||||
import memoize from "../../lib/common/memoize.ts";
|
||||
import { icon } from "../tailwind-utility-components.ts";
|
||||
import { QueryResponse, evaluateExpression } from "../store.ts";
|
||||
import debounce from "../../lib/common/debounce.ts";
|
||||
import { Expression } from "../../lib/types.ts";
|
||||
import Expression, { Value } from "../../lib/common/expression.ts";
|
||||
import { FlatDevice } from "../../lib/ui/db.ts";
|
||||
|
||||
const memoizedParse = memoize(parse);
|
||||
import Path from "../../lib/common/path.ts";
|
||||
|
||||
function escapeRegExp(str): string {
|
||||
return str.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&");
|
||||
}
|
||||
|
||||
const keysByDepth: WeakMap<Record<string, unknown>, string[][]> = new WeakMap();
|
||||
interface Parameter {
|
||||
path: Expression.Parameter;
|
||||
value?: Value;
|
||||
writable?: boolean;
|
||||
object?: boolean;
|
||||
}
|
||||
|
||||
function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
||||
if (keysByDepth.has(device)) return keysByDepth.get(device);
|
||||
const res: string[][] = [];
|
||||
for (const key of Object.keys(device)) {
|
||||
const prepareParams = memoize((device: FlatDevice): Parameter[][] => {
|
||||
const map = new Map<string, Parameter>();
|
||||
|
||||
for (const [k, v] of Object.entries(device)) {
|
||||
const [param, attr] = k.split(":");
|
||||
let attrs = map.get(param);
|
||||
if (!attrs)
|
||||
map.set(
|
||||
param,
|
||||
(attrs = { path: new Expression.Parameter(Path.parse(param)) }),
|
||||
);
|
||||
attrs[attr || "value"] = v;
|
||||
}
|
||||
|
||||
const res: Parameter[][] = [];
|
||||
for (const [key, attrs] of map) {
|
||||
let count = 0;
|
||||
for (
|
||||
let i = key.lastIndexOf(".", key.length - 2);
|
||||
@@ -29,11 +44,10 @@ function orderKeysByDepth(device: Record<string, unknown>): string[][] {
|
||||
)
|
||||
++count;
|
||||
while (res.length <= count) res.push([]);
|
||||
res[count].push(key);
|
||||
res[count].push(attrs);
|
||||
}
|
||||
keysByDepth.set(device, res);
|
||||
return res;
|
||||
}
|
||||
});
|
||||
|
||||
interface Attrs {
|
||||
device: FlatDevice;
|
||||
@@ -51,9 +65,13 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
return {
|
||||
view: (vnode) => {
|
||||
const device = vnode.attrs.device;
|
||||
const allParams = prepareParams(device);
|
||||
|
||||
const limit =
|
||||
(evaluateExpression(vnode.attrs.limit, device) as number) || 100;
|
||||
let limit = 100;
|
||||
if (vnode.attrs.limit) {
|
||||
const l = evaluateExpression(vnode.attrs.limit, device);
|
||||
if (typeof l.value === "number") limit = l.value;
|
||||
}
|
||||
|
||||
const search = m(
|
||||
"input.appearance-none border-0 block w-full px-4 py-3 border-stone-300 placeholder-stone-500 text-stone-900 focus:ring-cyan-500 text-sm rounded-t-lg font-mono focus:ring-2",
|
||||
@@ -75,35 +93,36 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
re = new RegExp(keywords.map((s) => escapeRegExp(s)).join(".*"), "i");
|
||||
}
|
||||
|
||||
const filteredKeys: string[] = [];
|
||||
const allKeys = orderKeysByDepth(device);
|
||||
const filteredParams: Parameter[] = [];
|
||||
let count = 0;
|
||||
for (const keys of allKeys) {
|
||||
for (const keys of allParams) {
|
||||
let c = 0;
|
||||
for (const k of keys) {
|
||||
const p = device[k];
|
||||
const str = p.value?.[0] ? `${k} ${p.value[0]}` : k;
|
||||
const str = k.value ? `${k.path.toString()} ${k.value}` : k;
|
||||
if (re && !re.test(str)) continue;
|
||||
++c;
|
||||
if (count < limit) filteredKeys.push(k);
|
||||
if (count < limit) filteredParams.push(k);
|
||||
}
|
||||
count += c;
|
||||
}
|
||||
|
||||
filteredKeys.sort();
|
||||
filteredParams.sort((a, b) => {
|
||||
if (a.path < b.path) return -1;
|
||||
if (a.path > b.path) return 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const rows = filteredKeys.map((k) => {
|
||||
const p = device[k];
|
||||
const rows = filteredParams.map((p) => {
|
||||
const val = [];
|
||||
if (p.object === false) {
|
||||
if (p.value) {
|
||||
val.push(
|
||||
m(
|
||||
"parameter",
|
||||
Object.assign({ device: device, parameter: memoizedParse(k) }),
|
||||
Object.assign({ device: device, parameter: p.path }),
|
||||
),
|
||||
);
|
||||
} else if (p.object && p.writable) {
|
||||
if (instanceRegex.test(k)) {
|
||||
if (instanceRegex.test(p.path.toString())) {
|
||||
val.push(
|
||||
m(
|
||||
"button",
|
||||
@@ -112,8 +131,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "deleteObject",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
objectName: k,
|
||||
device: device["DeviceID.ID"] as string,
|
||||
objectName: p.path.toString(),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -133,8 +152,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "addObject",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
objectName: k,
|
||||
device: device["DeviceID.ID"] as string,
|
||||
objectName: p.path.toString(),
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -156,8 +175,8 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
onclick: () => {
|
||||
taskQueue.queueTask({
|
||||
name: "getParameterValues",
|
||||
device: device["DeviceID.ID"].value[0] as string,
|
||||
parameterNames: [k],
|
||||
device: device["DeviceID.ID"] as string,
|
||||
parameterNames: [p.path.toString()],
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -170,7 +189,10 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
|
||||
return m(
|
||||
"tr",
|
||||
m("td.pl-4 pr-2 py-2 truncate", m("long-text", { text: k })),
|
||||
m(
|
||||
"td.pl-4 pr-2 py-2 truncate",
|
||||
m("long-text", { text: p.path.toString() }),
|
||||
),
|
||||
m("td.pr-4 py-2 text-right flex justify-end", val),
|
||||
);
|
||||
});
|
||||
@@ -195,7 +217,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
m(
|
||||
"span.text-xs",
|
||||
"Displaying ",
|
||||
m("span.font-medium", "" + filteredKeys.length),
|
||||
m("span.font-medium", "" + filteredParams.length),
|
||||
" out of ",
|
||||
m("span.font-medium", "" + count),
|
||||
" parameters",
|
||||
@@ -204,7 +226,7 @@ const component: ClosureComponent<Attrs> = () => {
|
||||
"a.text-cyan-700 hover:text-cyan-900 text-sm font-medium",
|
||||
{
|
||||
href: `api/devices/${encodeURIComponent(
|
||||
device["DeviceID.ID"].value[0],
|
||||
device["DeviceID.ID"],
|
||||
)}.csv`,
|
||||
download: "",
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user