File size: 2,830 Bytes
fa9c65f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | /**
* Shared scalar guards for the analysis adapters (#5696).
*
* Both adapter modules map untrusted seeded-cache JSON onto the analysis
* cores' input types, so both need the same defensive primitives. They were
* written independently and grew two names for each concept
* (`finiteNumber`/`num`, `nonEmptyString`/`str`); one definition removes the
* drift risk, since a divergence in null-handling here silently empties a
* result set rather than raising.
*
* Semantics are deliberately permissive on INPUT (a seeder that stores a
* number as a string still parses) and strict on VALIDATION (a coordinate
* must be real and in range).
*
* Dependency-free: importable from Vite client code, Vercel Edge bundles,
* server handlers, and tsx tests alike.
*/
/** Narrow to a plain object; arrays and null are rejected. */
export function asRecord(value: unknown): Record<string, unknown> | null {
return typeof value === 'object' && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
/** Always yields an iterable — a non-array becomes empty rather than throwing. */
export function asArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
/** Finite number, accepting a numeric string; anything else is null. */
export function finiteNumber(value: unknown): number | null {
if (typeof value === 'number') return Number.isFinite(value) ? value : null;
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
}
return null;
}
/** Trimmed string, or '' for anything non-string or whitespace-only. */
export function nonEmptyString(value: unknown): string {
return typeof value === 'string' && value.trim() !== '' ? value.trim() : '';
}
/**
* A coordinate pair usable as a real position: both components present, in
* range, and not null island — (0, 0) is overwhelmingly a missing-data
* artifact in these feeds rather than a location in the Gulf of Guinea.
*
* Note the exposure adapters deliberately do NOT apply this: they enrich
* whatever the caller asks about, and rejecting a point there would silently
* drop an event rather than decline to place it on a grid.
*/
export function usableCoord(lat: number | null, lon: number | null): lat is number {
return (
lat !== null && lon !== null &&
lat >= -90 && lat <= 90 &&
lon >= -180 && lon <= 180 &&
!(lat === 0 && lon === 0)
);
}
/** Read a nested `{ location: { latitude, longitude } }` pair. */
export function nestedLocation(record: Record<string, unknown>): { lat: number | null; lon: number | null } {
const location = asRecord(record.location);
return {
lat: finiteNumber(location?.latitude),
lon: finiteNumber(location?.longitude),
};
}
|