Skip to main content

oxirs_ttl/
namespace_mapper.rs

1//! Namespace/prefix management for Turtle and SPARQL serialization.
2//!
3//! Provides bidirectional mapping between prefixes (e.g. `rdf:`) and namespace IRIs
4//! (e.g. `http://www.w3.org/1999/02/22-rdf-syntax-ns#`), with helpers to
5//! abbreviate/expand CURIEs and generate `@prefix` / `PREFIX` declarations.
6
7use std::collections::HashMap;
8
9// ─────────────────────────────────────────────────
10// NamespaceError
11// ─────────────────────────────────────────────────
12
13/// Errors that can occur when manipulating namespace mappings.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum NamespaceError {
16    /// The given prefix is already registered.
17    DuplicatePrefix(String),
18    /// The given prefix string is malformed (e.g. contains `:`).
19    InvalidPrefix(String),
20    /// The given namespace IRI is empty or otherwise invalid.
21    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// ─────────────────────────────────────────────────
35// PrefixMapping (utility struct)
36// ─────────────────────────────────────────────────
37
38/// A single prefix → namespace mapping entry.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct PrefixMapping {
41    /// The short prefix name (without trailing `:`).
42    pub prefix: String,
43    /// The full namespace IRI.
44    pub namespace: String,
45}
46
47// ─────────────────────────────────────────────────
48// NamespaceMapper
49// ─────────────────────────────────────────────────
50
51/// Bidirectional registry of namespace prefixes and their IRIs.
52#[derive(Debug, Clone, Default)]
53pub struct NamespaceMapper {
54    /// prefix → namespace
55    mappings: HashMap<String, String>,
56    /// namespace → prefix (reverse lookup)
57    reverse: HashMap<String, String>,
58}
59
60impl NamespaceMapper {
61    /// Create an empty mapper.
62    pub fn new() -> Self {
63        NamespaceMapper {
64            mappings: HashMap::new(),
65            reverse: HashMap::new(),
66        }
67    }
68
69    /// Create a mapper pre-loaded with the most common RDF standard prefixes.
70    pub fn with_defaults() -> Self {
71        let mut m = Self::new();
72        // These are guaranteed to succeed; if they fail it is a programming error
73        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    /// Register a prefix mapping.
82    ///
83    /// Returns `Err(DuplicatePrefix)` if the prefix is already registered.
84    /// Returns `Err(InvalidPrefix)` if the prefix string is empty or contains `:`
85    ///   (only the bare name should be supplied; the trailing `:` is added automatically
86    ///    during serialization).
87    /// Returns `Err(InvalidNamespace)` if the namespace is empty.
88    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    /// Remove a prefix mapping.  Returns `true` if the mapping existed.
112    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    /// Look up the namespace for a given prefix.
122    pub fn get_namespace(&self, prefix: &str) -> Option<&str> {
123        self.mappings.get(prefix).map(String::as_str)
124    }
125
126    /// Reverse-lookup: find a prefix for a given namespace IRI.
127    pub fn get_prefix(&self, namespace: &str) -> Option<&str> {
128        self.reverse.get(namespace).map(String::as_str)
129    }
130
131    /// Number of registered mappings.
132    pub fn len(&self) -> usize {
133        self.mappings.len()
134    }
135
136    /// Return `true` when no mappings are registered.
137    pub fn is_empty(&self) -> bool {
138        self.mappings.is_empty()
139    }
140
141    /// Return all registered prefix names.
142    pub fn prefix_names(&self) -> Vec<&str> {
143        self.mappings.keys().map(String::as_str).collect()
144    }
145
146    /// Return all (prefix, namespace) pairs.
147    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    /// Try to abbreviate a full IRI to a CURIE (prefix:local).
155    ///
156    /// Iterates all registered namespaces and returns the first match, choosing
157    /// the longest matching namespace for determinism.
158    pub fn abbreviate(&self, iri: &str) -> Option<String> {
159        // Find the longest matching namespace
160        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    /// Expand a CURIE (prefix:local) to a full IRI.
173    ///
174    /// Returns `None` if the prefix is not registered.
175    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    /// Produce Turtle `@prefix` declarations for all registered mappings.
184    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    /// Produce SPARQL `PREFIX` declarations for all registered mappings.
198    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    /// Merge all mappings from `other` into this mapper.
212    ///
213    /// Silently skips any prefix that is already registered (no error).
214    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// ─────────────────────────────────────────────────
224// Tests
225// ─────────────────────────────────────────────────
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    // ── Default prefixes ──────────────────────────────────────
232
233    #[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    // ── Add / Remove ──────────────────────────────────────────
273
274    #[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    // ── Abbreviate IRI ────────────────────────────────────────
327
328    #[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    // ── Expand CURIE ──────────────────────────────────────────
358
359    #[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    // ── Turtle declarations ───────────────────────────────────
389
390    #[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    // ── SPARQL declarations ───────────────────────────────────
410
411    #[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    // ── Merge two mappers ─────────────────────────────────────
428
429    #[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        // Should not overwrite
452        m1.merge(&m2);
453        assert_eq!(m1.get_namespace("ex"), Some("http://example.org/"));
454    }
455
456    // ── Get prefix (reverse lookup) ───────────────────────────
457
458    #[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    // ── all_mappings & prefix_names ────────────────────────────
472
473    #[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    // ── Default impl ──────────────────────────────────────────
489
490    #[test]
491    fn test_default_is_empty() {
492        let m = NamespaceMapper::default();
493        assert!(m.is_empty());
494    }
495
496    // ── PrefixMapping struct ──────────────────────────────────
497
498    #[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    // ── NamespaceError Display ────────────────────────────────
509
510    #[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    // ── Round-trip abbreviate/expand ─────────────────────────
529
530    #[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    // ── Remove after add ──────────────────────────────────────
540
541    #[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        // Reverse lookup must also be gone
547        assert!(m.get_prefix("http://example.org/").is_none());
548    }
549
550    // ── Multiple additions ────────────────────────────────────
551
552    #[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}