Skip to content

Commit

Permalink
WIP - validation
Browse files Browse the repository at this point in the history
The various import_Foo functions are pretty well typechecked.
  • Loading branch information
mvollmer committed Dec 17, 2024
1 parent fd0927e commit 284b65b
Showing 1 changed file with 169 additions and 4 deletions.
173 changes: 169 additions & 4 deletions pkg/shell/manifests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,146 @@

import { JsonValue, JsonObject } from "cockpit";

// Generic validation machinery

class ValidationError extends Error { }

const validation_path: string[] = [];

function with_validation_path<T>(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<T>(val: JsonValue, importer: (val: JsonValue) => T): Record<string, T> {
const obj = import_json_object(val, {});
const res: Record<string, T> = {};
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<T>(val: JsonValue, importer: (val: JsonValue) => T): Array<T> {
const arr = import_json_array(val, []);
const res: Array<T> = [];
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;
}

Check notice

Code scanning / CodeQL

Unneeded defensive code Note

This guard always evaluates to true.
}
return res;
}

function import_optional<T, F extends keyof T>(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<T, V>(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]));
}

Check notice

Code scanning / CodeQL

Unneeded defensive code Note

This guard always evaluates to true.
// Concrete types

export interface ManifestKeyword {
matches: string[];
goto?: string;
weight?: number;
translate?: boolean;
}

function import_ManifestKeyword(val: JsonValue): ManifestKeyword {
const obj = import_json_object(val);
const res: ManifestKeyword = {
matches: import_mandatory<ManifestKeyword, string[]>(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<ManifestDocs, string>(obj, "label", import_string),
url: import_mandatory<ManifestDocs, string>(obj, "url", import_string),
};
return res;
}

export interface ManifestEntry {
path?: string;
label?: string;
Expand All @@ -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;
Expand All @@ -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 {
Expand All @@ -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;
}

0 comments on commit 284b65b

Please sign in to comment.