strixonomy_core/
document_lookup.rs1use crate::{Entity, OntologyDocument};
2
3pub fn normalize_iri(iri: &str) -> String {
5 iri.trim_end_matches('#').trim_end_matches('/').to_string()
6}
7
8pub fn file_uri_for_path(path: &std::path::Path) -> String {
13 let abs = path.canonicalize().unwrap_or_else(|_| {
14 if path.is_absolute() {
15 path.to_path_buf()
16 } else {
17 std::env::current_dir().unwrap_or_default().join(path)
18 }
19 });
20 url::Url::from_file_path(&abs).map(|u| u.to_string()).unwrap_or_else(|_| {
21 let display = abs.to_string_lossy().replace('\\', "/");
22 if display.starts_with('/') {
23 format!("file://{display}")
24 } else {
25 format!("file:///{display}")
26 }
27 })
28}
29
30pub fn document_matches_entity(entity: &Entity, doc: &OntologyDocument) -> bool {
35 if entity.ontology_id == doc.id {
36 return true;
37 }
38 if let Some(base) = &doc.base_iri {
39 if normalize_iri(base) == normalize_iri(&entity.ontology_id) {
40 return true;
41 }
42 }
43 false
44}
45
46fn base_iri_prefix_len(entity: &Entity, doc: &OntologyDocument) -> Option<usize> {
47 let base = doc.base_iri.as_ref()?;
48 if entity.iri.starts_with(base.as_str()) {
49 Some(base.len())
50 } else {
51 None
52 }
53}
54
55pub fn document_matches_ontology_id(ontology_id: &str, doc: &OntologyDocument) -> bool {
57 if ontology_id == doc.id {
58 return true;
59 }
60 if let Some(base) = &doc.base_iri {
61 if normalize_iri(base) == normalize_iri(ontology_id) {
62 return true;
63 }
64 }
65 false
66}
67
68pub fn document_for_entity<'a>(
75 documents: &'a [OntologyDocument],
76 entity: &Entity,
77) -> Option<&'a OntologyDocument> {
78 if let Some(doc) = documents.iter().find(|d| entity.ontology_id == d.id) {
79 return Some(doc);
80 }
81 if let Some(doc) = documents.iter().find(|d| {
82 d.base_iri
83 .as_ref()
84 .is_some_and(|base| normalize_iri(base) == normalize_iri(&entity.ontology_id))
85 }) {
86 return Some(doc);
87 }
88 documents
89 .iter()
90 .filter_map(|d| base_iri_prefix_len(entity, d).map(|len| (d, len)))
91 .max_by_key(|(_, len)| *len)
92 .map(|(d, _)| d)
93}
94
95pub fn document_for_ontology_id<'a>(
97 documents: &'a [OntologyDocument],
98 ontology_id: &str,
99) -> Option<&'a OntologyDocument> {
100 documents.iter().find(|d| document_matches_ontology_id(ontology_id, d))
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::{EntityKind, OntologyFormat, ParseStatus};
107 use std::collections::BTreeMap;
108 use std::path::Path;
109
110 fn doc(id: &str, base_iri: Option<&str>) -> OntologyDocument {
111 OntologyDocument {
112 id: id.to_string(),
113 path: Path::new(&format!("{id}.ttl")).to_path_buf(),
114 format: OntologyFormat::Turtle,
115 base_iri: base_iri.map(str::to_string),
116 version_iri: None,
117 imports: vec![],
118 namespaces: BTreeMap::new(),
119 parse_status: ParseStatus::Ok,
120 content_hash: "h".to_string(),
121 modified_time: 0,
122 parse_message: None,
123 parse_error_location: None,
124 }
125 }
126
127 fn entity(iri: &str, ontology_id: &str) -> Entity {
128 Entity {
129 iri: iri.to_string(),
130 short_name: iri.rsplit(['#', '/']).next().unwrap_or(iri).to_string(),
131 kind: EntityKind::Class,
132 ontology_id: ontology_id.to_string(),
133 source_location: Default::default(),
134 labels: vec![],
135 comments: vec![],
136 deprecated: false,
137 obo_id: None,
138 characteristics: Default::default(),
139 }
140 }
141
142 #[test]
143 fn file_uri_for_path_uses_url_encoding() {
144 let dir = tempfile::tempdir().unwrap();
145 let file = dir.path().join("sample.ttl");
146 std::fs::write(&file, "").unwrap();
147 let uri = file_uri_for_path(&file);
148 assert!(uri.starts_with("file://"), "got {uri}");
149 assert!(!uri.contains('\\'), "Windows separators must not appear raw: {uri}");
150 #[cfg(windows)]
152 {
153 assert!(
154 uri.starts_with("file:///"),
155 "Windows file URI should use three slashes: {uri}"
156 );
157 assert!(uri.contains(":/") || uri.contains("%3A"), "got {uri}");
158 }
159 #[cfg(unix)]
160 {
161 assert!(uri.starts_with("file:///"), "got {uri}");
162 }
163 }
164
165 #[test]
166 fn matches_entity_by_ontology_iri() {
167 let document = doc("doc-1", Some("http://example.org/people"));
168 let ent = entity("http://example.org/people#Person", "http://example.org/people");
169 assert!(document_matches_entity(&ent, &document));
170 }
171
172 #[test]
173 fn broader_base_iri_does_not_match_when_ontology_id_differs() {
174 let broad = doc("doc-A", Some("http://example.org/"));
175 let ent = entity("http://example.org/people#Person", "doc-B");
176 assert!(!document_matches_entity(&ent, &broad));
177 }
178
179 #[test]
180 fn document_for_entity_prefers_exact_ontology_id_over_broader_base() {
181 let broad = doc("doc-A", Some("http://example.org/"));
182 let specific = doc("doc-B", Some("http://example.org/people#"));
183 let ent = entity("http://example.org/people#Person", "doc-B");
184 let documents = vec![broad, specific];
186 let found = document_for_entity(&documents, &ent).expect("owner");
187 assert_eq!(found.id, "doc-B");
188 }
189
190 #[test]
191 fn document_for_entity_longest_base_iri_wins_without_ontology_id_match() {
192 let broad = doc("doc-A", Some("http://example.org/"));
193 let specific = doc("doc-B", Some("http://example.org/people#"));
194 let ent = entity("http://example.org/people#Person", "unknown");
196 let documents = vec![broad, specific];
197 let found = document_for_entity(&documents, &ent).expect("owner");
198 assert_eq!(found.id, "doc-B");
199 }
200
201 #[test]
202 fn document_for_entity_longest_base_iri_wins_when_broad_is_second() {
203 let specific = doc("doc-B", Some("http://example.org/people#"));
204 let broad = doc("doc-A", Some("http://example.org/"));
205 let ent = entity("http://example.org/people#Person", "unknown");
206 let documents = vec![specific, broad];
207 let found = document_for_entity(&documents, &ent).expect("owner");
208 assert_eq!(found.id, "doc-B");
209 }
210}