Skip to main content

strixonomy_owl/
load.rs

1use crate::bridge::bridge_ontology;
2use crate::error::{OwlError, Result};
3use crate::OwlBridgeResult;
4use horned_owl::io::rdf::reader::read;
5use horned_owl::model::{RcAnnotatedComponent, RcStr};
6use horned_owl::ontology::component_mapped::ComponentMappedOntology;
7use horned_owl::ontology::set::SetOntology;
8use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
9use oxigraph::model::Quad;
10use std::collections::BTreeMap;
11use std::io::Cursor;
12use std::path::Path;
13use strixonomy_core::OntologyFormat;
14
15/// Result of loading a document through the Horned-OWL pipeline.
16#[derive(Debug, Clone)]
17pub struct OwlLoadResult {
18    pub bridge: OwlBridgeResult,
19    pub incomplete: bool,
20    pub load_warning: Option<String>,
21    /// RDF quads for the Oxigraph SPARQL store (empty for Turtle — caller already has quads).
22    pub quads: Vec<Quad>,
23}
24
25/// Load Turtle source via Oxigraph quads → RDF/XML → Horned-OWL.
26pub fn load_turtle_text(
27    path: &Path,
28    ontology_id: &str,
29    source_text: &str,
30    quads: &[Quad],
31    namespaces: &BTreeMap<String, String>,
32) -> Result<OwlLoadResult> {
33    let _ = path;
34    match load_from_quads(quads) {
35        Ok((ontology, incomplete)) => {
36            let bridge = bridge_ontology(ontology, ontology_id, source_text, namespaces);
37            Ok(OwlLoadResult {
38                bridge,
39                incomplete,
40                load_warning: if incomplete {
41                    Some("Horned-OWL reported incomplete RDF parse".to_string())
42                } else {
43                    None
44                },
45                quads: Vec::new(),
46            })
47        }
48        Err(e) => Err(e),
49    }
50}
51
52/// Convert Oxigraph quads to Horned-OWL ontology via RDF/XML.
53pub fn load_from_quads(
54    quads: &[Quad],
55) -> Result<(
56    horned_owl::io::rdf::reader::ConcreteRDFOntology<
57        horned_owl::model::RcStr,
58        horned_owl::model::RcAnnotatedComponent,
59    >,
60    bool,
61)> {
62    let rdf_xml = quads_to_rdf_xml(quads).map_err(OwlError::LoadFailed)?;
63    let mut cursor = Cursor::new(rdf_xml);
64    let (ontology, incomplete) =
65        read(&mut cursor, Default::default()).map_err(|e| OwlError::LoadFailed(e.to_string()))?;
66    Ok((ontology, !incomplete.is_complete()))
67}
68
69fn quads_to_rdf_xml(quads: &[Quad]) -> std::result::Result<Vec<u8>, String> {
70    let mut serializer = RdfSerializer::from_format(RdfFormat::RdfXml).for_writer(Vec::new());
71    for quad in quads {
72        serializer.serialize_quad(quad).map_err(|e| e.to_string())?;
73    }
74    serializer.finish().map_err(|e| e.to_string())
75}
76
77/// Project a Horned ontology to Oxigraph quads (N-Triples via Horned RDF writer).
78fn ontology_to_quads(
79    ont: &ComponentMappedOntology<RcStr, RcAnnotatedComponent>,
80) -> Result<Vec<Quad>> {
81    let mut buf = Vec::new();
82    horned_owl::io::rdf::writer::write_to_rdf_format(&mut buf, ont, "ttl")
83        .map_err(|e| OwlError::LoadFailed(e.to_string()))?;
84    let parser = RdfParser::from_format(RdfFormat::NTriples);
85    parser
86        .for_reader(buf.as_slice())
87        .collect::<std::result::Result<Vec<_>, _>>()
88        .map_err(|e| OwlError::LoadFailed(e.to_string()))
89}
90
91/// Load OWL/XML (`.owx`) source via Horned-OWL.
92///
93/// Horned's OWX reader does not expose RDF `IncompleteParse`. After a successful OWX load we
94/// project the ontology to RDF for the SPARQL store and propagate incompleteness from that
95/// RDF round-trip (analogous to Turtle `load_from_quads`).
96pub fn load_owx_text(
97    path: &Path,
98    ontology_id: &str,
99    source_text: &str,
100    namespaces: &BTreeMap<String, String>,
101) -> Result<OwlLoadResult> {
102    let _ = path;
103    use horned_owl::io::owx::reader::read as read_owx;
104    let mut cursor = Cursor::new(source_text.as_bytes());
105    let (set_ont, mapping): (SetOntology<RcStr>, _) = read_owx(&mut cursor, Default::default())
106        .map_err(|e| OwlError::LoadFailed(e.to_string()))?;
107
108    let mut merged_ns = namespaces.clone();
109    for (prefix, iri) in mapping.mappings() {
110        merged_ns.entry(prefix.clone()).or_insert_with(|| iri.clone());
111    }
112
113    let mapped: ComponentMappedOntology<RcStr, RcAnnotatedComponent> = set_ont.into();
114    let quads = ontology_to_quads(&mapped)?;
115    let incomplete = match load_from_quads(&quads) {
116        Ok((_, incomplete)) => incomplete,
117        Err(_) => {
118            // Projection produced quads Horned cannot re-parse as RDF/XML — treat as incomplete.
119            true
120        }
121    };
122    let bridge = bridge_ontology(mapped, ontology_id, source_text, &merged_ns);
123    Ok(OwlLoadResult {
124        bridge,
125        incomplete,
126        load_warning: if incomplete {
127            Some("Horned-OWL reported incomplete RDF projection of OWL/XML".to_string())
128        } else {
129            None
130        },
131        quads,
132    })
133}
134
135/// Whether Horned-OWL loading is supported for this format.
136pub fn supports_horned_load(format: OntologyFormat) -> bool {
137    matches!(
138        format,
139        OntologyFormat::Turtle
140            | OntologyFormat::Owl
141            | OntologyFormat::RdfXml
142            | OntologyFormat::OwlXml
143    )
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use oxigraph::io::RdfParser;
150
151    #[test]
152    fn loads_fixture_turtle_via_horned() {
153        let ttl = include_str!("../../../fixtures/example.ttl");
154        let parser = RdfParser::from_format(RdfFormat::Turtle);
155        let quads: Vec<Quad> =
156            parser.for_reader(ttl.as_bytes()).collect::<std::result::Result<Vec<_>, _>>().unwrap();
157        let namespaces = BTreeMap::from([
158            ("ex".to_string(), "http://example.org/people#".to_string()),
159            ("owl".to_string(), "http://www.w3.org/2002/07/owl#".to_string()),
160        ]);
161        let result = load_turtle_text(Path::new("example.ttl"), "doc-1", ttl, &quads, &namespaces)
162            .expect("load");
163        assert!(!result.bridge.entities.is_empty());
164        assert!(result.bridge.entities.iter().any(|e| e.short_name == "Person"));
165        assert!(result.quads.is_empty());
166    }
167
168    #[test]
169    fn loads_owx_with_rdf_quads_for_sparql() {
170        let owx = include_str!("../../../examples/protege-roundtrip/example.owx");
171        let result = load_owx_text(Path::new("example.owx"), "doc-owx", owx, &BTreeMap::new())
172            .expect("load owx");
173        assert!(result.bridge.entities.iter().any(|e| e.short_name == "Department"));
174        assert!(!result.quads.is_empty(), "OWL/XML load must project RDF quads for SPARQL");
175        assert!(
176            result.quads.iter().any(|q| {
177                q.subject.to_string().contains("Department")
178                    || q.object.to_string().contains("Department")
179            }),
180            "expected Department IRI in projected quads"
181        );
182        // Well-formed OWX projects cleanly; incompleteness is computed (not hardcoded false).
183        assert!(!result.incomplete);
184        assert!(result.load_warning.is_none());
185    }
186
187    #[test]
188    fn owx_prefixes_merged_into_bridge_namespaces() {
189        let owx = include_str!("../../../examples/protege-roundtrip/example.owx");
190        let result = load_owx_text(Path::new("example.owx"), "doc-owx", owx, &BTreeMap::new())
191            .expect("load owx");
192        assert!(
193            result
194                .bridge
195                .namespace_rows
196                .iter()
197                .any(|n| n.prefix == "ex" && n.iri.contains("example.org/org")),
198            "expected ex: prefix from OWX PrefixMapping, got {:?}",
199            result.bridge.namespace_rows
200        );
201    }
202}