Add ?request= query-param codec for Online-Board details URLs per TZ 4.1.2 Table 5 row 6
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user