Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/lib/rdf/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,99 @@ describe('parseRDF', () => {
expect(ontology.entityTypes[0].name).toBe('Widget');
});
});

describe('rdf:Description typed-node syntax (#85)', () => {
// Serializers like Python's rdflib (used by Brick and many published
// ontologies) declare resources as <rdf:Description> with an rdf:type
// child instead of typed elements like <owl:Class>.
const descriptionRdf = `<?xml version="1.0" encoding="UTF-8"?>
<rdf:RDF
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
xmlns:owl="http://www.w3.org/2002/07/owl#">

<rdf:Description rdf:about="http://example.org/desc/">
<rdf:type rdf:resource="http://www.w3.org/2002/07/owl#Ontology"/>
<rdfs:label>Description Ontology</rdfs:label>
<rdfs:comment>Serialized via rdf:Description</rdfs:comment>
</rdf:Description>

<rdf:Description rdf:about="http://example.org/desc/Building">
<rdf:type rdf:resource="http://www.w3.org/2002/07/owl#Class"/>
<rdfs:label>Building</rdfs:label>
<rdfs:comment>A building</rdfs:comment>
</rdf:Description>

<rdf:Description rdf:about="http://example.org/desc/Sensor">
<rdf:type rdf:resource="http://www.w3.org/2002/07/owl#Class"/>
<rdfs:label>Sensor</rdfs:label>
</rdf:Description>

<rdf:Description rdf:about="http://example.org/desc/building_name">
<rdf:type rdf:resource="http://www.w3.org/2002/07/owl#DatatypeProperty"/>
<rdfs:label>name</rdfs:label>
<rdfs:domain rdf:resource="http://example.org/desc/Building"/>
<rdfs:range rdf:resource="http://www.w3.org/2001/XMLSchema#string"/>
</rdf:Description>

<rdf:Description rdf:about="http://example.org/desc/hasSensor">
<rdf:type rdf:resource="http://www.w3.org/2002/07/owl#ObjectProperty"/>
<rdfs:label>hasSensor</rdfs:label>
<rdfs:domain rdf:resource="http://example.org/desc/Building"/>
<rdfs:range rdf:resource="http://example.org/desc/Sensor"/>
</rdf:Description>

</rdf:RDF>`;

it('extracts ontology metadata from rdf:Description elements', () => {
const { ontology } = parseRDF(descriptionRdf);
expect(ontology.name).toBe('Description Ontology');
expect(ontology.description).toBe('Serialized via rdf:Description');
});

it('extracts classes declared via rdf:type', () => {
const { ontology } = parseRDF(descriptionRdf);
expect(ontology.entityTypes).toHaveLength(2);
const names = ontology.entityTypes.map((e) => e.name);
expect(names).toContain('Building');
expect(names).toContain('Sensor');
});

it('extracts datatype properties declared via rdf:type', () => {
const { ontology } = parseRDF(descriptionRdf);
const building = ontology.entityTypes.find((e) => e.name === 'Building');
expect(building?.properties).toHaveLength(1);
expect(building?.properties[0]).toMatchObject({ name: 'name', type: 'string' });
});

it('extracts object properties declared via rdf:type', () => {
const { ontology } = parseRDF(descriptionRdf);
expect(ontology.relationships).toHaveLength(1);
expect(ontology.relationships[0]).toMatchObject({ from: 'building', to: 'sensor' });
});

it('does not duplicate entities when typed elements and descriptions coexist', () => {
const mixed = descriptionRdf.replace(
'</rdf:RDF>',
`<owl:Class rdf:about="http://example.org/desc/Building">
<rdfs:label>Building</rdfs:label>
</owl:Class></rdf:RDF>`
);
const { ontology } = parseRDF(mixed);
expect(ontology.entityTypes).toHaveLength(2);
});
});

describe('Turtle input detection (#85)', () => {
it('gives an actionable error for Turtle content', () => {
const ttl = `@prefix brick: <https://brickschema.org/schema/Brick#> .\n@prefix owl: <http://www.w3.org/2002/07/owl#> .\n\nbrick:Building a owl:Class .`;
expect(() => parseRDF(ttl)).toThrow(RDFParseError);
expect(() => parseRDF(ttl)).toThrow(/Turtle/);
});

it('still reports malformed XML for non-Turtle garbage', () => {
expect(() => parseRDF('this is not xml at all')).toThrow(RDFParseError);
expect(() => parseRDF('this is not xml at all')).toThrow(/Malformed XML|No ontology metadata/);
});
});
});
60 changes: 54 additions & 6 deletions src/lib/rdf/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,41 @@ const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
const OWL_NS = 'http://www.w3.org/2002/07/owl#';

