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
231 changes: 231 additions & 0 deletions src/lib/rdf/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,235 @@ 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/);
});
});

describe('rdfs:subClassOf hierarchy (#101)', () => {
// Taxonomy-shaped ontologies (e.g. Brick) carry their graph structure in
// rdfs:subClassOf rather than object-property domain/range pairs.
const hierarchyRdf = `<?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#">

<owl:Class rdf:about="http://example.org/h/Equipment">
<rdfs:label>Equipment</rdfs:label>
</owl:Class>

<owl:Class rdf:about="http://example.org/h/HVAC">
<rdfs:label>HVAC</rdfs:label>
<rdfs:subClassOf rdf:resource="http://example.org/h/Equipment"/>
</owl:Class>

<owl:Class rdf:about="http://example.org/h/AHU">
<rdfs:label>AHU</rdfs:label>
<rdfs:subClassOf rdf:resource="http://example.org/h/HVAC"/>
</owl:Class>

</rdf:RDF>`;

it('extracts subClassOf references as relationships', () => {
const { ontology } = parseRDF(hierarchyRdf);
expect(ontology.relationships).toHaveLength(2);
expect(ontology.relationships).toContainEqual({
id: 'hVAC-subClassOf-equipment',
name: 'subClassOf',
from: 'hVAC',
to: 'equipment',
cardinality: 'many-to-one',
});
expect(ontology.relationships).toContainEqual({
id: 'aHU-subClassOf-hVAC',
name: 'subClassOf',
from: 'aHU',
to: 'hVAC',
cardinality: 'many-to-one',
});
});

it('extracts subClassOf from rdf:Description-typed classes (Brick style)', () => {
const rdf = `<?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/h/Equipment">
<rdf:type rdf:resource="http://www.w3.org/2002/07/owl#Class"/>
<rdfs:label>Equipment</rdfs:label>
</rdf:Description>
<rdf:Description rdf:about="http://example.org/h/Fan">
<rdf:type rdf:resource="http://www.w3.org/2002/07/owl#Class"/>
<rdfs:label>Fan</rdfs:label>
<rdfs:subClassOf rdf:resource="http://example.org/h/Equipment"/>
</rdf:Description>
</rdf:RDF>`;
const { ontology } = parseRDF(rdf);
expect(ontology.relationships).toHaveLength(1);
expect(ontology.relationships[0]).toMatchObject({
name: 'subClassOf',
from: 'fan',
to: 'equipment',
});
});

it('supports multiple parents (multiple inheritance)', () => {
const rdf = hierarchyRdf.replace(
'<rdfs:subClassOf rdf:resource="http://example.org/h/HVAC"/>',
`<rdfs:subClassOf rdf:resource="http://example.org/h/HVAC"/>
<rdfs:subClassOf rdf:resource="http://example.org/h/Equipment"/>`
);
const { ontology } = parseRDF(rdf);
expect(ontology.relationships).toHaveLength(3);
const ahuParents = ontology.relationships
.filter((r) => r.from === 'aHU')
.map((r) => r.to)
.sort();
expect(ahuParents).toEqual(['equipment', 'hVAC']);
});

it('skips references to classes not present in the document', () => {
const rdf = hierarchyRdf.replace(
'http://example.org/h/Equipment"/>',
'http://www.w3.org/2002/07/owl#Thing"/>'
);
const { ontology } = parseRDF(rdf);
// HVAC → owl:Thing dropped; AHU → HVAC kept
expect(ontology.relationships).toHaveLength(1);
expect(ontology.relationships[0]).toMatchObject({ from: 'aHU', to: 'hVAC' });
});

it('skips self-references and duplicate edges', () => {
const rdf = hierarchyRdf.replace(
'<rdfs:subClassOf rdf:resource="http://example.org/h/Equipment"/>',
`<rdfs:subClassOf rdf:resource="http://example.org/h/Equipment"/>
<rdfs:subClassOf rdf:resource="http://example.org/h/Equipment"/>
<rdfs:subClassOf rdf:resource="http://example.org/h/HVAC"/>`
);
const { ontology } = parseRDF(rdf);
expect(ontology.relationships).toHaveLength(2);
});

it('ignores nested owl:Restriction subClassOf nodes', () => {
const rdf = hierarchyRdf.replace(
'<rdfs:subClassOf rdf:resource="http://example.org/h/HVAC"/>',
`<rdfs:subClassOf rdf:resource="http://example.org/h/HVAC"/>
<rdfs:subClassOf>
<owl:Restriction>
<owl:onProperty rdf:resource="http://example.org/h/hasPart"/>
<owl:someValuesFrom rdf:resource="http://example.org/h/Equipment"/>
</owl:Restriction>
</rdfs:subClassOf>`
);
const { ontology } = parseRDF(rdf);
expect(ontology.relationships).toHaveLength(2);
});

it('coexists with object-property relationships', () => {
const rdf = hierarchyRdf.replace(
'</rdf:RDF>',
`<owl:ObjectProperty rdf:about="http://example.org/h/feeds">
<rdfs:label>feeds</rdfs:label>
<rdfs:domain rdf:resource="http://example.org/h/AHU"/>
<rdfs:range rdf:resource="http://example.org/h/HVAC"/>
</owl:ObjectProperty></rdf:RDF>`
);
const { ontology } = parseRDF(rdf);
expect(ontology.relationships).toHaveLength(3);
expect(ontology.relationships.map((r) => r.name)).toContain('feeds');
});
});
});
Loading