1use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum NamespaceError {
16 DuplicatePrefix(String),
18 InvalidPrefix(String),
20 InvalidNamespace(String),
22}
23
24impl std::fmt::Display for NamespaceError {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 match self {
27 NamespaceError::DuplicatePrefix(p) => write!(f, "Duplicate prefix: {p}"),
28 NamespaceError::InvalidPrefix(p) => write!(f, "Invalid prefix: {p}"),
29 NamespaceError::InvalidNamespace(n) => write!(f, "Invalid namespace: {n}"),
30 }
31 }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PrefixMapping {
41 pub prefix: String,
43 pub namespace: String,
45}
46
47#[derive(Debug, Clone, Default)]
53pub struct NamespaceMapper {
54 mappings: HashMap<String, String>,
56 reverse: HashMap<String, String>,
58}
59
60impl NamespaceMapper {
61 pub fn new() -> Self {
63 NamespaceMapper {
64 mappings: HashMap::new(),
65 reverse: HashMap::new(),
66 }
67 }
68
69 pub fn with_defaults() -> Self {
71 let mut m = Self::new();
72 let _ = m.add("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
74 let _ = m.add("rdfs", "http://www.w3.org/2000/01/rdf-schema#");
75 let _ = m.add("owl", "http://www.w3.org/2002/07/owl#");
76 let _ = m.add("xsd", "http://www.w3.org/2001/XMLSchema#");
77 let _ = m.add("dc", "http://purl.org/dc/elements/1.1/");
78 m
79 }
80
81 pub fn add(
89 &mut self,
90 prefix: impl Into<String>,
91 namespace: impl Into<String>,
92 ) -> Result<(), NamespaceError> {
93 let prefix = prefix.into();
94 let namespace = namespace.into();
95
96 if prefix.contains(':') {
97 return Err(NamespaceError::InvalidPrefix(prefix));
98 }
99 if namespace.is_empty() {
100 return Err(NamespaceError::InvalidNamespace(namespace));
101 }
102 if self.mappings.contains_key(&prefix) {
103 return Err(NamespaceError::DuplicatePrefix(prefix));
104 }
105
106 self.reverse.insert(namespace.clone(), prefix.clone());
107 self.mappings.insert(prefix, namespace);
108 Ok(())
109 }
110
111 pub fn remove(&mut self, prefix: &str) -> bool {
113 if let Some(ns) = self.mappings.remove(prefix) {
114 self.reverse.remove(&ns);
115 true
116 } else {
117 false
118 }
119 }
120
121 pub fn get_namespace(&self, prefix: &str) -> Option<&str> {
123 self.mappings.get(prefix).map(String::as_str)
124 }
125
126 pub fn get_prefix(&self, namespace: &str) -> Option<&str> {
128 self.reverse.get(namespace).map(String::as_str)
129 }
130
131 pub fn len(&self) -> usize {
133 self.mappings.len()
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.mappings.is_empty()
139 }
140
141 pub fn prefix_names(&self) -> Vec<&str> {
143 self.mappings.keys().map(String::as_str).collect()
144 }
145
146 pub fn all_mappings(&self) -> Vec<(&str, &str)> {
148 self.mappings
149 .iter()
150 .map(|(k, v)| (k.as_str(), v.as_str()))
151 .collect()
152 }
153
154 pub fn abbreviate(&self, iri: &str) -> Option<String> {
159 let best = self
161 .mappings
162 .iter()
163 .filter(|(_, ns)| iri.starts_with(ns.as_str()))
164 .max_by_key(|(_, ns)| ns.len());
165
166 best.map(|(prefix, ns)| {
167 let local = &iri[ns.len()..];
168 format!("{prefix}:{local}")
169 })
170 }
171
172 pub fn expand(&self, curie: &str) -> Option<String> {
176 let colon = curie.find(':')?;
177 let prefix = &curie[..colon];
178 let local = &curie[colon + 1..];
179 let ns = self.mappings.get(prefix)?;
180 Some(format!("{ns}{local}"))
181 }
182
183 pub fn to_turtle_declarations(&self) -> String {
185 let mut entries: Vec<(&str, &str)> = self
186 .mappings
187 .iter()
188 .map(|(k, v)| (k.as_str(), v.as_str()))
189 .collect();
190 entries.sort_by_key(|(p, _)| *p);
191 entries
192 .iter()
193 .map(|(p, ns)| format!("@prefix {p}: <{ns}> .\n"))
194 .collect()
195 }
196
197 pub fn to_sparql_declarations(&self) -> String {
199 let mut entries: Vec<(&str, &str)> = self
200 .mappings
201 .iter()
202 .map(|(k, v)| (k.as_str(), v.as_str()))
203 .collect();
204 entries.sort_by_key(|(p, _)| *p);
205 entries
206 .iter()
207 .map(|(p, ns)| format!("PREFIX {p}: <{ns}>\n"))
208 .collect()
209 }
210
211 pub fn merge(&mut self, other: &NamespaceMapper) {
215 for (prefix, namespace) in &other.mappings {
216 if !self.mappings.contains_key(prefix.as_str()) {
217 let _ = self.add(prefix.clone(), namespace.clone());
218 }
219 }
220 }
221}
222
223#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
234 fn test_defaults_contains_rdf() {
235 let m = NamespaceMapper::with_defaults();
236 assert_eq!(
237 m.get_namespace("rdf"),
238 Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
239 );
240 }
241
242 #[test]
243 fn test_defaults_contains_rdfs() {
244 let m = NamespaceMapper::with_defaults();
245 assert!(m.get_namespace("rdfs").is_some());
246 }
247
248 #[test]
249 fn test_defaults_contains_owl() {
250 let m = NamespaceMapper::with_defaults();
251 assert!(m.get_namespace("owl").is_some());
252 }
253
254 #[test]
255 fn test_defaults_contains_xsd() {
256 let m = NamespaceMapper::with_defaults();
257 assert!(m.get_namespace("xsd").is_some());
258 }
259
260 #[test]
261 fn test_defaults_contains_dc() {
262 let m = NamespaceMapper::with_defaults();
263 assert!(m.get_namespace("dc").is_some());
264 }
265
266 #[test]
267 fn test_defaults_len_at_least_five() {
268 let m = NamespaceMapper::with_defaults();
269 assert!(m.len() >= 5);
270 }
271
272 #[test]
275 fn test_add_lookup() {
276 let mut m = NamespaceMapper::new();
277 m.add("ex", "http://example.org/").expect("should succeed");
278 assert_eq!(m.get_namespace("ex"), Some("http://example.org/"));
279 }
280
281 #[test]
282 fn test_add_duplicate_error() {
283 let mut m = NamespaceMapper::new();
284 m.add("ex", "http://example.org/").expect("should succeed");
285 let err = m.add("ex", "http://other.org/").unwrap_err();
286 assert_eq!(err, NamespaceError::DuplicatePrefix("ex".into()));
287 }
288
289 #[test]
290 fn test_add_invalid_prefix_with_colon() {
291 let mut m = NamespaceMapper::new();
292 let err = m.add("ex:", "http://example.org/").unwrap_err();
293 assert_eq!(err, NamespaceError::InvalidPrefix("ex:".into()));
294 }
295
296 #[test]
297 fn test_add_empty_namespace_error() {
298 let mut m = NamespaceMapper::new();
299 let err = m.add("ex", "").unwrap_err();
300 assert_eq!(err, NamespaceError::InvalidNamespace("".into()));
301 }
302
303 #[test]
304 fn test_remove_existing() {
305 let mut m = NamespaceMapper::new();
306 m.add("ex", "http://example.org/").expect("should succeed");
307 assert!(m.remove("ex"));
308 assert!(m.get_namespace("ex").is_none());
309 }
310
311 #[test]
312 fn test_remove_nonexistent() {
313 let mut m = NamespaceMapper::new();
314 assert!(!m.remove("nonexistent"));
315 }
316
317 #[test]
318 fn test_len_and_is_empty() {
319 let mut m = NamespaceMapper::new();
320 assert!(m.is_empty());
321 m.add("a", "http://a.org/").expect("should succeed");
322 assert_eq!(m.len(), 1);
323 assert!(!m.is_empty());
324 }
325
326 #[test]
329 fn test_abbreviate_rdf_type() {
330 let m = NamespaceMapper::with_defaults();
331 let curie = m.abbreviate("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
332 assert_eq!(curie, Some("rdf:type".into()));
333 }
334
335 #[test]
336 fn test_abbreviate_rdfs_label() {
337 let m = NamespaceMapper::with_defaults();
338 let curie = m.abbreviate("http://www.w3.org/2000/01/rdf-schema#label");
339 assert_eq!(curie, Some("rdfs:label".into()));
340 }
341
342 #[test]
343 fn test_abbreviate_unknown_iri_returns_none() {
344 let m = NamespaceMapper::with_defaults();
345 assert!(m.abbreviate("http://unknown.example.org/term").is_none());
346 }
347
348 #[test]
349 fn test_abbreviate_empty_local() {
350 let mut m = NamespaceMapper::new();
351 m.add("ns", "http://ns.example.org/")
352 .expect("should succeed");
353 let curie = m.abbreviate("http://ns.example.org/");
354 assert_eq!(curie, Some("ns:".into()));
355 }
356
357 #[test]
360 fn test_expand_rdf_type() {
361 let m = NamespaceMapper::with_defaults();
362 let iri = m.expand("rdf:type");
363 assert_eq!(
364 iri,
365 Some("http://www.w3.org/1999/02/22-rdf-syntax-ns#type".into())
366 );
367 }
368
369 #[test]
370 fn test_expand_unknown_prefix_returns_none() {
371 let m = NamespaceMapper::with_defaults();
372 assert!(m.expand("unknown:term").is_none());
373 }
374
375 #[test]
376 fn test_expand_no_colon_returns_none() {
377 let m = NamespaceMapper::with_defaults();
378 assert!(m.expand("nodot").is_none());
379 }
380
381 #[test]
382 fn test_expand_xsd_string() {
383 let m = NamespaceMapper::with_defaults();
384 let iri = m.expand("xsd:string");
385 assert_eq!(iri, Some("http://www.w3.org/2001/XMLSchema#string".into()));
386 }
387
388 #[test]
391 fn test_turtle_declarations_format() {
392 let mut m = NamespaceMapper::new();
393 m.add("ex", "http://example.org/").expect("should succeed");
394 let decls = m.to_turtle_declarations();
395 assert!(decls.contains("@prefix ex: <http://example.org/> ."));
396 }
397
398 #[test]
399 fn test_turtle_declarations_all_defaults() {
400 let m = NamespaceMapper::with_defaults();
401 let decls = m.to_turtle_declarations();
402 assert!(decls.contains("@prefix rdf:"));
403 assert!(decls.contains("@prefix rdfs:"));
404 assert!(decls.contains("@prefix owl:"));
405 assert!(decls.contains("@prefix xsd:"));
406 assert!(decls.contains("@prefix dc:"));
407 }
408
409 #[test]
412 fn test_sparql_declarations_format() {
413 let mut m = NamespaceMapper::new();
414 m.add("ex", "http://example.org/").expect("should succeed");
415 let decls = m.to_sparql_declarations();
416 assert!(decls.contains("PREFIX ex: <http://example.org/>"));
417 }
418
419 #[test]
420 fn test_sparql_declarations_all_defaults() {
421 let m = NamespaceMapper::with_defaults();
422 let decls = m.to_sparql_declarations();
423 assert!(decls.contains("PREFIX rdf:"));
424 assert!(decls.contains("PREFIX rdfs:"));
425 }
426
427 #[test]
430 fn test_merge_adds_missing_prefixes() {
431 let mut m1 = NamespaceMapper::new();
432 m1.add("ex", "http://example.org/").expect("should succeed");
433
434 let mut m2 = NamespaceMapper::new();
435 m2.add("schema", "https://schema.org/")
436 .expect("should succeed");
437
438 m1.merge(&m2);
439 assert!(m1.get_namespace("ex").is_some());
440 assert!(m1.get_namespace("schema").is_some());
441 }
442
443 #[test]
444 fn test_merge_skips_duplicates() {
445 let mut m1 = NamespaceMapper::new();
446 m1.add("ex", "http://example.org/").expect("should succeed");
447
448 let mut m2 = NamespaceMapper::new();
449 m2.add("ex", "http://other.org/").expect("should succeed");
450
451 m1.merge(&m2);
453 assert_eq!(m1.get_namespace("ex"), Some("http://example.org/"));
454 }
455
456 #[test]
459 fn test_get_prefix_reverse() {
460 let m = NamespaceMapper::with_defaults();
461 let prefix = m.get_prefix("http://www.w3.org/1999/02/22-rdf-syntax-ns#");
462 assert_eq!(prefix, Some("rdf"));
463 }
464
465 #[test]
466 fn test_get_prefix_unknown_returns_none() {
467 let m = NamespaceMapper::with_defaults();
468 assert!(m.get_prefix("http://totally-unknown.example/").is_none());
469 }
470
471 #[test]
474 fn test_all_mappings_count() {
475 let m = NamespaceMapper::with_defaults();
476 let all = m.all_mappings();
477 assert!(all.len() >= 5);
478 }
479
480 #[test]
481 fn test_prefix_names_contains_defaults() {
482 let m = NamespaceMapper::with_defaults();
483 let names = m.prefix_names();
484 assert!(names.contains(&"rdf"));
485 assert!(names.contains(&"owl"));
486 }
487
488 #[test]
491 fn test_default_is_empty() {
492 let m = NamespaceMapper::default();
493 assert!(m.is_empty());
494 }
495
496 #[test]
499 fn test_prefix_mapping_struct() {
500 let pm = PrefixMapping {
501 prefix: "ex".into(),
502 namespace: "http://example.org/".into(),
503 };
504 assert_eq!(pm.prefix, "ex");
505 assert_eq!(pm.namespace, "http://example.org/");
506 }
507
508 #[test]
511 fn test_error_display_duplicate() {
512 let e = NamespaceError::DuplicatePrefix("rdf".into());
513 assert!(e.to_string().contains("rdf"));
514 }
515
516 #[test]
517 fn test_error_display_invalid_prefix() {
518 let e = NamespaceError::InvalidPrefix("bad:".into());
519 assert!(e.to_string().contains("bad:"));
520 }
521
522 #[test]
523 fn test_error_display_invalid_namespace() {
524 let e = NamespaceError::InvalidNamespace("".into());
525 assert!(e.to_string().contains("Invalid namespace"));
526 }
527
528 #[test]
531 fn test_round_trip_abbreviate_expand() {
532 let m = NamespaceMapper::with_defaults();
533 let iri = "http://www.w3.org/2002/07/owl#Class";
534 let curie = m.abbreviate(iri).expect("should succeed");
535 let expanded = m.expand(&curie).expect("should succeed");
536 assert_eq!(expanded, iri);
537 }
538
539 #[test]
542 fn test_remove_clears_reverse_index() {
543 let mut m = NamespaceMapper::new();
544 m.add("ex", "http://example.org/").expect("should succeed");
545 m.remove("ex");
546 assert!(m.get_prefix("http://example.org/").is_none());
548 }
549
550 #[test]
553 fn test_add_many_prefixes() {
554 let mut m = NamespaceMapper::new();
555 for i in 0..10_u32 {
556 m.add(format!("ns{i}"), format!("http://ns{i}.example.org/"))
557 .expect("should succeed");
558 }
559 assert_eq!(m.len(), 10);
560 }
561}