← Adapter lesson
Lab

Adapter

// The dashboard's contract: a permit, null for "no permit",
// or PermitUnavailable when the source cannot answer.
type Permit = { plate: string; zone: string; expiresAt: string };
class PermitUnavailable extends Error {}
interface PermitLookup {
  lookupPermit(plate: string, zone: string): Permit | null;
}

// Two 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;
}

// 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);
    const { registration, curb, end } = reply;
    return { plate: registration, zone: curb, expiresAt: 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}`);
    }
    const { vehicle, district, paidThrough } = reply;
    return { plate: vehicle, zone: district, expiresAt: paidThrough };
  }
}

// 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`;
  }
}

// What each provider says about zone C7 today, in memory.
const legacy: LegacyMeter = {
  lookup: (plate) =>
    plate === 'KLM-204'
      ? { state: 'active', registration: plate, curb: 'C7', end: '18:30' }
      : { state: 'none', registration: '', curb: '', end: '' },
};
const blank = { vehicle: '', district: '', paidThrough: '' };
const partner: PartnerMeter = {
  fetchPermit: (plate) =>
    plate === 'KLM-204'
      ? { status: 200, vehicle: plate, district: 'C7', paidThrough: '18:30' }
      : plate === 'RTE-118'
        ? { status: 429, ...blank } // too many requests: try later
        : { status: 404, ...blank },
};

const partnerLookup = new PartnerPermitAdapter(partner);
const fromLegacy = showPermit(new LegacyPermitAdapter(legacy), 'KLM-204', 'C7');
const fromPartner = showPermit(partnerLookup, 'KLM-204', 'C7');
const noPermit = showPermit(partnerLookup, 'BXT-551', 'C7');
const rateLimited = showPermit(partnerLookup, 'RTE-118', 'C7');
console.log(`[legacy] ${fromLegacy}`);
console.log(`[partner] ${fromPartner}`);
console.log(`[partner] ${noPermit}`);
console.log(`[partner] ${rateLimited}`);
Legacy meterstate · registration · curb · end
Payment partnerstatus · vehicle · district · paid through
Kerbside dashboard · PermitLookup
—in zone—?
Source-call statement · observedWaiting for a recorded source call
Source reply · observedExisting API
Not observed

The source’s structured reply appears when it is recorded.

adapter
boundary
Caller contract · observedPermitLookup
Awaiting the caller’s value

A source reply alone does not establish the adapted outcome.

Dashboard line · actual runtime observation

No dashboard answer observed yet.

PermitNo permitCheck again

This source changes the scene’s named bindings. Use Recorded objects to inspect its data.

Source bindings changed. Inspect recorded objects for details.

One caller contract · two existing source interfaces · shapes name roles; they do not assert storage layout.