feat(lidar): add dataset gateway boundary
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
export type DatasetRepresentationId =
|
||||
| "native-scan"
|
||||
| "normalized-scan"
|
||||
| "rolling-local-map";
|
||||
|
||||
export interface DatasetGatewayCatalog {
|
||||
storage: {
|
||||
configured: boolean;
|
||||
admitted: boolean;
|
||||
status: "ready" | "blocked-storage-policy";
|
||||
requiredWindowsRoot: string;
|
||||
requiredWslRoot: string;
|
||||
};
|
||||
source: {
|
||||
sourceId: string;
|
||||
displayName: string;
|
||||
role: string;
|
||||
license: string;
|
||||
format: string;
|
||||
frameSemantics: "one-lidar-revolution";
|
||||
platforms: string[];
|
||||
superclasses: string[];
|
||||
validationArchiveGb: number;
|
||||
admissionStatus: "ready-for-download" | "blocked-storage-policy";
|
||||
};
|
||||
representations: Array<{
|
||||
id: DatasetRepresentationId;
|
||||
title: string;
|
||||
purpose: string;
|
||||
accumulation: boolean;
|
||||
}>;
|
||||
pipeline: Array<{
|
||||
stage: string;
|
||||
requires: string[];
|
||||
produces: string;
|
||||
}>;
|
||||
currentInput: {
|
||||
representation: "vendor-mapped-increment";
|
||||
nativeScan: false;
|
||||
perPointTime: false;
|
||||
ringOrLine: false;
|
||||
admittedForPatchworkpp: false;
|
||||
reason: string;
|
||||
};
|
||||
nextAction: string;
|
||||
}
|
||||
|
||||
export class DatasetGatewayContractError extends Error {}
|
||||
|
||||
type DatasetFetch = (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
) => Promise<Response>;
|
||||
|
||||
const SAFE_ID = /^[a-z0-9][a-z0-9._:/-]{0,159}$/;
|
||||
|
||||
function record(value: unknown, label: string): Record<string, unknown> {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw new DatasetGatewayContractError(`${label}: ожидался объект`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function array(value: unknown, label: string): unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new DatasetGatewayContractError(`${label}: ожидался массив`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function string(value: unknown, label: string, safe = false): string {
|
||||
if (
|
||||
typeof value !== "string"
|
||||
|| !value
|
||||
|| (safe && !SAFE_ID.test(value))
|
||||
) {
|
||||
throw new DatasetGatewayContractError(`${label}: некорректная строка`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function strings(value: unknown, label: string): string[] {
|
||||
return array(value, label).map((item, index) =>
|
||||
string(item, `${label}[${index}]`, true)
|
||||
);
|
||||
}
|
||||
|
||||
function displayStrings(value: unknown, label: string): string[] {
|
||||
return array(value, label).map((item, index) =>
|
||||
string(item, `${label}[${index}]`)
|
||||
);
|
||||
}
|
||||
|
||||
function boolean(value: unknown, label: string): boolean {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new DatasetGatewayContractError(`${label}: ожидался boolean`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function number(value: unknown, label: string): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
||||
throw new DatasetGatewayContractError(`${label}: некорректное число`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseDatasetGatewayCatalog(
|
||||
value: unknown,
|
||||
): DatasetGatewayCatalog {
|
||||
const source = record(value, "Dataset Gateway");
|
||||
if (
|
||||
source.schema_version !== "missioncore.dataset-gateway-catalog/v1"
|
||||
|| source.access !== "read-only"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Dataset Gateway contract несовместим");
|
||||
}
|
||||
const storage = record(source.storage, "storage");
|
||||
const storageStatus = storage.status;
|
||||
if (storageStatus !== "ready" && storageStatus !== "blocked-storage-policy") {
|
||||
throw new DatasetGatewayContractError("storage.status: неизвестное значение");
|
||||
}
|
||||
if (storage.path_exposed !== false) {
|
||||
throw new DatasetGatewayContractError("Dataset Gateway раскрыл локальный путь");
|
||||
}
|
||||
const sources = array(source.sources, "sources");
|
||||
if (sources.length !== 1) {
|
||||
throw new DatasetGatewayContractError("Ожидался один первичный dataset source");
|
||||
}
|
||||
const dataset = record(sources[0], "sources[0]");
|
||||
const download = record(dataset.download, "source.download");
|
||||
const admission = record(dataset.admission, "source.admission");
|
||||
if (download.automatic !== false) {
|
||||
throw new DatasetGatewayContractError("Большой dataset нельзя загружать автоматически");
|
||||
}
|
||||
const admissionStatus = admission.status;
|
||||
if (
|
||||
admissionStatus !== "ready-for-download"
|
||||
&& admissionStatus !== "blocked-storage-policy"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("source admission status неизвестен");
|
||||
}
|
||||
const representations = array(
|
||||
source.representations,
|
||||
"representations",
|
||||
).map((value, index) => {
|
||||
const item = record(value, `representations[${index}]`);
|
||||
const id = item.id;
|
||||
if (
|
||||
id !== "native-scan"
|
||||
&& id !== "normalized-scan"
|
||||
&& id !== "rolling-local-map"
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Неизвестная LiDAR representation");
|
||||
}
|
||||
const normalizedId: DatasetRepresentationId = id;
|
||||
return {
|
||||
id: normalizedId,
|
||||
title: string(item.title, "representation.title"),
|
||||
purpose: string(item.purpose, "representation.purpose", true),
|
||||
accumulation: boolean(item.accumulation, "representation.accumulation"),
|
||||
};
|
||||
});
|
||||
const pipeline = array(source.pipeline, "pipeline").map((value, index) => {
|
||||
const item = record(value, `pipeline[${index}]`);
|
||||
return {
|
||||
stage: string(item.stage, "pipeline.stage", true),
|
||||
requires: strings(item.requires, "pipeline.requires"),
|
||||
produces: string(item.produces, "pipeline.produces", true),
|
||||
};
|
||||
});
|
||||
const inputs = array(source.known_inputs, "known_inputs");
|
||||
const currentInput = record(inputs[0], "known_inputs[0]");
|
||||
if (
|
||||
currentInput.representation !== "vendor-mapped-increment"
|
||||
|| currentInput.native_scan !== false
|
||||
|| currentInput.per_point_time !== false
|
||||
|| currentInput.ring_or_line !== false
|
||||
|| currentInput.admitted_for_patchworkpp !== false
|
||||
) {
|
||||
throw new DatasetGatewayContractError("Vendor-map boundary завышен");
|
||||
}
|
||||
if (dataset.frame_semantics !== "one-lidar-revolution") {
|
||||
throw new DatasetGatewayContractError("GOOSE frame semantics несовместима");
|
||||
}
|
||||
return {
|
||||
storage: {
|
||||
configured: boolean(storage.configured, "storage.configured"),
|
||||
admitted: boolean(storage.admitted, "storage.admitted"),
|
||||
status: storageStatus,
|
||||
requiredWindowsRoot: string(
|
||||
storage.required_windows_root,
|
||||
"storage.required_windows_root",
|
||||
),
|
||||
requiredWslRoot: string(storage.required_wsl_root, "storage.required_wsl_root"),
|
||||
},
|
||||
source: {
|
||||
sourceId: string(dataset.source_id, "source_id", true),
|
||||
displayName: string(dataset.display_name, "display_name"),
|
||||
role: string(dataset.role, "role", true),
|
||||
license: string(dataset.license, "license"),
|
||||
format: string(dataset.format, "format", true),
|
||||
frameSemantics: "one-lidar-revolution",
|
||||
platforms: displayStrings(dataset.platforms, "platforms"),
|
||||
superclasses: strings(dataset.superclasses, "superclasses"),
|
||||
validationArchiveGb: number(
|
||||
download.validation_archive_gb,
|
||||
"validation_archive_gb",
|
||||
),
|
||||
admissionStatus,
|
||||
},
|
||||
representations,
|
||||
pipeline,
|
||||
currentInput: {
|
||||
representation: "vendor-mapped-increment",
|
||||
nativeScan: false,
|
||||
perPointTime: false,
|
||||
ringOrLine: false,
|
||||
admittedForPatchworkpp: false,
|
||||
reason: string(currentInput.reason, "known_inputs.reason", true),
|
||||
},
|
||||
nextAction: string(source.next_action, "next_action", true),
|
||||
};
|
||||
}
|
||||
|
||||
async function responseJson(response: Response): Promise<unknown> {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Dataset Gateway HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function fetchDatasetGatewayCatalog(
|
||||
options: { signal?: AbortSignal; fetcher?: DatasetFetch } = {},
|
||||
): Promise<DatasetGatewayCatalog> {
|
||||
const fetcher = options.fetcher ?? fetch;
|
||||
const response = await fetcher("/api/v1/lidar/dataset-gateway", {
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" },
|
||||
signal: options.signal,
|
||||
});
|
||||
return parseDatasetGatewayCatalog(await responseJson(response));
|
||||
}
|
||||
Reference in New Issue
Block a user