diff --git a/pkg/shell/manifests.ts b/pkg/shell/manifests.ts index 704a5295174b..714aae178fab 100644 --- a/pkg/shell/manifests.ts +++ b/pkg/shell/manifests.ts @@ -19,6 +19,114 @@ import { JsonValue, JsonObject } from "cockpit"; +// Generic validation machinery + +class ValidationError extends Error { } + +const validation_path: string[] = []; + +function with_validation_path(p: string, func: () => T): T { + validation_path.push(p); + try { + return func(); + } finally { + validation_path.pop(); + } +} + +function validation_error(msg: string) { + console.error("Validation error:", JSON.stringify(validation_path), msg); +} + +function import_string(val: JsonValue, fallback?: string): string { + if (typeof val == "string") + return val; + validation_error(`Not a string: ${JSON.stringify(val)}`); + if (fallback !== undefined) + return fallback; + throw new ValidationError(); +} + +function import_number(val: JsonValue, fallback?: number): number { + if (typeof val == "number") + return val; + validation_error(`Not a number: ${JSON.stringify(val)}`); + if (fallback !== undefined) + return fallback; + throw new ValidationError(); +} + +function import_boolean(val: JsonValue, fallback?: boolean): boolean { + if (typeof val == "boolean") + return val; + validation_error(`Not a boolean: ${JSON.stringify(val)}`); + if (fallback !== undefined) + return fallback; + throw new ValidationError(); +} + +function import_json_object(val: JsonValue, fallback?: JsonObject): JsonObject { + if (!!val && typeof val == "object" && val.length === undefined) + return val as JsonObject; + validation_error(`Not an object: ${JSON.stringify(val)}`); + if (fallback !== undefined) + return fallback; + throw new ValidationError(); +} + +function import_json_array(val: JsonValue, fallback?: JsonValue[]): JsonValue[] { + if (!!val && typeof val == "object" && val.length !== undefined) + return val as JsonValue[]; + validation_error(`Not an array: ${JSON.stringify(val)}`); + if (fallback !== undefined) + return fallback; + throw new ValidationError(); +} + +function import_record(val: JsonValue, importer: (val: JsonValue) => T): Record { + const obj = import_json_object(val, {}); + const res: Record = {}; + for (const key of Object.keys(obj)) { + try { + with_validation_path(key, () => { res[key] = importer(obj[key]) }); + } catch (e) { + if (!(e instanceof ValidationError)) + throw e; + } + } + return res; +} + +function import_array(val: JsonValue, importer: (val: JsonValue) => T): Array { + const arr = import_json_array(val, []); + const res: Array = []; + for (let i = 0; i < arr.length; i++) { + try { + with_validation_path(`index ${i}`, () => { res.push(importer(arr[i])) }); + } catch (e) { + if (!(e instanceof ValidationError)) + throw e; + } + } + return res; +} + +function import_optional(res: T, obj: JsonObject, field: F, importer: (val: JsonValue) => T[F]): void { + if (obj[field as string] === undefined) + return; + with_validation_path(field as string, () => { res[field] = importer(obj[field]) }); +} + +function import_mandatory(obj: JsonObject, field: keyof T, importer: (val: JsonValue) => V): V { + if (obj[field as string] === undefined) { + validation_error(`Field ${String(field)} is missing`); + throw new ValidationError(); + } + return with_validation_path(field as string, () => importer(obj[field])); +} + +// Concrete types + export interface ManifestKeyword { matches: string[]; goto?: string; @@ -26,11 +134,31 @@ export interface ManifestKeyword { translate?: boolean; } +function import_ManifestKeyword(val: JsonValue): ManifestKeyword { + const obj = import_json_object(val); + const res: ManifestKeyword = { + matches: import_mandatory(obj, "matches", v => import_array(v, import_string)), + }; + import_optional(res, obj, "goto", import_string); + import_optional(res, obj, "weight", v => import_number(v, 0)); + import_optional(res, obj, "translate", v => import_boolean(v, false)); + return res; +} + export interface ManifestDocs { label: string; url: string; } +function import_ManifestDocs(val: JsonValue): ManifestDocs { + const obj = import_json_object(val); + const res: ManifestDocs = { + label: import_mandatory(obj, "label", import_string), + url: import_mandatory(obj, "url", import_string), + }; + return res; +} + export interface ManifestEntry { path?: string; label?: string; @@ -39,15 +167,38 @@ export interface ManifestEntry { keywords?: ManifestKeyword[]; } +function import_ManifestEntry(val: JsonValue): ManifestEntry { + const obj = import_json_object(val); + const res: ManifestEntry = { }; + import_optional(res, obj, "path", import_string); + import_optional(res, obj, "label", import_string); + import_optional(res, obj, "order", v => import_number(v, 0)); + import_optional(res, obj, "docs", v => import_array(v, import_ManifestDocs)); + import_optional(res, obj, "keywords", v => import_array(v, import_ManifestKeyword)); + return res; +} + export interface ManifestSection { [name: string]: ManifestEntry; } +function import_ManifestSection(val: JsonValue): ManifestSection { + return import_record(val, import_ManifestEntry); +} + export interface ManifestParentSection { component?: string; docs?: ManifestDocs[]; } +function import_ManifestParentSection(val: JsonValue): ManifestParentSection { + const obj = import_json_object(val); + const res: ManifestParentSection = { }; + import_optional(res, obj, "component", import_string); + import_optional(res, obj, "docs", v => import_array(v, import_ManifestDocs)); + return res; +} + export interface Manifest { dashboard?: ManifestSection; menu?: ManifestSection; @@ -58,13 +209,24 @@ export interface Manifest { ".checksum"?: string; } +function import_Manifest(val: JsonValue): Manifest { + const obj = import_json_object(val); + const res: Manifest = { }; + import_optional(res, obj, "dashboard", import_ManifestSection); + import_optional(res, obj, "menu", import_ManifestSection); + import_optional(res, obj, "tools", import_ManifestSection); + import_optional(res, obj, "preload", v => import_array(v, import_string)); + import_optional(res, obj, "parent", import_ManifestParentSection); + import_optional(res, obj, ".checksum", import_string); + return res; +} + export interface Manifests { [pkg: string]: Manifest; } export function import_Manifests(val: JsonValue): Manifests { - // TODO - validate against schema - return val as unknown as Manifests; + return with_validation_path("Manifests", () => import_record(val, import_Manifest)); } export interface ShellManifest { @@ -73,6 +235,9 @@ export interface ShellManifest { } export function import_ShellManifest(val: JsonValue): ShellManifest { - // TODO - validate against schema - return val as unknown as ShellManifest; + const obj = import_json_object(val); + const res: ShellManifest = { }; + import_optional(res, obj, "docs", v => import_array(v, import_ManifestDocs)); + import_optional(res, obj, "locales", v => import_record(v, import_string)); + return res; }