Lab
Interpreter
// A user as the request arrived: their plan, whether they joined the beta,
// and the country the geo lookup found (null when it failed).
type User = {
id: string;
plan: 'free' | 'pro';
beta: boolean;
country: string | null;
};
// What one evaluation reads.
type Context = { user: User };
type Result = { ok: true; value: boolean } | { ok: false; error: string };
// Every expression in the targeting language answers for itself in a context.
interface Expression {
interpret(context: Context): Result;
}
// Beta is true for users who joined the beta.
class Beta implements Expression {
interpret({ user }: Context): Result {
return { ok: true, value: user.beta };
}
}
// PlanIs is true for users on one plan.
class PlanIs implements Expression {
plan: string;
constructor(plan: string) {
this.plan = plan;
}
interpret({ user }: Context): Result {
return { ok: true, value: user.plan === this.plan };
}
}
// CountryIn is true for users in one of its countries.
class CountryIn implements Expression {
codes: string[];
constructor(codes: string[]) {
this.codes = codes;
}
// Written for the EU launch in May, when its countries were Germany
// and France.
interpret({ user }: Context): Result {
return { ok: true, value: user.country === 'DE' || user.country === 'FR' };
}
}
// 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 flag service's caller: off whenever the rule cannot be answered,
// with the reason.
function decide(rule: Expression, user: User): string {
const result = rule.interpret({ user });
if (!result.ok) return `off (${result.error})`;
return result.value ? 'on' : 'off';
}