Lab
Adapter
// The dashboard's contract: a permit, null for "no permit", or an error
// saying why the source could not answer.
type Permit = { plate: string; zone: string; expiresAt: string };
// The source could not answer: ask again later.
class PermitUnavailable extends Error {}
interface PermitLookup {
lookupPermit(plate: string, zone: string): Permit | null;
}
// Three existing APIs, as they already are.
type LegacyReply = { state: string; registration: string; curb: string; end: string };
interface LegacyMeter {
lookup(plate: string, curb: string): LegacyReply;
}
type PartnerReply = { status: number; vehicle: string; district: string; paidThrough: string };
interface PartnerMeter {
fetchPermit(plate: string, zone: string): PartnerReply;
}
// The city's resident-permit registry. It is asked about a plate written
// without dashes (KLM204) and lists every zone that plate holds a permit in.
// A permit ends endsAtMinute minutes after midnight; ok false means the
// registry could not answer.
type ResidentPermit = { zone: string; endsAtMinute: number };
type ResidentReply = { ok: boolean; permits: ResidentPermit[] };
interface ResidentRegistry {
permitsFor(plate: string): ResidentReply;
}
// Each adapter holds one source and answers the dashboard's question with it.
class LegacyPermitAdapter implements PermitLookup {
constructor(private source: LegacyMeter) {}
lookupPermit(plate: string, zone: string): Permit | null {
const reply = this.source.lookup(plate, zone);
if (reply.state === 'none') return null;
if (reply.state !== 'active') throw new PermitUnavailable(reply.state);
return { plate: reply.registration, zone: reply.curb, expiresAt: reply.end };
}
}
class PartnerPermitAdapter implements PermitLookup {
constructor(private source: PartnerMeter) {}
lookupPermit(plate: string, zone: string): Permit | null {
const reply = this.source.fetchPermit(plate, zone);
if (reply.status === 404) return null;
if (reply.status !== 200) throw new PermitUnavailable(`status ${reply.status}`);
return { plate: reply.vehicle, zone: reply.district, expiresAt: reply.paidThrough };
}
}
// First draft, from the pull request that added the registry.
class ResidentPermitAdapter implements PermitLookup {
constructor(private source: ResidentRegistry) {}
lookupPermit(plate: string, zone: string): Permit | null {
const reply = this.source.permitsFor(plate);
if (!reply.ok) throw new PermitUnavailable('registry');
const permit = reply.permits[0];
if (!permit) return null;
return { plate, zone, expiresAt: String(permit.endsAtMinute) };
}
}
// The dashboard knows PermitLookup and no provider.
function showPermit(reader: PermitLookup, plate: string, zone: string): string {
try {
const permit = reader.lookupPermit(plate, zone);
if (!permit) return `${plate} in ${zone}: no active permit`;
return `${permit.plate} in ${permit.zone}: permit through ${permit.expiresAt}`;
} catch {
return `${plate} in ${zone}: check again, source unavailable`;
}
}