/**
* Collect all elements declaring a resource of the given type, supporting both
* RDF/XML syntaxes:
*
* 1. Typed node elements: `<owl:Class rdf:about="...">`
* 2. rdf:Description elements: `<rdf:Description rdf:about="...">`
* ` <rdf:type rdf:resource=".../owl#Class"/>`
*
* Many serializers (e.g. Python's rdflib, used by Brick and other published
* ontologies) emit only the second form, which we previously ignored (#85).
*/
function getTypedElements(root: Element, namespace: string, localName: string): Element[] {
const results: Element[] = Array.from(root.getElementsByTagNameNS(namespace, localName));
const typeUri = namespace + localName;

// Snapshot the live collection: indexed access on live collections is
// expensive in some DOM implementations (quadratic in jsdom).
const descEls = Array.from(root.getElementsByTagNameNS(RDF_NS, 'Description'));
for (let i = 0; i < descEls.length; i++) {
const el = descEls[i];
for (let j = 0; j < el.children.length; j++) {
const child = el.children[j];
const childLocal = child.localName || child.tagName.split(':').pop();
if (childLocal !== 'type') continue;
const resource =
child.getAttribute('rdf:resource') || child.getAttributeNS(RDF_NS, 'resource');
if (resource === typeUri) {
results.push(el);
break;
}
}
}
return results;
}

interface ParsedDatatypeProperty {
about: string;
label: string;
Expand All @@ -143,6 +178,15 @@ interface ParsedDatatypeProperty {
* Parse an RDF/XML (OWL) string into an Ontology and optional DataBindings.
*/
export function parseRDF(rdfXml: string): { ontology: Ontology; bindings: DataBinding[] } {
// Give Turtle input a clear, actionable error instead of an XML parse error.
const sniff = rdfXml.trimStart();
if (!sniff.startsWith('<') && /^(@prefix|@base|PREFIX\s|BASE\s)/m.test(sniff)) {
throw new RDFParseError(
'This file appears to be Turtle (.ttl), which is not supported yet. ' +
'Please convert it to RDF/XML first (for example with Apache Jena "riot" or an online RDF converter).'
);
}

const parser = new DOMParser();
const doc = parser.parseFromString(rdfXml, 'application/xml');

Expand All @@ -158,15 +202,15 @@ export function parseRDF(rdfXml: string): { ontology: Ontology; bindings: DataBi
let ontologyName = '';
let ontologyDescription = '';

const ontologyEls = root.getElementsByTagNameNS(OWL_NS, 'Ontology');
const ontologyEls = getTypedElements(root, OWL_NS, 'Ontology');
if (ontologyEls.length > 0) {
const ontEl = ontologyEls[0];
ontologyName = getChildText(ontEl, 'label', RDFS_NS) || '';
ontologyDescription = getChildText(ontEl, 'comment', RDFS_NS) || '';
}

// --- Extract OWL Classes → EntityTypes ---
const classEls = root.getElementsByTagNameNS(OWL_NS, 'Class');
const classEls = getTypedElements(root, OWL_NS, 'Class');
const entityMap = new Map<string, EntityType>();

for (let i = 0; i < classEls.length; i++) {
Expand All @@ -192,7 +236,7 @@ export function parseRDF(rdfXml: string): { ontology: Ontology; bindings: DataBi
}

// --- Extract DatatypeProperties → Properties + Relationship Attributes ---
const dtPropEls = root.getElementsByTagNameNS(OWL_NS, 'DatatypeProperty');
const dtPropEls = getTypedElements(root, OWL_NS, 'DatatypeProperty');
const parsedDtProps: ParsedDatatypeProperty[] = [];

for (let i = 0; i < dtPropEls.length; i++) {
Expand Down Expand Up @@ -268,7 +312,7 @@ export function parseRDF(rdfXml: string): { ontology: Ontology; bindings: DataBi
}

// --- Extract ObjectProperties → Relationships ---
const objPropEls = root.getElementsByTagNameNS(OWL_NS, 'ObjectProperty');
const objPropEls = getTypedElements(root, OWL_NS, 'ObjectProperty');
const relationships: Relationship[] = [];

for (let i = 0; i < objPropEls.length; i++) {
Expand Down Expand Up @@ -323,8 +367,12 @@ export function parseRDF(rdfXml: string): { ontology: Ontology; bindings: DataBi

// --- Extract DataBindings ---
const bindings: DataBinding[] = [];
// Look for ont:DataBinding elements (they use the ontology namespace)
const allElements = root.getElementsByTagName('*');
// Look for ont:DataBinding elements (they use the ontology namespace).
// Use a static snapshot instead of the live getElementsByTagName('*')
// collection: repeated indexed access on live collections is quadratic in
// some DOM implementations, which made importing large ontologies
// (e.g. Brick, ~500k elements) pathologically slow.
const allElements = root.querySelectorAll('*');
for (let i = 0; i < allElements.length; i++) {
const el = allElements[i];
const localName = el.localName || el.tagName.split(':').pop();
Expand Down