feat: implement RDFVocabularyMapper for ExoRDF to RDF/RDFS mapping#406
feat: implement RDFVocabularyMapper for ExoRDF to RDF/RDFS mapping#406
Conversation
Implements class and property hierarchy mapping between ExoRDF and W3C RDF/RDFS standards. **Changes:** - Add RDFVocabularyMapper with class hierarchy generation (exo:Asset → rdfs:Resource) - Add property hierarchy generation (exo:Instance_class → rdf:type) - Add individual property mapping with generateMappedTriple() - Add mapping detection utility hasMappingFor() - Export RDFVocabularyMapper from core package **Testing:** - 17 comprehensive test cases covering all mapping scenarios - Tests for unmapped properties returning null - Tests for string and IRI value handling - All tests passing locally **Implementation follows:** - ExoRDF-Mapping.md specification - TDD methodology (tests written first) - Storage-agnostic design pattern Related: #367
There was a problem hiding this comment.
Pull Request Overview
This PR implements RDFVocabularyMapper to provide semantic interoperability between Exocortex's ExoRDF framework and W3C RDF/RDFS standards. The mapper generates triples that express class and property hierarchies, enabling standard SPARQL queries and semantic web tool compatibility.
- Core functionality includes mapping 6 ExoRDF classes and 6 properties to their RDF/RDFS counterparts
- Follows the ExoRDF-Mapping.md specification for subclass/subproperty relationships
- Storage-agnostic implementation with comprehensive test coverage
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
| packages/core/src/infrastructure/rdf/RDFVocabularyMapper.ts | Implements RDFVocabularyMapper with methods for generating class/property hierarchy triples and mapping individual properties |
| packages/core/tests/unit/infrastructure/rdf/RDFVocabularyMapper.test.ts | Adds 17 comprehensive test cases covering all mapping functionality |
| packages/core/src/index.ts | Exports RDFVocabularyMapper from core package for reusability |
| import { Triple } from "../../domain/models/rdf/Triple"; | ||
| import { IRI } from "../../domain/models/rdf/IRI"; | ||
| import { Namespace } from "../../domain/models/rdf/Namespace"; | ||
|
|
There was a problem hiding this comment.
The RDFVocabularyMapper class lacks JSDoc documentation. According to the project's documentation standards (see NoteToRDFConverter as an example), public classes and methods should include JSDoc comments with descriptions, parameters, return types, and usage examples. This is especially important for a core infrastructure component that will be exported and used by other packages.
Consider adding documentation like:
/**
* Maps ExoRDF vocabulary to W3C RDF/RDFS standards for semantic interoperability.
*
* Generates triples that express the relationship between ExoRDF classes/properties
* and their RDF/RDFS superclasses/superproperties according to the ExoRDF-Mapping specification.
*
* @example
* ```typescript
* const mapper = new RDFVocabularyMapper();
* const classTriples = mapper.generateClassHierarchyTriples();
* const propertyTriples = mapper.generatePropertyHierarchyTriples();
* ```
*/
export class RDFVocabularyMapper {
// ...
}| /** | |
| * Maps ExoRDF vocabulary to W3C RDF/RDFS standards for semantic interoperability. | |
| * | |
| * Generates triples that express the relationship between ExoRDF classes/properties | |
| * and their RDF/RDFS superclasses/superproperties according to the ExoRDF-Mapping specification. | |
| * | |
| * @example | |
| * ```typescript | |
| * const mapper = new RDFVocabularyMapper(); | |
| * const classTriples = mapper.generateClassHierarchyTriples(); | |
| * const propertyTriples = mapper.generatePropertyHierarchyTriples(); | |
| * ``` | |
| */ |
|
|
||
| return triples; | ||
| } | ||
|
|
There was a problem hiding this comment.
The generatePropertyHierarchyTriples() method lacks JSDoc documentation. Public methods should be documented with descriptions, return types, and examples.
Consider adding:
/**
* Generates RDF triples that define the property hierarchy mappings between
* ExoRDF properties and their RDF/RDFS superproperties.
*
* @returns Array of triples expressing rdfs:subPropertyOf relationships
*
* @example
* ```typescript
* const triples = mapper.generatePropertyHierarchyTriples();
* // Returns triples like: exo:Instance_class rdfs:subPropertyOf rdf:type
* ```
*/| /** | |
| * Generates RDF triples that define the property hierarchy mappings between | |
| * ExoRDF properties and their RDF/RDFS superproperties. | |
| * | |
| * @returns {Triple[]} Array of triples expressing rdfs:subPropertyOf relationships | |
| * | |
| * @example | |
| * ```typescript | |
| * const mapper = new RDFVocabularyMapper(); | |
| * const triples = mapper.generatePropertyHierarchyTriples(); | |
| * // Returns triples like: exo:Instance_class rdfs:subPropertyOf rdf:type | |
| * ``` | |
| */ |
|
|
||
| return triples; | ||
| } | ||
|
|
There was a problem hiding this comment.
The generateMappedTriple() method lacks JSDoc documentation. This is a key public API method that requires clear documentation explaining its purpose, parameters, return value, and edge cases.
Consider adding:
/**
* Generates an RDF/RDFS triple for an individual ExoRDF property value.
*
* Maps ExoRDF property names to their RDF/RDFS equivalents and creates
* a triple using the mapped predicate. Returns null if no mapping exists.
*
* @param subject - The subject IRI for the triple
* @param exoProperty - ExoRDF property name (e.g., "exo__Instance_class")
* @param value - Property value as string or IRI
* @returns Triple with mapped RDF/RDFS predicate, or null if unmapped
*
* @example
* ```typescript
* const triple = mapper.generateMappedTriple(
* new IRI("https://example.org/asset"),
* "exo__Instance_class",
* "ems__Task"
* );
* // Returns: <asset> rdf:type ems:Task
* ```
*/| /** | |
| * Generates an RDF/RDFS triple for an individual ExoRDF property value. | |
| * | |
| * Maps ExoRDF property names to their RDF/RDFS equivalents and creates | |
| * a triple using the mapped predicate. Returns null if no mapping exists. | |
| * | |
| * @param subject - The subject IRI for the triple | |
| * @param exoProperty - ExoRDF property name (e.g., "exo__Instance_class") | |
| * @param value - Property value as string or IRI | |
| * @returns Triple with mapped RDF/RDFS predicate, or null if unmapped | |
| * | |
| * @example | |
| * ```typescript | |
| * const triple = mapper.generateMappedTriple( | |
| * new IRI("https://example.org/asset"), | |
| * "exo__Instance_class", | |
| * "ems__Task" | |
| * ); | |
| * // Returns: <asset> rdf:type ems:Task | |
| * ``` | |
| */ |
| const triples: Triple[] = []; | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Asset"), | ||
| Namespace.RDFS.term("subClassOf"), | ||
| Namespace.RDFS.term("Resource"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Class"), | ||
| Namespace.RDFS.term("subClassOf"), | ||
| Namespace.RDFS.term("Class"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Property"), | ||
| Namespace.RDFS.term("subClassOf"), | ||
| Namespace.RDF.term("Property"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EMS.term("Task"), | ||
| Namespace.RDFS.term("subClassOf"), | ||
| Namespace.EXO.term("Asset"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EMS.term("Project"), | ||
| Namespace.RDFS.term("subClassOf"), | ||
| Namespace.EXO.term("Asset"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EMS.term("Area"), | ||
| Namespace.RDFS.term("subClassOf"), | ||
| Namespace.EXO.term("Asset"), | ||
| ), | ||
| ); | ||
|
|
||
| return triples; |
There was a problem hiding this comment.
[nitpick] The repetitive triples.push(new Triple(...)) pattern in generateClassHierarchyTriples() could be refactored to improve maintainability. Consider using a data-driven approach:
generateClassHierarchyTriples(): Triple[] {
const mappings = [
{ subject: "Asset", object: Namespace.RDFS.term("Resource") },
{ subject: "Class", object: Namespace.RDFS.term("Class") },
{ subject: "Property", object: Namespace.RDF.term("Property"), namespace: Namespace.EXO },
{ subject: "Task", object: Namespace.EXO.term("Asset"), namespace: Namespace.EMS },
{ subject: "Project", object: Namespace.EXO.term("Asset"), namespace: Namespace.EMS },
{ subject: "Area", object: Namespace.EXO.term("Asset"), namespace: Namespace.EMS },
];
return mappings.map(({ subject, object, namespace = Namespace.EXO }) =>
new Triple(
namespace.term(subject),
Namespace.RDFS.term("subClassOf"),
object
)
);
}This reduces code duplication and makes it easier to add or modify mappings.
| const triples: Triple[] = []; | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Asset"), | |
| Namespace.RDFS.term("subClassOf"), | |
| Namespace.RDFS.term("Resource"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Class"), | |
| Namespace.RDFS.term("subClassOf"), | |
| Namespace.RDFS.term("Class"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Property"), | |
| Namespace.RDFS.term("subClassOf"), | |
| Namespace.RDF.term("Property"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EMS.term("Task"), | |
| Namespace.RDFS.term("subClassOf"), | |
| Namespace.EXO.term("Asset"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EMS.term("Project"), | |
| Namespace.RDFS.term("subClassOf"), | |
| Namespace.EXO.term("Asset"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EMS.term("Area"), | |
| Namespace.RDFS.term("subClassOf"), | |
| Namespace.EXO.term("Asset"), | |
| ), | |
| ); | |
| return triples; | |
| const mappings = [ | |
| { subject: "Asset", object: Namespace.RDFS.term("Resource"), namespace: Namespace.EXO }, | |
| { subject: "Class", object: Namespace.RDFS.term("Class"), namespace: Namespace.EXO }, | |
| { subject: "Property", object: Namespace.RDF.term("Property"), namespace: Namespace.EXO }, | |
| { subject: "Task", object: Namespace.EXO.term("Asset"), namespace: Namespace.EMS }, | |
| { subject: "Project", object: Namespace.EXO.term("Asset"), namespace: Namespace.EMS }, | |
| { subject: "Area", object: Namespace.EXO.term("Asset"), namespace: Namespace.EMS }, | |
| ]; | |
| return mappings.map(({ subject, object, namespace }) => | |
| new Triple( | |
| namespace.term(subject), | |
| Namespace.RDFS.term("subClassOf"), | |
| object | |
| ) | |
| ); |
| const triples: Triple[] = []; | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Instance_class"), | ||
| Namespace.RDFS.term("subPropertyOf"), | ||
| Namespace.RDF.term("type"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Asset_isDefinedBy"), | ||
| Namespace.RDFS.term("subPropertyOf"), | ||
| Namespace.RDFS.term("isDefinedBy"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Class_superClass"), | ||
| Namespace.RDFS.term("subPropertyOf"), | ||
| Namespace.RDFS.term("subClassOf"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Property_range"), | ||
| Namespace.RDFS.term("subPropertyOf"), | ||
| Namespace.RDFS.term("range"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Property_domain"), | ||
| Namespace.RDFS.term("subPropertyOf"), | ||
| Namespace.RDFS.term("domain"), | ||
| ), | ||
| ); | ||
|
|
||
| triples.push( | ||
| new Triple( | ||
| Namespace.EXO.term("Property_superProperty"), | ||
| Namespace.RDFS.term("subPropertyOf"), | ||
| Namespace.RDFS.term("subPropertyOf"), | ||
| ), | ||
| ); | ||
|
|
||
| return triples; |
There was a problem hiding this comment.
[nitpick] The repetitive triples.push(new Triple(...)) pattern in generatePropertyHierarchyTriples() could be refactored to improve maintainability. Consider using a data-driven approach:
generatePropertyHierarchyTriples(): Triple[] {
const mappings = [
{ subject: "Instance_class", object: Namespace.RDF.term("type") },
{ subject: "Asset_isDefinedBy", object: Namespace.RDFS.term("isDefinedBy") },
{ subject: "Class_superClass", object: Namespace.RDFS.term("subClassOf") },
{ subject: "Property_range", object: Namespace.RDFS.term("range") },
{ subject: "Property_domain", object: Namespace.RDFS.term("domain") },
{ subject: "Property_superProperty", object: Namespace.RDFS.term("subPropertyOf") },
];
return mappings.map(({ subject, object }) =>
new Triple(
Namespace.EXO.term(subject),
Namespace.RDFS.term("subPropertyOf"),
object
)
);
}This reduces code duplication, aligns with the pattern used in the propertyMappings Map, and makes it easier to maintain consistency between the two.
| const triples: Triple[] = []; | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Instance_class"), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| Namespace.RDF.term("type"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Asset_isDefinedBy"), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| Namespace.RDFS.term("isDefinedBy"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Class_superClass"), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| Namespace.RDFS.term("subClassOf"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Property_range"), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| Namespace.RDFS.term("range"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Property_domain"), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| Namespace.RDFS.term("domain"), | |
| ), | |
| ); | |
| triples.push( | |
| new Triple( | |
| Namespace.EXO.term("Property_superProperty"), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| ), | |
| ); | |
| return triples; | |
| const mappings = [ | |
| { subject: "Instance_class", object: Namespace.RDF.term("type") }, | |
| { subject: "Asset_isDefinedBy", object: Namespace.RDFS.term("isDefinedBy") }, | |
| { subject: "Class_superClass", object: Namespace.RDFS.term("subClassOf") }, | |
| { subject: "Property_range", object: Namespace.RDFS.term("range") }, | |
| { subject: "Property_domain", object: Namespace.RDFS.term("domain") }, | |
| { subject: "Property_superProperty", object: Namespace.RDFS.term("subPropertyOf") }, | |
| ]; | |
| return mappings.map(({ subject, object }) => | |
| new Triple( | |
| Namespace.EXO.term(subject), | |
| Namespace.RDFS.term("subPropertyOf"), | |
| object | |
| ) | |
| ); |
| ["exo__Property_superProperty", Namespace.RDFS.term("subPropertyOf")], | ||
| ]); | ||
| } | ||
|
|
There was a problem hiding this comment.
The generateClassHierarchyTriples() method lacks JSDoc documentation. Public methods should be documented with descriptions, return types, and examples.
Consider adding:
/**
* Generates RDF triples that define the class hierarchy mappings between
* ExoRDF classes and their RDF/RDFS superclasses.
*
* @returns Array of triples expressing rdfs:subClassOf relationships
*
* @example
* ```typescript
* const triples = mapper.generateClassHierarchyTriples();
* // Returns triples like: ems:Task rdfs:subClassOf exo:Asset
* ```
*/| /** | |
| * Generates RDF triples that define the class hierarchy mappings between | |
| * ExoRDF classes and their RDF/RDFS superclasses. | |
| * | |
| * @returns {Triple[]} Array of triples expressing rdfs:subClassOf relationships | |
| * | |
| * @example | |
| * const mapper = new RDFVocabularyMapper(); | |
| * const triples = mapper.generateClassHierarchyTriples(); | |
| * // Returns triples like: ems:Task rdfs:subClassOf exo:Asset | |
| */ |
|
|
||
| return new Triple(subject, rdfPredicate, objectIRI); | ||
| } | ||
|
|
There was a problem hiding this comment.
The hasMappingFor() method lacks JSDoc documentation. Public methods should be documented with descriptions, parameters, return types, and examples.
Consider adding:
/**
* Checks if a mapping exists for the given ExoRDF property.
*
* @param property - ExoRDF property name (e.g., "exo__Instance_class")
* @returns true if mapping exists, false otherwise
*
* @example
* ```typescript
* if (mapper.hasMappingFor("exo__Instance_class")) {
* // Property can be mapped to RDF/RDFS
* }
* ```
*/| /** | |
| * Checks if a mapping exists for the given ExoRDF property. | |
| * | |
| * @param property - ExoRDF property name (e.g., "exo__Instance_class") | |
| * @returns true if mapping exists, false otherwise | |
| * | |
| * @example | |
| * ```typescript | |
| * if (mapper.hasMappingFor("exo__Instance_class")) { | |
| * // Property can be mapped to RDF/RDFS | |
| * } | |
| * ``` | |
| */ |
| @@ -0,0 +1,261 @@ | |||
| import { RDFVocabularyMapper } from "../../../../src/infrastructure/rdf/RDFVocabularyMapper"; | |||
| import { Namespace } from "../../../../src/domain/models/rdf/Namespace"; | |||
There was a problem hiding this comment.
Unused import Namespace.
| import { Namespace } from "../../../../src/domain/models/rdf/Namespace"; |
Summary
Implements
RDFVocabularyMapperfor semantic interoperability between ExoRDF and W3C RDF/RDFS standards.Changes
New Classes
generateClassHierarchyTriples(): exo:Asset → rdfs:Resource, ems:Task → exo:Asset, etc.generatePropertyHierarchyTriples(): exo:Instance_class → rdf:type, etc.generateMappedTriple(): Generate RDF/RDFS triple for individual propertyhasMappingFor(): Check if property has mappingTesting
Design Patterns
Test Coverage
All tests passing:
Related Issues
Closes #367
Next Steps