Lab
Interpreter
// A help-desk ticket, and what one evaluation reads: the ticket and who is
// looking (null until the viewer's sign-in has finished).
type Ticket = {
id: string;
status: 'open' | 'closed';
priority: 'urgent' | 'normal';
assignee: string | null;
};
type Context = { ticket: Ticket; viewer: string | null };
type Result = { ok: true; value: boolean } | { ok: false; error: string };
// Every expression in the filter language answers for itself in a context.
interface Expression {
interpret(context: Context): Result;
}
// An atom reads one fact about the ticket or the viewer.
class Atom implements Expression {
name: 'open' | 'urgent' | 'mine';
constructor(name: 'open' | 'urgent' | 'mine') {
this.name = name;
}
interpret({ ticket, viewer }: Context): Result {
if (this.name === 'open') {
return { ok: true, value: ticket.status === 'open' };
}
if (this.name === 'urgent') {
return { ok: true, value: ticket.priority === 'urgent' };
}
if (viewer === null) return { ok: false, error: 'missing-viewer' };
return { ok: true, value: ticket.assignee === viewer };
}
}
// AND reads its left side first; only a true left needs the right side.
class And implements Expression {
left: Expression;
right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(context: Context): Result {
const left = this.left.interpret(context);
if (!left.ok || !left.value) return left;
return this.right.interpret(context);
}
}
// OR reads its left side first; only a false left needs the right side.
class Or implements Expression {
left: Expression;
right: Expression;
constructor(left: Expression, right: Expression) {
this.left = left;
this.right = right;
}
interpret(context: Context): Result {
const left = this.left.interpret(context);
if (!left.ok || left.value) return left;
return this.right.interpret(context);
}
}
// NOT negates an answer and passes an error on unchanged.
class Not implements Expression {
child: Expression;
constructor(child: Expression) {
this.child = child;
}
interpret(context: Context): Result {
const child = this.child.interpret(context);
return child.ok ? { ok: true, value: !child.value } : child;
}
}
// The saved view's caller: the matching IDs in order, or the first failure,
// never a partial list.
function search(
rule: Expression,
tickets: Ticket[],
viewer: string | null,
): string {
const ids: string[] = [];
for (const ticket of tickets) {
const result = rule.interpret({ ticket, viewer });
if (!result.ok) return `error ${result.error} at ${ticket.id}`;
if (result.value) ids.push(ticket.id);
}
return ids.join(', ') || '(none)';
}
const tickets: Ticket[] = [
{ id: 'T-101', status: 'open', priority: 'urgent', assignee: 'rian' },
{ id: 'T-102', status: 'open', priority: 'normal', assignee: 'maya' },
{ id: 'T-103', status: 'closed', priority: 'normal', assignee: 'maya' },
];
// open AND (urgent OR mine): saved once, answered for each viewer.
const saved = new And(
new Atom('open'),
new Or(new Atom('urgent'), new Atom('mine')),
);
const forMaya = search(saved, tickets, 'maya');
const forRian = search(saved, tickets, 'rian');
console.log('maya: ' + forMaya);
console.log('rian: ' + forRian);
TODO
Visual to come