Add ?request= query-param codec for Online-Board details URLs per TZ 4.1.2 Table 5 row 6

This commit is contained in:
2026-04-21 16:33:16 +03:00
parent 750a328528
commit 531ace6abc
2 changed files with 275 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
import { describe, expect, it } from "vitest";
import {
buildDetailsRequestParam,
parseDetailsRequestParam,
type DetailsRequest,
} from "./detailsRequestParam.js";
describe("detailsRequestParam — build", () => {
it("4.1.2-R-Request-flight: encodes flight-number parent", () => {
const r: DetailsRequest = {
area: "onlineboard",
kind: "flight",
flightNumber: "SU1234",
date: "20260515",
};
expect(buildDetailsRequestParam(r)).toBe("onlineboard-flight-SU1234-20260515");
});
it("4.1.2-R-Request-departure: encodes departure parent without time", () => {
expect(
buildDetailsRequestParam({
area: "onlineboard",
kind: "departure",
station: "SVO",
date: "20260515",
}),
).toBe("onlineboard-departure-SVO-20260515");
});
it("4.1.2-R-Request-departure-time: encodes departure parent with time range", () => {
expect(
buildDetailsRequestParam({
area: "onlineboard",
kind: "departure",
station: "SVO",
date: "20260515",
timeFrom: "0600",
timeTo: "2200",
}),
).toBe("onlineboard-departure-SVO-20260515-06002200");
});
it("4.1.2-R-Request-arrival: encodes arrival parent", () => {
expect(
buildDetailsRequestParam({
area: "onlineboard",
kind: "arrival",
station: "SVO",
date: "20260515",
}),
).toBe("onlineboard-arrival-SVO-20260515");
});
it("4.1.2-R-Request-route: encodes route parent", () => {
expect(
buildDetailsRequestParam({
area: "onlineboard",
kind: "route",
departure: "MOW",
arrival: "LED",
date: "20260515",
}),
).toBe("onlineboard-route-MOW-LED-20260515");
});
it("4.1.2-R-Request-route-time: encodes route parent with time range", () => {
expect(
buildDetailsRequestParam({
area: "onlineboard",
kind: "route",
departure: "MOW",
arrival: "LED",
date: "20260515",
timeFrom: "0600",
timeTo: "2200",
}),
).toBe("onlineboard-route-MOW-LED-20260515-06002200");
});
});
describe("detailsRequestParam — parse", () => {
it("returns null for empty or non-matching input", () => {
expect(parseDetailsRequestParam("")).toBeNull();
expect(parseDetailsRequestParam("garbage")).toBeNull();
expect(parseDetailsRequestParam("schedule-flight-SU1234-20260515")).toBeNull();
});
it("parses flight-number form", () => {
expect(parseDetailsRequestParam("onlineboard-flight-SU1234-20260515")).toEqual({
area: "onlineboard",
kind: "flight",
flightNumber: "SU1234",
date: "20260515",
});
});
it("parses departure without time", () => {
expect(parseDetailsRequestParam("onlineboard-departure-SVO-20260515")).toEqual({
area: "onlineboard",
kind: "departure",
station: "SVO",
date: "20260515",
});
});
it("parses departure with time range", () => {
expect(parseDetailsRequestParam("onlineboard-departure-SVO-20260515-06002200")).toEqual({
area: "onlineboard",
kind: "departure",
station: "SVO",
date: "20260515",
timeFrom: "0600",
timeTo: "2200",
});
});
it("parses arrival", () => {
expect(parseDetailsRequestParam("onlineboard-arrival-LED-20260515")).toEqual({
area: "onlineboard",
kind: "arrival",
station: "LED",
date: "20260515",
});
});
it("parses route without time", () => {
expect(parseDetailsRequestParam("onlineboard-route-MOW-LED-20260515")).toEqual({
area: "onlineboard",
kind: "route",
departure: "MOW",
arrival: "LED",
date: "20260515",
});
});
it("parses route with time range", () => {
expect(parseDetailsRequestParam("onlineboard-route-MOW-LED-20260515-06002200")).toEqual({
area: "onlineboard",
kind: "route",
departure: "MOW",
arrival: "LED",
date: "20260515",
timeFrom: "0600",
timeTo: "2200",
});
});
});
describe("detailsRequestParam — roundtrip", () => {
it.each<DetailsRequest>([
{ area: "onlineboard", kind: "flight", flightNumber: "SU1234", date: "20260515" },
{ area: "onlineboard", kind: "departure", station: "SVO", date: "20260515" },
{ area: "onlineboard", kind: "departure", station: "SVO", date: "20260515", timeFrom: "0600", timeTo: "2200" },
{ area: "onlineboard", kind: "arrival", station: "LED", date: "20260515" },
{ area: "onlineboard", kind: "route", departure: "MOW", arrival: "LED", date: "20260515" },
{ area: "onlineboard", kind: "route", departure: "MOW", arrival: "LED", date: "20260515", timeFrom: "0600", timeTo: "2200" },
])("round-trips %o", (input) => {
const encoded = buildDetailsRequestParam(input);
const decoded = parseDetailsRequestParam(encoded);
expect(decoded).toEqual(input);
});
});
+113
View File
@@ -0,0 +1,113 @@
/**
* Codec for the `?request=...` query parameter on Online-Board flight-details URLs.
* Per TZ §4.1.2 Table 5 row 6. The param encodes the parent search context so
* the mini-list can rebuild the list on refresh / deep-link entry.
*
* Forms (canonical):
* onlineboard-flight-{flightNumber}-{yyyyMMdd}
* onlineboard-departure-{iata}-{yyyyMMdd}[-{HHmmHHmm}]
* onlineboard-arrival-{iata}-{yyyyMMdd}[-{HHmmHHmm}]
* onlineboard-route-{depIata}-{arrIata}-{yyyyMMdd}[-{HHmmHHmm}]
*/
export type DetailsRequest =
| { area: "onlineboard"; kind: "flight"; flightNumber: string; date: string }
| {
area: "onlineboard";
kind: "departure" | "arrival";
station: string;
date: string;
timeFrom?: string;
timeTo?: string;
}
| {
area: "onlineboard";
kind: "route";
departure: string;
arrival: string;
date: string;
timeFrom?: string;
timeTo?: string;
};
const AREA = "onlineboard";
function isDate(s: string | undefined): s is string {
return typeof s === "string" && /^\d{8}$/.test(s);
}
function isTimeRange(s: string | undefined): s is string {
return typeof s === "string" && /^\d{8}$/.test(s);
}
export function buildDetailsRequestParam(r: DetailsRequest): string {
switch (r.kind) {
case "flight":
return `${r.area}-flight-${r.flightNumber}-${r.date}`;
case "departure":
case "arrival": {
const base = `${r.area}-${r.kind}-${r.station}-${r.date}`;
return r.timeFrom && r.timeTo ? `${base}-${r.timeFrom}${r.timeTo}` : base;
}
case "route": {
const base = `${r.area}-route-${r.departure}-${r.arrival}-${r.date}`;
return r.timeFrom && r.timeTo ? `${base}-${r.timeFrom}${r.timeTo}` : base;
}
}
}
export function parseDetailsRequestParam(raw: string): DetailsRequest | null {
if (!raw) return null;
const parts = raw.split("-");
if (parts.length < 4) return null;
if (parts[0] !== AREA) return null;
const kind = parts[1];
if (kind === "flight") {
const [, , flightNumber, date] = parts;
if (!flightNumber || !isDate(date) || parts.length !== 4) return null;
return { area: "onlineboard", kind: "flight", flightNumber, date };
}
if (kind === "departure" || kind === "arrival") {
const [, , station, date, maybeTime] = parts;
if (!station || !isDate(date)) return null;
if (parts.length === 4) {
return { area: "onlineboard", kind, station, date };
}
if (parts.length === 5 && isTimeRange(maybeTime)) {
return {
area: "onlineboard",
kind,
station,
date,
timeFrom: maybeTime.slice(0, 4),
timeTo: maybeTime.slice(4, 8),
};
}
return null;
}
if (kind === "route") {
const [, , departure, arrival, date, maybeTime] = parts;
if (!departure || !arrival || !isDate(date)) return null;
if (parts.length === 5) {
return { area: "onlineboard", kind: "route", departure, arrival, date };
}
if (parts.length === 6 && isTimeRange(maybeTime)) {
return {
area: "onlineboard",
kind: "route",
departure,
arrival,
date,
timeFrom: maybeTime.slice(0, 4),
timeTo: maybeTime.slice(4, 8),
};
}
return null;
}
return null;
}