Skip to main content

strixonomy_owl/
serialize.rs

1//! Serialize Horned ontologies to RDF/XML and OWL/XML (v0.21).
2
3use crate::error::{OwlError, Result};
4use curie::PrefixMapping;
5use horned_owl::model::{RcAnnotatedComponent, RcStr};
6use horned_owl::ontology::component_mapped::ComponentMappedOntology;
7use std::collections::BTreeMap;
8use std::io::Cursor;
9
10/// Serialize a component-mapped ontology as RDF/XML (`application/rdf+xml`).
11pub fn serialize_rdf_xml(
12    ont: &ComponentMappedOntology<RcStr, RcAnnotatedComponent>,
13) -> Result<String> {
14    let mut buf = Vec::new();
15    horned_owl::io::rdf::writer::write(&mut buf, ont)
16        .map_err(|e| OwlError::SerializeFailed(e.to_string()))?;
17    String::from_utf8(buf).map_err(|e| OwlError::SerializeFailed(e.to_string()))
18}
19
20/// Serialize a component-mapped ontology as OWL/XML.
21///
22/// `namespaces` become OWL/XML `Prefix` declarations when non-empty.
23pub fn serialize_owl_xml(
24    ont: &ComponentMappedOntology<RcStr, RcAnnotatedComponent>,
25    namespaces: &BTreeMap<String, String>,
26) -> Result<String> {
27    let mut mapping = PrefixMapping::default();
28    for (prefix, iri) in namespaces {
29        if prefix.is_empty() {
30            continue;
31        }
32        let _ = mapping.add_prefix(prefix, iri);
33    }
34    for (prefix, iri) in [
35        ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
36        ("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
37        ("owl", "http://www.w3.org/2002/07/owl#"),
38        ("xsd", "http://www.w3.org/2001/XMLSchema#"),
39    ] {
40        let _ = mapping.add_prefix(prefix, iri);
41    }
42
43    let mut buf = Vec::new();
44    horned_owl::io::owx::writer::write(&mut buf, ont, Some(&mapping))
45        .map_err(|e| OwlError::SerializeFailed(e.to_string()))?;
46    String::from_utf8(buf).map_err(|e| OwlError::SerializeFailed(e.to_string()))
47}
48
49/// Load RDF/XML source into a mutable component-mapped ontology.
50pub fn load_rdf_xml_ontology(
51    source: &str,
52) -> Result<(ComponentMappedOntology<RcStr, RcAnnotatedComponent>, bool)> {
53    let mut cursor = Cursor::new(source.as_bytes());
54    // Horned-OWL 1.4 can panic on some malformed RDF/XML; convert to structured errors.
55    let parse = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
56        horned_owl::io::rdf::reader::read(&mut cursor, Default::default())
57    }));
58    let (concrete, incomplete) = match parse {
59        Ok(Ok(pair)) => pair,
60        Ok(Err(e)) => return Err(OwlError::LoadFailed(e.to_string())),
61        Err(payload) => {
62            let msg = if let Some(s) = payload.downcast_ref::<String>() {
63                s.clone()
64            } else if let Some(s) = payload.downcast_ref::<&str>() {
65                (*s).to_string()
66            } else {
67                "RDF/XML parser panicked on malformed input".into()
68            };
69            return Err(OwlError::LoadFailed(msg));
70        }
71    };
72    let mapped: ComponentMappedOntology<RcStr, RcAnnotatedComponent> = concrete.into();
73    Ok((mapped, !incomplete.is_complete()))
74}
75
76/// Load OWL/XML source into a mutable component-mapped ontology plus prefix map.
77///
78/// The third return value is `true` when an RDF projection round-trip reports incompleteness
79/// (same policy as catalog `load_owx_text` / #383).
80#[allow(clippy::type_complexity)]
81pub fn load_owl_xml_ontology(
82    source: &str,
83) -> Result<(ComponentMappedOntology<RcStr, RcAnnotatedComponent>, BTreeMap<String, String>, bool)>
84{
85    use horned_owl::io::owx::reader::read as read_owx;
86    use horned_owl::ontology::set::SetOntology;
87
88    let mut cursor = Cursor::new(source.as_bytes());
89    let (set_ont, mapping): (SetOntology<RcStr>, _) = read_owx(&mut cursor, Default::default())
90        .map_err(|e| OwlError::LoadFailed(e.to_string()))?;
91    let mut namespaces = BTreeMap::new();
92    for (prefix, iri) in mapping.mappings() {
93        namespaces.insert(prefix.clone(), iri.clone());
94    }
95    let mapped: ComponentMappedOntology<RcStr, RcAnnotatedComponent> = set_ont.into();
96    let incomplete = match serialize_rdf_xml(&mapped) {
97        Ok(rdf) => load_rdf_xml_ontology(&rdf).map(|(_, inc)| inc).unwrap_or(true),
98        Err(_) => true,
99    };
100    Ok((mapped, namespaces, incomplete))
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn rdf_xml_roundtrip_preserves_department_label() {
109        let src = include_str!("../../../examples/protege-roundtrip/organization.owl");
110        let (ont, incomplete) = load_rdf_xml_ontology(src).expect("load");
111        assert!(!incomplete);
112        let out = serialize_rdf_xml(&ont).expect("serialize");
113        let (again, _) = load_rdf_xml_ontology(&out).expect("reload");
114        let bridge = crate::bridge_ontology(again, "doc", &out, &BTreeMap::new());
115        assert!(bridge.entities.iter().any(|e| e.short_name == "Department"));
116        assert!(bridge.entities.iter().any(|e| e.labels.iter().any(|l| l == "Department")));
117    }
118
119    #[test]
120    fn owl_xml_roundtrip_preserves_department() {
121        let src = include_str!("../../../examples/protege-roundtrip/example.owx");
122        let (ont, ns, incomplete) = load_owl_xml_ontology(src).expect("load");
123        assert!(!incomplete);
124        let out = serialize_owl_xml(&ont, &ns).expect("serialize");
125        let (again, _, _) = load_owl_xml_ontology(&out).expect("reload");
126        let bridge = crate::bridge_ontology(again, "doc", &out, &BTreeMap::new());
127        assert!(bridge.entities.iter().any(|e| e.short_name == "Department"));
128    }
129}