diff --git a/packages/frontend/src/components/json_import.tsx b/packages/frontend/src/components/json_import.tsx index 464e5e00d..1016b404d 100644 --- a/packages/frontend/src/components/json_import.tsx +++ b/packages/frontend/src/components/json_import.tsx @@ -6,8 +6,8 @@ import type { Document } from "catlog-wasm"; import "./json_import.css"; interface JsonImportProps { - onImport: (data: Document) => void; - validate?: (data: Document) => boolean | string; + onImport: (doc: Document) => void; + parse: (jsonString: string) => Document | Error; } /** @@ -20,30 +20,16 @@ export const JsonImport = (props: JsonImportProps) => { const [error, setError] = createSignal(null); const [importValue, setImportValue] = createSignal(""); - const handleError = (e: unknown) => { - setError(e instanceof Error ? e.message : "Unknown error occurred"); - }; - - const validateAndImport = (jsonString: string) => { - try { - const data = JSON.parse(jsonString); - - // Run custom validation if provided - if (props.validate) { - const validationResult = props.validate(data); - if (typeof validationResult === "string") { - setError(validationResult); - return; - } - } - - // Clear any previous errors and import - setError(null); - props.onImport(data); - setImportValue(""); // Clear paste area after successful import - } catch (e) { - handleError(e); + const parseAndImport = async (jsonString: string) => { + const result = props.parse(jsonString); + if (result instanceof Error) { + setError(result.message); + return; } + // Clear any previous errors and import + setError(null); + props.onImport(result); + setImportValue(""); // Clear paste area after successful import }; // Handle file upload @@ -68,7 +54,7 @@ export const JsonImport = (props: JsonImportProps) => { } const text = await file.text(); - validateAndImport(text); + void parseAndImport(text); // Reset file input input.value = ""; @@ -80,7 +66,7 @@ export const JsonImport = (props: JsonImportProps) => { setError("Please enter some JSON"); return; } - validateAndImport(importValue()); + void parseAndImport(importValue()); }; const handleInput: JSX.EventHandler = (event) => { diff --git a/packages/frontend/src/page/import_document.tsx b/packages/frontend/src/page/import_document.tsx index e7f61947f..36290a2fa 100644 --- a/packages/frontend/src/page/import_document.tsx +++ b/packages/frontend/src/page/import_document.tsx @@ -1,9 +1,9 @@ import { useNavigate } from "@solidjs/router"; -import invariant from "tiny-invariant"; import type { Document } from "catlog-wasm"; import { useApi } from "../api"; import { JsonImport } from "../components"; +import { convertFromPetrinaut, isFromPetrinaut } from "./import_from_petrinaut"; const isImportableDocument = (doc: Document) => doc.type === "model" || doc.type === "diagram"; @@ -13,25 +13,31 @@ export function ImportDocument(props: { onComplete?: () => void }) { const navigate = useNavigate(); const handleImport = async (data: Document) => { - invariant( - isImportableDocument(data), - "Only models and diagrams are importable at this time", - ); - const newRef = await api.createDoc(data); navigate(`/${data.type}/${newRef}`); props.onComplete?.(); }; - // Placeholder, not doing more than typechecking does for now but - // will eventually validate against json schema - const validateJson = (data: Document) => { - if (!isImportableDocument(data)) { - return "Only models and diagrams are importable at this time"; + const parseDoc = (inputString: string): Document | Error => { + let doc: Document; + try { + doc = JSON.parse(inputString); + } catch { + return Error("Invalid JSON"); + } + if (isFromPetrinaut(doc)) { + try { + return convertFromPetrinaut(doc); + } catch { + return Error("Petrinaut file detected but the JSON appears invalid"); + } + } + if (!isImportableDocument(doc)) { + return Error("Only models and diagrams are importable at this time"); } - return true; + return doc; }; - return ; + return ; } diff --git a/packages/frontend/src/page/import_from_petrinaut.ts b/packages/frontend/src/page/import_from_petrinaut.ts new file mode 100644 index 000000000..7272e4023 --- /dev/null +++ b/packages/frontend/src/page/import_from_petrinaut.ts @@ -0,0 +1,120 @@ +import { v7 } from "uuid"; + +import { + currentVersion, + type Document, + type ModelJudgment, + type NotebookCell, + type Ob, +} from "catlog-wasm"; + +/** Detects a Petrinaut-exported JSON file. */ +export function isFromPetrinaut(data: unknown): boolean { + if (typeof data !== "object" || data === null) { + return false; + } + const { meta } = data as Record; + if (typeof meta !== "object" || meta === null) { + return false; + } + const { generator } = meta as Record; + return generator === "Petrinaut"; +} + +// Petrinaut schema fragment that we're interested in + +type PetrinautArc = { placeId: string }; + +type PetrinautPlace = { id: string; name: string }; + +type PetrinautTransition = { + id: string; + name: string; + inputArcs: PetrinautArc[]; + outputArcs: PetrinautArc[]; +}; + +type PetrinautFile = { + title: string; + places: PetrinautPlace[]; + transitions: PetrinautTransition[]; +}; + +function tensorOb(contentIds: string[]): Ob { + return { + tag: "App", + content: { + op: { tag: "Basic", content: "tensor" }, + ob: { + tag: "List", + content: { + modality: "SymmetricList", + objects: contentIds.map((id) => ({ tag: "Basic", content: id })), + }, + }, + }, + }; +} + +/** Converts a Petrinaut-exported JSON file to a CatCoLab petri-net model document. */ +export function convertFromPetrinaut(data: unknown): Document { + const { title, places, transitions } = data as PetrinautFile; + + const placeIds = new Map(); + for (const place of places) { + placeIds.set(place.id, { cellId: v7(), contentId: v7() }); + } + + const cellOrder: string[] = []; + const cellContents: Record> = {}; + + for (const place of places) { + const { cellId, contentId } = placeIds.get(place.id)!; + cellOrder.push(cellId); + cellContents[cellId] = { + id: cellId, + tag: "formal", + content: { + tag: "object", + id: contentId, + name: place.name, + obType: { tag: "Basic" as const, content: "Object" }, + }, + }; + } + + for (const transition of transitions) { + const cellId = v7(); + const contentId = v7(); + const domContentIds = transition.inputArcs.map( + (arc) => placeIds.get(arc.placeId)!.contentId, + ); + const codContentIds = transition.outputArcs.map( + (arc) => placeIds.get(arc.placeId)!.contentId, + ); + cellOrder.push(cellId); + cellContents[cellId] = { + id: cellId, + tag: "formal", + content: { + tag: "morphism", + id: contentId, + name: transition.name, + morType: { + tag: "Hom" as const, + content: { tag: "Basic" as const, content: "Object" }, + }, + dom: tensorOb(domContentIds), + cod: tensorOb(codContentIds), + }, + }; + } + + return { + type: "model", + theory: "petri-net", + name: title, + version: currentVersion(), + notebook: { cellOrder, cellContents }, + }; +}