plan/react-rewrite #1
@@ -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
|
||||
Reference in New Issue
Block a user