From ca85bbd6be81c4d3132e1ae912ae7f546af62138 Mon Sep 17 00:00:00 2001 From: Fred Boniface Date: Tue, 9 Sep 2025 20:46:43 +0100 Subject: [PATCH] Add getNearestStations to Reference client, along with types, validators and tests --- src/clients/ReferenceClientV2.ts | 10 ++++-- src/inputValidation/inputValidation.test.ts | 36 ++++++++++++++++++++- src/inputValidation/inputValidation.ts | 23 +++++++++++++ src/types/reference/ReferenceTypesV2.ts | 10 +++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/src/clients/ReferenceClientV2.ts b/src/clients/ReferenceClientV2.ts index 07887ec..d9f328a 100644 --- a/src/clients/ReferenceClientV2.ts +++ b/src/clients/ReferenceClientV2.ts @@ -1,5 +1,5 @@ -import { validateCrs, validateNlc, validateReasonCode, validateStanox, validateTiploc, validateStation } from "../inputValidation/inputValidation"; -import { ReferenceV2_LocationReferenceCodes, ReferenceV2_LocationReferenceCodeType, ReferenceV2_ReasonCode } from "../types/reference/ReferenceTypesV2"; +import { validateCrs, validateNlc, validateReasonCode, validateStanox, validateTiploc, validateStation, validateLatLon } from "../inputValidation/inputValidation"; +import { ReferenceV2_LocationReferenceCodes, ReferenceV2_LocationReferenceCodeType, ReferenceV2_NearestStations, ReferenceV2_ReasonCode } from "../types/reference/ReferenceTypesV2"; import { BaseOwlBoardClient } from "./client"; export class ReferenceClientV2 { @@ -15,6 +15,12 @@ export class ReferenceClientV2 { return this.client.makeRequest("GET", path); } + async getNearestStations(latitude: number, longitude: number): Promise { + validateLatLon(latitude, longitude); + const path = `/api/v2/live/station/nearest/${latitude}/${longitude}` + return this.client.makeRequest("GET", path); + } + async lookupLocationReference(type: ReferenceV2_LocationReferenceCodeType, referenceCode: string): Promise { const validators: Record boolean> = { crs: validateCrs, diff --git a/src/inputValidation/inputValidation.test.ts b/src/inputValidation/inputValidation.test.ts index 346345c..e113a8f 100644 --- a/src/inputValidation/inputValidation.test.ts +++ b/src/inputValidation/inputValidation.test.ts @@ -7,7 +7,8 @@ import { validateCrs, validateHeadcode, validateNlc, validateStanox, - validateStation} from "./inputValidation"; + validateStation, + validateLatLon} from "./inputValidation"; import { ValidationError } from "../errors"; describe("PIS Validation Tests", () => { @@ -228,3 +229,36 @@ describe("Headcode Validation Tests", () => { expect(() => validateHeadcode(true)).toThrow(ValidationError); }) }) + +describe("Latitude/Logitude Validation Tests", () => { + test("Valid Latitude/Logitude values should return true", () => { + expect(validateLatLon(51.5074, "-0.1278")).toBe(true); + expect(validateLatLon("51.5074", "-0.1278")).toBe(true); + expect(validateLatLon(0, 0)).toBe(true); + expect(validateLatLon("0", "0")).toBe(true); + expect(validateLatLon(90, 180)).toBe(true); + expect(validateLatLon(-90, -180)).toBe(true); + expect(validateLatLon(" 51.5074 ", " -0.1278 ")).toBe(true); + expect(validateLatLon(45.123456, -73.987654)).toBe(true); + expect(validateLatLon("89.9999999999", "179.9999999999")).toBe(true); + }) + + test("Invalid types should throw Validation Error", () => { + expect(() => validateLatLon(true, false)).toThrow(ValidationError); + expect(() => validateLatLon([], [])).toThrow(ValidationError); + expect(() => validateLatLon(null, null)).toThrow(ValidationError); + expect(() => validateLatLon("str", "ing")).toThrow(ValidationError); + expect(() => validateLatLon({}, {})).toThrow(ValidationError); + expect(() => validateLatLon(14.235, "whale")).toThrow(ValidationError); + expect(() => validateLatLon("", "")).toThrow(ValidationError); + }) + + test("Out of bound inputs should throw Validation Error", () => { + expect(() => validateLatLon(91, 0)).toThrow(ValidationError); + expect(() => validateLatLon(-91, 47)).toThrow(ValidationError); + expect(() => validateLatLon(12, 181)).toThrow(ValidationError); + expect(() => validateLatLon(78, -181)).toThrow(ValidationError); + expect(() => validateLatLon("90.0000001", "0")).toThrow(ValidationError); + expect(() => validateLatLon("0", "180.0000001")).toThrow(ValidationError); + }) +}) \ No newline at end of file diff --git a/src/inputValidation/inputValidation.ts b/src/inputValidation/inputValidation.ts index 0795e80..2d70d7f 100644 --- a/src/inputValidation/inputValidation.ts +++ b/src/inputValidation/inputValidation.ts @@ -126,3 +126,26 @@ export function validateHeadcode(headcode: unknown): boolean { return true; } +export function validateLatLon(lat: unknown, lon: unknown): boolean { + const toNumber = (val: unknown, label: string): number => { + if (typeof val === "number") return val; + if (typeof val === "string" && val.trim() !== "") { + const num = Number(val); + if (Number.isFinite(num)) return num; + } + throw new ValidationError(`Invalid input: ${label} must be a valid number`) + }; + + const latNum = toNumber(lat, "latitude"); + const lonNum = toNumber(lon, "longitude"); + + if (latNum < -90 || latNum > 90) { + throw new ValidationError("Latitude must be between -90 and 90"); + } + if (lonNum < -180 || lonNum > 180) { + throw new ValidationError("Longitude must be between -180 and 180"); + } + + return true; +} + diff --git a/src/types/reference/ReferenceTypesV2.ts b/src/types/reference/ReferenceTypesV2.ts index e32afe0..11edc04 100644 --- a/src/types/reference/ReferenceTypesV2.ts +++ b/src/types/reference/ReferenceTypesV2.ts @@ -19,4 +19,12 @@ interface ReferenceV2_ReasonCodeObject { export type ReferenceV2_ReasonCode = ReferenceV2_ReasonCodeObject[] -export type ReferenceV2_LocationReferenceCodeType = "tiploc" | "crs" | "stanox" | "nlc" | "station" \ No newline at end of file +export type ReferenceV2_LocationReferenceCodeType = "tiploc" | "crs" | "stanox" | "nlc" | "station" + +interface ReferenceV2_NearestStationItem { + "3ALPHA": string; + NLCDESC: string; + miles: number; // Float +} + +export type ReferenceV2_NearestStations = ReferenceV2_NearestStationItem[] \ No newline at end of file