From 59ef8256e6e1a3e6a595248699594498600e4ab9 Mon Sep 17 00:00:00 2001 From: baldo Date: Wed, 24 Aug 2022 23:47:55 +0200 Subject: [PATCH] Make layers config typesafe. --- server/config.ts | 28 ++++++++++++++++++++- server/shared/types/index.ts | 47 ++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/server/config.ts b/server/config.ts index d494df5..72da832 100644 --- a/server/config.ts +++ b/server/config.ts @@ -3,7 +3,15 @@ import commandLineUsage from "command-line-usage"; import fs from "graceful-fs"; import url from "url"; import { parse } from "sparkson"; -import { Config, hasOwnProperty, Url, Version } from "./types"; +import { + Config, + hasOwnProperty, + isLayerConfig, + isPlainObject, + isString, + Url, + Version, +} from "./types"; export let config: Config = {} as Config; export let version: Version = "unknown" as Version; @@ -89,6 +97,24 @@ export function parseCommandLine(): void { config = parse(Config, configJSON); + if (!isPlainObject(config.client.coordsSelector.layers)) { + console.error( + "Error in config.json: client.coordsSelector.layers is not an JSON object." + ); + process.exit(1); + } + + for (const [id, layerConfig] of Object.entries( + config.client.coordsSelector.layers + )) { + if (!isLayerConfig(layerConfig)) { + console.error( + `Error in config.json: client.coordsSelector.layers[${id}] is not a valid layer config.` + ); + process.exit(1); + } + } + function stripTrailingSlash(url: Url): Url { return url.endsWith("/") ? (url.substring(0, url.length - 1) as Url) diff --git a/server/shared/types/index.ts b/server/shared/types/index.ts index 0421aad..5c8a77e 100644 --- a/server/shared/types/index.ts +++ b/server/shared/types/index.ts @@ -1,4 +1,9 @@ -import { ArrayField, Field, RawJsonField } from "sparkson"; +import { + ArrayField, + Field, + RawJsonField, + registerStringMapper, +} from "sparkson"; // Types shared with the client. export type TypeGuard = (arg: unknown) => arg is T; @@ -282,12 +287,50 @@ export function isCoordinatesConfig(arg: unknown): arg is CoordinatesConfig { return isNumber(coords.lat) && isNumber(coords.lng); } +export type LayerOptions = { + attribution: string; + subdomains?: string; + maxZoom: number; +}; + +export function isLayerOptions(arg: unknown): arg is LayerOptions { + if (!isPlainObject(arg)) { + return false; + } + const obj = arg as LayerOptions; + return ( + isString(obj.attribution) && + isOptional(obj.subdomains, isString) && + isNumber(obj.maxZoom) + ); +} + +export type LayerConfig = { + name: string; + url: Url; + type: string; + layerOptions: LayerOptions; +}; + +export function isLayerConfig(arg: unknown): arg is LayerConfig { + if (!isPlainObject(arg)) { + return false; + } + const obj = arg as LayerConfig; + return ( + isString(obj.name) && + isUrl(obj.url) && + isString(obj.type) && + isLayerOptions(obj.layerOptions) + ); +} + export class CoordinatesSelectorConfig { constructor( @Field("lat") public lat: number, @Field("lng") public lng: number, @Field("defaultZoom") public defaultZoom: number, - @RawJsonField("layers") public layers: JSONObject + @RawJsonField("layers") public layers: Record ) {} }