From 73d724f76a54afde279d58b4290a7b96c1517f02 Mon Sep 17 00:00:00 2001 From: gnezim Date: Wed, 15 Apr 2026 08:00:12 +0300 Subject: [PATCH] Add Phase 2B URL serializer implementation plan TDD plan for porting Angular OnlineBoardUrlBuilder/Parser to pure TypeScript functions covering all 6 URL types (start, flight, departure, arrival, route, details) with roundtrip and edge case tests. --- .../2026-04-15-phase-2b-url-serializer.md | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-15-phase-2b-url-serializer.md diff --git a/docs/superpowers/plans/2026-04-15-phase-2b-url-serializer.md b/docs/superpowers/plans/2026-04-15-phase-2b-url-serializer.md new file mode 100644 index 00000000..2e06fa4a --- /dev/null +++ b/docs/superpowers/plans/2026-04-15-phase-2b-url-serializer.md @@ -0,0 +1,152 @@ +# Phase 2B — URL Serializer/Parser + +> **Parent:** `2026-04-14-phase-2-online-board-master.md` (sub-plan 2B) +> **Status:** Ready to execute + +## Goal + +TDD port of the Angular `OnlineBoardUrlBuilderService` / `OnlineBoardUrlParserService` into pure TypeScript functions with zero Angular dependencies. Byte-exact URL parity with Angular. + +## Deliverable + +`src/features/online-board/url.ts` — pure functions, no side effects, no Date objects. + +## URL Format Reference (from Angular analysis) + +| Type | Pattern | Example | +|---|---|---| +| Start | `/onlineboard` or `/onlineboard/` | `/onlineboard` | +| Flight | `/onlineboard/flight/{carrier}{flightNumber}{suffix}-{yyyyMMdd}` | `/onlineboard/flight/SU0100D-20250115` | +| Departure | `/onlineboard/departure/{station}-{yyyyMMdd}[-{HHmmHHmm}]` | `/onlineboard/departure/SVO-20250115-08001800` | +| Arrival | `/onlineboard/arrival/{station}-{yyyyMMdd}[-{HHmmHHmm}]` | `/onlineboard/arrival/LED-20250115` | +| Route | `/onlineboard/route/{dep}-{arr}-{yyyyMMdd}[-{HHmmHHmm}]` | `/onlineboard/route/SVO-LED-20250115` | +| Details | `/onlineboard/{carrier}{flightNumber}{suffix}-{yyyyMMdd}` | `/onlineboard/SU0100-20250115` | + +### Key rules + +- **Date format:** `yyyyMMdd` (8 digits, no separators) +- **Time range:** `{HHmm}{HHmm}` (8 digits continuous, from-to) +- **Flight number:** zero-padded to 4 digits in the URL (e.g., `100` becomes `0100`), total flightNumber+suffix = last 5 chars +- **Carrier code:** 2 characters (IATA), or 3 characters if 3rd char is a letter (rare 3-char carriers) +- **Suffix:** optional single trailing letter on a flight identifier (e.g., `D` in `SU0100D`) +- **Station codes:** 3-letter IATA airport codes + +## Exported API + +```ts +type OnlineBoardParams = + | { type: "start" } + | { type: "flight"; carrier: string; flightNumber: string; suffix?: string; date: string } + | { type: "departure"; station: string; date: string; timeFrom?: string; timeTo?: string } + | { type: "arrival"; station: string; date: string; timeFrom?: string; timeTo?: string } + | { type: "route"; departure: string; arrival: string; date: string; timeFrom?: string; timeTo?: string } + | { type: "details"; carrier: string; flightNumber: string; suffix?: string; date: string }; + +function parseOnlineBoardUrl(path: string): OnlineBoardParams | null; +function buildOnlineBoardUrl(params: OnlineBoardParams): string; + +// Lower-level helpers (also exported for 2C/2E consumption) +function parseFlightUrlParams(raw: string): IParsedFlightId | null; +function buildFlightUrlParams(id: IParsedFlightId): string; +function parseStationUrlParams(raw: string): { station: string; date: string; timeFrom?: string; timeTo?: string } | null; +function parseRouteUrlParams(raw: string): { departure: string; arrival: string; date: string; timeFrom?: string; timeTo?: string } | null; +``` + +## Tasks + +### Task 1: Write failing tests for `parseFlightUrlParams` + +**File:** `src/features/online-board/url.test.ts` + +Test cases: +- `SU0100-20250115` -> `{ carrier: "SU", flightNumber: "0100", date: "20250115" }` +- `SU0100D-20250115` -> `{ carrier: "SU", flightNumber: "0100", suffix: "D", date: "20250115" }` +- `SU100-20250115` -> `{ carrier: "SU", flightNumber: "100", date: "20250115" }` (3-digit) +- `SU1234-20250115` -> `{ carrier: "SU", flightNumber: "1234", date: "20250115" }` (4-digit) +- Empty string -> `null` +- Missing date part -> `null` + +### Task 2: Implement `parseFlightUrlParams` to pass tests + +### Task 3: Write failing tests for `buildFlightUrlParams` + +Test cases: +- `{ carrier: "SU", flightNumber: "100", date: "20250115" }` -> `SU0100-20250115` (padded) +- `{ carrier: "SU", flightNumber: "0100", suffix: "D", date: "20250115" }` -> `SU0100D-20250115` +- `{ carrier: "SU", flightNumber: "1234", date: "20250115" }` -> `SU1234-20250115` + +### Task 4: Implement `buildFlightUrlParams` to pass tests + +### Task 5: Write failing tests for `parseStationUrlParams` + +Test cases: +- `SVO-20250115` -> `{ station: "SVO", date: "20250115" }` +- `SVO-20250115-08001800` -> `{ station: "SVO", date: "20250115", timeFrom: "0800", timeTo: "1800" }` +- Empty string -> `null` + +### Task 6: Implement `parseStationUrlParams` to pass tests + +### Task 7: Write failing tests for `parseRouteUrlParams` + +Test cases: +- `SVO-LED-20250115` -> `{ departure: "SVO", arrival: "LED", date: "20250115" }` +- `SVO-LED-20250115-08001800` -> `{ departure: "SVO", arrival: "LED", date: "20250115", timeFrom: "0800", timeTo: "1800" }` +- Empty string -> `null` + +### Task 8: Implement `parseRouteUrlParams` to pass tests + +### Task 9: Write failing tests for `parseOnlineBoardUrl` + +Test cases: +- `/onlineboard` -> `{ type: "start" }` +- `/onlineboard/` -> `{ type: "start" }` +- `/onlineboard/flight/SU0100-20250115` -> `{ type: "flight", carrier: "SU", flightNumber: "0100", date: "20250115" }` +- `/onlineboard/departure/SVO-20250115` -> `{ type: "departure", station: "SVO", date: "20250115" }` +- `/onlineboard/arrival/LED-20250115-08001800` -> `{ type: "arrival", station: "LED", date: "20250115", timeFrom: "0800", timeTo: "1800" }` +- `/onlineboard/route/SVO-LED-20250115` -> `{ type: "route", departure: "SVO", arrival: "LED", date: "20250115" }` +- `/onlineboard/SU0100-20250115` -> `{ type: "details", carrier: "SU", flightNumber: "0100", date: "20250115" }` +- `/some/other/path` -> `null` +- Empty string -> `null` + +### Task 10: Implement `parseOnlineBoardUrl` to pass tests + +### Task 11: Write failing tests for `buildOnlineBoardUrl` + +Test cases: +- `{ type: "start" }` -> `onlineboard` +- `{ type: "flight", carrier: "SU", flightNumber: "100", date: "20250115" }` -> `onlineboard/flight/SU0100-20250115` +- `{ type: "departure", station: "SVO", date: "20250115" }` -> `onlineboard/departure/SVO-20250115` +- `{ type: "departure", station: "SVO", date: "20250115", timeFrom: "0800", timeTo: "1800" }` -> `onlineboard/departure/SVO-20250115-08001800` +- `{ type: "arrival", station: "LED", date: "20250115" }` -> `onlineboard/arrival/LED-20250115` +- `{ type: "route", departure: "SVO", arrival: "LED", date: "20250115" }` -> `onlineboard/route/SVO-LED-20250115` +- `{ type: "details", carrier: "SU", flightNumber: "0100", date: "20250115" }` -> `onlineboard/SU0100-20250115` + +### Task 12: Implement `buildOnlineBoardUrl` to pass tests + +### Task 13: Roundtrip tests + +For every URL type: `buildOnlineBoardUrl(parseOnlineBoardUrl(url)) === url` (after normalization). + +### Task 14: Edge case tests + +- Suffixed flights roundtrip: `SU0100D-20250115` +- 3-digit flight numbers: build pads to 4, parse handles both +- Time range only partially specified (only timeFrom, no timeTo) -> no time range in URL +- Invalid date format -> null +- Unknown route prefix -> null + +### Task 15: Export from barrel + +Update `src/features/online-board/index.ts` to re-export the public API. + +### Task 16: Verification + +Run `pnpm typecheck && pnpm lint && pnpm test`. + +## Exit criteria + +- All tests pass +- `parseOnlineBoardUrl` returns null for invalid inputs, never throws +- Zero `any` types +- Pure functions only — no side effects, no imports from Angular +- Dates are strings (`yyyyMMdd`), not Date objects