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#[derive(Debug, Clone)]
17pub struct OwlLoadResult {
18 pub bridge: OwlBridgeResult,
19 pub incomplete: bool,
20 pub load_warning: Option<String>,
21 pub quads: Vec<Quad>,
23}
24
25pub 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
52pub 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
77fn 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
91pub 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 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
135pub 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 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}