diff --git a/packages/okf/src/ossie/detect.ts b/packages/okf/src/ossie/detect.ts index f96b7fcd..ac7a3d79 100644 --- a/packages/okf/src/ossie/detect.ts +++ b/packages/okf/src/ossie/detect.ts @@ -19,7 +19,8 @@ export function detectModelFormat(input: { fileName?: string; text: string }): M const first = (lines[i] ?? "").trimStart(); const looksOssie = first.startsWith("{") || OSSIE_KEY.test(first); // Frontmatter in OKF docs can also start with `name:` / `version:`, so also - // require a top-level `datasets` / `semantic_model` key. - const hasModelKey = /^(datasets|semantic_model)\s*:/m.test(lines.join("\n")) || /"(datasets|semantic_model)"\s*:/.test(text); + // require a top-level Ossie key. Ontology and mapping documents count too: + // the Ossie reader explains that they aren't semantic models. + const hasModelKey = /^(datasets|semantic_model|ontology|concept_mappings)\s*:/m.test(lines.join("\n")) || /"(datasets|semantic_model|ontology|concept_mappings)"\s*:/.test(text); return looksOssie && hasModelKey ? "ossie" : "okf"; } diff --git a/packages/okf/src/ossie/parse.ts b/packages/okf/src/ossie/parse.ts index 44c0fdd9..e66ad478 100644 --- a/packages/okf/src/ossie/parse.ts +++ b/packages/okf/src/ossie/parse.ts @@ -42,6 +42,23 @@ export function parseOssie(text: string): OssieImport { } } +const USE_SEMANTIC_MODEL = 'The canvas imports semantic models: import the file with a top-level "datasets" list instead.'; + +// Ossie also has ontology documents (business concepts) and mapping documents +// (ontology ↔ semantic model). Neither describes datasets, so say what the file +// is instead of a bare "not an Ossie model". +function notSemanticModelMessage(obj: Record): string { + if (Array.isArray(obj.concept_mappings)) { + const ref = isObj(obj.semantic_model_ref) ? obj.semantic_model_ref.iri : undefined; + const iri = typeof ref === "string" ? ref.trim().slice(0, 200) : ""; + return iri + ? `This is an Apache Ossie mapping document, not a semantic model. Import the semantic model it points to instead: ${iri}` + : `This is an Apache Ossie mapping document, not a semantic model. ${USE_SEMANTIC_MODEL}`; + } + if (Array.isArray(obj.ontology)) return `This is an Apache Ossie ontology (business concepts), not a semantic model. ${USE_SEMANTIC_MODEL}`; + return "This file isn't an Ossie model"; +} + function convert(text: string): OssieImport { let raw: unknown; try { raw = YAML.parse(text, { maxAliasCount: 100 }); } @@ -60,7 +77,7 @@ function convert(text: string): OssieImport { } else if (Array.isArray(obj.datasets)) { doc = obj as unknown as OssieDoc; } else { - throw new Error("This file isn't an Ossie model"); + throw new Error(notSemanticModelMessage(obj)); } const vendors: string[] = []; diff --git a/packages/okf/test/ossie-detect.test.ts b/packages/okf/test/ossie-detect.test.ts index 00dc5214..acc14fbf 100644 --- a/packages/okf/test/ossie-detect.test.ts +++ b/packages/okf/test/ossie-detect.test.ts @@ -15,4 +15,8 @@ describe("detectModelFormat", () => { expect(detectModelFormat({ text: "---\nname: Orders\ntitle: Orders\n---\n# Orders" })).toBe("okf"); expect(detectModelFormat({ text: "\n---\ntitle: A\n---" })).toBe("okf"); }); + it("routes pasted Ossie ontologies and mappings to the Ossie reader (which explains them)", () => { + expect(detectModelFormat({ text: "version: 0.2.0.dev0\nname: Flights\nontology:\n- concept: Airport" })).toBe("ossie"); + expect(detectModelFormat({ text: "version: 0.2.0.dev0\nname: m\nconcept_mappings:\n- concept: Runway" })).toBe("ossie"); + }); }); diff --git a/packages/okf/test/ossie-parse.test.ts b/packages/okf/test/ossie-parse.test.ts index b90f6f31..5b05a706 100644 --- a/packages/okf/test/ossie-parse.test.ts +++ b/packages/okf/test/ossie-parse.test.ts @@ -93,3 +93,25 @@ describe("parseOssie — malformed but parseable input", () => { expect(r.graph.edges[0].keys).toEqual([{ left: "a", right: "c" }]); }); }); + +describe("parseOssie — Ossie documents that aren't semantic models", () => { + const ontology = "version: 0.2.0.dev0\nname: Flights\nontology:\n- concept: Airport\n type: EntityType\n"; + const mapping = "version: 0.2.0.dev0\nname: flights_mapping\nontology_ref:\n name: Flights\n iri: ./flights.ontology.yaml\nsemantic_model_ref:\n name: Flights semantic model\n iri: ./flights.semantic_model.yaml\nconcept_mappings:\n- concept: Runway\n"; + it("explains that an ontology isn't a semantic model", () => { + expect(() => parseOssie(ontology)).toThrow(/Apache Ossie ontology/); + expect(() => parseOssie(ontology)).toThrow(/top-level "datasets" list/); + }); + it("treats the legacy combined file (ontology + ontology_mappings) as an ontology", () => { + expect(() => parseOssie(`${ontology}ontology_mappings:\n- name: m\n`)).toThrow(/Apache Ossie ontology/); + }); + it("points a mapping document at the semantic model it references", () => { + expect(() => parseOssie(mapping)).toThrow(/Apache Ossie mapping/); + expect(() => parseOssie(mapping)).toThrow(/flights\.semantic_model\.yaml/); + }); + it("still explains a mapping without a usable semantic model reference", () => { + expect(() => parseOssie("concept_mappings: []\nsemantic_model_ref: 5\n")).toThrow(/Apache Ossie mapping.*top-level "datasets" list/); + }); + it("keeps the generic error for anything else", () => { + expect(() => parseOssie("ontology: nope")).toThrow("This file isn't an Ossie model"); + }); +}); diff --git a/packages/web/public/ossie-format.md b/packages/web/public/ossie-format.md index 3219128c..865d5ec0 100644 --- a/packages/web/public/ossie-format.md +++ b/packages/web/public/ossie-format.md @@ -15,6 +15,8 @@ The canvas reads the flat `0.2.x` document and the legacy `semantic_model: [ ... ]` wrapper (YAML or JSON). With several models in one file, it imports the first and tells you how many it skipped. +**Ontologies and mappings are not imported yet.** Ossie also defines ontology documents (`ontology:` — business concepts, see [ontology.md](https://github.com/apache/ossie/blob/main/ontology/ontology.md)) and mapping documents (`concept_mappings:` — ontology ↔ semantic model). They describe no datasets, so the import dialog says what the file is and asks for the semantic model instead. For a mapping document it names the semantic model the mapping points to (`semantic_model_ref.iri`). In the Ossie examples, `flights.semantic_model.yaml` imports; `flights.ontology.yaml`, `flights.mapping.yaml` and `flights.yaml` do not. + --- ## What the canvas reads diff --git a/packages/web/src/okf/io.test.ts b/packages/web/src/okf/io.test.ts index 0095fc51..671b0069 100644 --- a/packages/web/src/okf/io.test.ts +++ b/packages/web/src/okf/io.test.ts @@ -53,6 +53,11 @@ describe("loadModel*", () => { await expect(loadModelFiles({ "m.yaml": OSSIE, "a.md": "---\ntitle: A\n---" })).rejects.toThrow("Import one format at a time"); await expect(loadModelFiles({ "a.json": '{"a":1}' })).rejects.toThrow("This file isn't an Ossie model"); }); + it("explains an Ossie ontology from a file or pasted text", async () => { + const onto = "version: 0.2.0.dev0\nname: Flights\nontology:\n- concept: Airport\n type: EntityType\n"; + await expect(loadModelFiles({ "flights.ontology.yaml": onto })).rejects.toThrow(/Apache Ossie ontology/); + await expect(loadModelText(onto)).rejects.toThrow(/Apache Ossie ontology/); + }); }); describe("size cap", () => {