1use std::collections::HashMap;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct PrefixDeclaration {
10 pub prefix: String,
12 pub namespace: String,
14 pub line: usize,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum ResolveSource {
21 Prefix(String),
23 Base,
25 Absolute,
27}
28
29#[derive(Debug, Clone)]
31pub struct ResolveResult {
32 pub iri: String,
34 pub source: ResolveSource,
36}
37
38#[derive(Debug, Clone)]
40pub enum ResolveError {
41 UnknownPrefix(String),
43 NoBaseIri,
45 InvalidCurie(String),
47 InvalidIri(String),
49}
50
51impl std::fmt::Display for ResolveError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::UnknownPrefix(p) => write!(f, "Unknown prefix: {p}"),
55 Self::NoBaseIri => write!(f, "No base IRI set"),
56 Self::InvalidCurie(c) => write!(f, "Invalid CURIE: {c}"),
57 Self::InvalidIri(i) => write!(f, "Invalid IRI: {i}"),
58 }
59 }
60}
61
62impl std::error::Error for ResolveError {}
63
64#[derive(Debug, Default)]
66pub struct PrefixResolver {
67 prefixes: HashMap<String, String>,
68 base_iri: Option<String>,
69 declarations: Vec<PrefixDeclaration>,
70}
71
72impl PrefixResolver {
73 pub fn new() -> Self {
75 Self {
76 prefixes: HashMap::new(),
77 base_iri: None,
78 declarations: Vec::new(),
79 }
80 }
81
82 pub fn with_base(base_iri: impl Into<String>) -> Self {
84 let base = base_iri.into();
85 Self {
86 prefixes: HashMap::new(),
87 base_iri: Some(base),
88 declarations: Vec::new(),
89 }
90 }
91
92 pub fn add_prefix(
96 &mut self,
97 prefix: impl Into<String>,
98 namespace: impl Into<String>,
99 line: usize,
100 ) -> Result<(), ResolveError> {
101 let prefix = prefix.into();
102 let namespace = namespace.into();
103 self.prefixes.insert(prefix.clone(), namespace.clone());
104 self.declarations.push(PrefixDeclaration {
105 prefix,
106 namespace,
107 line,
108 });
109 Ok(())
110 }
111
112 pub fn set_base(&mut self, base_iri: impl Into<String>) {
114 self.base_iri = Some(base_iri.into());
115 }
116
117 pub fn resolve(&self, term: &str) -> Result<ResolveResult, ResolveError> {
123 if let Some(colon) = term.find(':') {
125 let prefix = &term[..colon];
126 let after_colon = &term[colon + 1..];
127
128 if !prefix.is_empty() && self.prefixes.contains_key(prefix) {
130 let ns = &self.prefixes[prefix];
131 return Ok(ResolveResult {
132 iri: format!("{ns}{after_colon}"),
133 source: ResolveSource::Prefix(prefix.to_string()),
134 });
135 }
136
137 if after_colon.starts_with("//") {
139 return Ok(ResolveResult {
140 iri: term.to_string(),
141 source: ResolveSource::Absolute,
142 });
143 }
144
145 if !prefix.is_empty() && Self::is_known_scheme(prefix) {
147 return Ok(ResolveResult {
148 iri: term.to_string(),
149 source: ResolveSource::Absolute,
150 });
151 }
152
153 if !prefix.is_empty() {
155 return Err(ResolveError::UnknownPrefix(prefix.to_string()));
156 }
157 } else if Self::is_absolute_iri(term) {
158 return Ok(ResolveResult {
159 iri: term.to_string(),
160 source: ResolveSource::Absolute,
161 });
162 }
163
164 self.resolve_relative(term).map(|iri| ResolveResult {
166 iri,
167 source: ResolveSource::Base,
168 })
169 }
170
171 pub fn resolve_curie(&self, curie: &str) -> Result<String, ResolveError> {
173 let colon = curie
174 .find(':')
175 .ok_or_else(|| ResolveError::InvalidCurie(curie.to_string()))?;
176 let prefix = &curie[..colon];
177 let local = &curie[colon + 1..];
178 let ns = self
179 .prefixes
180 .get(prefix)
181 .ok_or_else(|| ResolveError::UnknownPrefix(prefix.to_string()))?;
182 Ok(format!("{ns}{local}"))
183 }
184
185 pub fn resolve_relative(&self, relative: &str) -> Result<String, ResolveError> {
187 let base = self.base_iri.as_deref().ok_or(ResolveError::NoBaseIri)?;
188
189 if relative.is_empty() {
190 return Ok(base.to_string());
191 }
192
193 if relative.starts_with('#') {
195 return Ok(format!("{base}{relative}"));
196 }
197
198 let base_path = if let Some(idx) = base.rfind('/') {
200 &base[..=idx]
201 } else {
202 base
203 };
204 Ok(format!("{base_path}{relative}"))
205 }
206
207 pub fn abbreviate(&self, iri: &str) -> Option<String> {
210 let mut best: Option<(&str, &str)> = None;
212 for (prefix, ns) in &self.prefixes {
213 if iri.starts_with(ns.as_str())
214 && best.map_or(true, |(_, best_ns)| ns.len() > best_ns.len())
215 {
216 best = Some((prefix.as_str(), ns.as_str()));
217 }
218 }
219 best.map(|(prefix, ns)| {
220 let local = &iri[ns.len()..];
221 format!("{prefix}:{local}")
222 })
223 }
224
225 pub fn prefix_count(&self) -> usize {
227 self.prefixes.len()
228 }
229
230 pub fn has_base(&self) -> bool {
232 self.base_iri.is_some()
233 }
234
235 pub fn declarations(&self) -> &[PrefixDeclaration] {
237 &self.declarations
238 }
239
240 pub fn is_absolute_iri(s: &str) -> bool {
245 if s.contains(' ') {
246 return false;
247 }
248 if let Some(colon_pos) = s.find(':') {
249 let scheme = &s[..colon_pos];
250 let after = &s[colon_pos + 1..];
251 if scheme.is_empty() {
252 return false;
253 }
254 let scheme_chars_valid = scheme
255 .chars()
256 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.');
257 if !scheme_chars_valid {
258 return false;
259 }
260 if after.starts_with("//") {
262 return true;
263 }
264 Self::is_known_scheme(scheme)
266 } else {
267 false
268 }
269 }
270
271 fn is_known_scheme(scheme: &str) -> bool {
273 matches!(
274 scheme.to_ascii_lowercase().as_str(),
275 "urn"
276 | "mailto"
277 | "data"
278 | "file"
279 | "tel"
280 | "fax"
281 | "news"
282 | "http"
283 | "https"
284 | "ftp"
285 | "ftps"
286 | "ldap"
287 | "ldaps"
288 | "irc"
289 | "ircs"
290 | "xmpp"
291 | "sip"
292 | "sips"
293 | "coap"
294 | "coaps"
295 | "ws"
296 | "wss"
297 | "urn:ietf"
298 | "tag"
299 )
300 }
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
310 fn test_add_prefix_and_resolve_curie() {
311 let mut r = PrefixResolver::new();
312 r.add_prefix("ex", "http://example.org/", 1)
313 .expect("should succeed");
314 assert_eq!(
315 r.resolve_curie("ex:Person").expect("should succeed"),
316 "http://example.org/Person"
317 );
318 }
319
320 #[test]
321 fn test_resolve_curie_unknown_prefix() {
322 let r = PrefixResolver::new();
323 let err = r.resolve_curie("ex:Thing");
324 assert!(matches!(err, Err(ResolveError::UnknownPrefix(_))));
325 }
326
327 #[test]
328 fn test_resolve_curie_no_colon() {
329 let r = PrefixResolver::new();
330 let err = r.resolve_curie("nocolon");
331 assert!(matches!(err, Err(ResolveError::InvalidCurie(_))));
332 }
333
334 #[test]
335 fn test_resolve_curie_empty_local() {
336 let mut r = PrefixResolver::new();
337 r.add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 1)
338 .expect("should succeed");
339 assert_eq!(
340 r.resolve_curie("rdf:").expect("should succeed"),
341 "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
342 );
343 }
344
345 #[test]
348 fn test_resolve_absolute_iri_passthrough() {
349 let r = PrefixResolver::new();
350 let result = r.resolve("http://example.org/foo").expect("should succeed");
351 assert_eq!(result.iri, "http://example.org/foo");
352 assert_eq!(result.source, ResolveSource::Absolute);
353 }
354
355 #[test]
356 fn test_resolve_curie_via_resolve() {
357 let mut r = PrefixResolver::new();
358 r.add_prefix("ex", "http://example.org/", 1)
359 .expect("should succeed");
360 let result = r.resolve("ex:Cat").expect("should succeed");
361 assert_eq!(result.iri, "http://example.org/Cat");
362 assert!(matches!(result.source, ResolveSource::Prefix(_)));
363 }
364
365 #[test]
366 fn test_resolve_relative_via_resolve() {
367 let r = PrefixResolver::with_base("http://example.org/doc/page.ttl");
368 let result = r.resolve("other.ttl").expect("should succeed");
369 assert_eq!(result.iri, "http://example.org/doc/other.ttl");
370 assert_eq!(result.source, ResolveSource::Base);
371 }
372
373 #[test]
376 fn test_resolve_relative_simple() {
377 let r = PrefixResolver::with_base("http://example.org/base/");
378 let iri = r.resolve_relative("foo").expect("should succeed");
379 assert_eq!(iri, "http://example.org/base/foo");
380 }
381
382 #[test]
383 fn test_resolve_relative_no_base() {
384 let r = PrefixResolver::new();
385 let err = r.resolve_relative("foo");
386 assert!(matches!(err, Err(ResolveError::NoBaseIri)));
387 }
388
389 #[test]
390 fn test_resolve_relative_fragment() {
391 let r = PrefixResolver::with_base("http://example.org/ont");
392 let iri = r.resolve_relative("#Alice").expect("should succeed");
393 assert_eq!(iri, "http://example.org/ont#Alice");
394 }
395
396 #[test]
397 fn test_resolve_relative_empty_string_returns_base() {
398 let r = PrefixResolver::with_base("http://example.org/doc");
399 let iri = r.resolve_relative("").expect("should succeed");
400 assert_eq!(iri, "http://example.org/doc");
401 }
402
403 #[test]
406 fn test_set_base_updates() {
407 let mut r = PrefixResolver::new();
408 r.set_base("http://first.org/");
409 r.set_base("http://second.org/");
410 let iri = r.resolve_relative("x").expect("should succeed");
411 assert!(iri.contains("second.org"));
412 }
413
414 #[test]
415 fn test_has_base_false() {
416 let r = PrefixResolver::new();
417 assert!(!r.has_base());
418 }
419
420 #[test]
421 fn test_has_base_true() {
422 let r = PrefixResolver::with_base("http://base.org/");
423 assert!(r.has_base());
424 }
425
426 #[test]
429 fn test_abbreviate_success() {
430 let mut r = PrefixResolver::new();
431 r.add_prefix("ex", "http://example.org/", 1)
432 .expect("should succeed");
433 let abbrev = r.abbreviate("http://example.org/Person");
434 assert_eq!(abbrev.as_deref(), Some("ex:Person"));
435 }
436
437 #[test]
438 fn test_abbreviate_no_match() {
439 let r = PrefixResolver::new();
440 assert!(r.abbreviate("http://unknown.org/foo").is_none());
441 }
442
443 #[test]
444 fn test_abbreviate_prefers_longest_prefix() {
445 let mut r = PrefixResolver::new();
446 r.add_prefix("ex", "http://example.org/", 1)
447 .expect("should succeed");
448 r.add_prefix("exv", "http://example.org/vocab/", 2)
449 .expect("should succeed");
450 let abbrev = r
451 .abbreviate("http://example.org/vocab/Foo")
452 .expect("should succeed");
453 assert_eq!(abbrev, "exv:Foo");
454 }
455
456 #[test]
459 fn test_prefix_count_zero() {
460 let r = PrefixResolver::new();
461 assert_eq!(r.prefix_count(), 0);
462 }
463
464 #[test]
465 fn test_prefix_count_after_adds() {
466 let mut r = PrefixResolver::new();
467 r.add_prefix("ex", "http://example.org/", 1)
468 .expect("should succeed");
469 r.add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 2)
470 .expect("should succeed");
471 assert_eq!(r.prefix_count(), 2);
472 }
473
474 #[test]
475 fn test_prefix_count_redeclaration_same() {
476 let mut r = PrefixResolver::new();
477 r.add_prefix("ex", "http://a.org/", 1)
478 .expect("should succeed");
479 r.add_prefix("ex", "http://b.org/", 2)
480 .expect("should succeed");
481 assert_eq!(r.prefix_count(), 1);
483 }
484
485 #[test]
488 fn test_declarations_list() {
489 let mut r = PrefixResolver::new();
490 r.add_prefix("ex", "http://example.org/", 5)
491 .expect("should succeed");
492 let decls = r.declarations();
493 assert_eq!(decls.len(), 1);
494 assert_eq!(decls[0].prefix, "ex");
495 assert_eq!(decls[0].line, 5);
496 }
497
498 #[test]
499 fn test_declarations_order() {
500 let mut r = PrefixResolver::new();
501 r.add_prefix("a", "http://a.org/", 1)
502 .expect("should succeed");
503 r.add_prefix("b", "http://b.org/", 2)
504 .expect("should succeed");
505 r.add_prefix("c", "http://c.org/", 3)
506 .expect("should succeed");
507 assert_eq!(r.declarations().len(), 3);
508 assert_eq!(r.declarations()[0].prefix, "a");
509 assert_eq!(r.declarations()[2].prefix, "c");
510 }
511
512 #[test]
515 fn test_is_absolute_http() {
516 assert!(PrefixResolver::is_absolute_iri("http://example.org/foo"));
517 }
518
519 #[test]
520 fn test_is_absolute_https() {
521 assert!(PrefixResolver::is_absolute_iri("https://example.org/"));
522 }
523
524 #[test]
525 fn test_is_absolute_urn() {
526 assert!(PrefixResolver::is_absolute_iri("urn:example:a123,z456"));
527 }
528
529 #[test]
530 fn test_is_absolute_relative() {
531 assert!(!PrefixResolver::is_absolute_iri("foo/bar"));
532 }
533
534 #[test]
535 fn test_is_absolute_no_scheme() {
536 assert!(!PrefixResolver::is_absolute_iri("no-colon-here"));
537 }
538
539 #[test]
542 fn test_resolve_source_absolute_variant() {
543 let r = PrefixResolver::new();
544 let result = r.resolve("http://x.org/").expect("should succeed");
545 assert_eq!(result.source, ResolveSource::Absolute);
546 }
547
548 #[test]
549 fn test_resolve_source_prefix_variant() {
550 let mut r = PrefixResolver::new();
551 r.add_prefix("x", "http://x.org/", 1)
552 .expect("should succeed");
553 let result = r.resolve("x:Foo").expect("should succeed");
554 assert!(matches!(result.source, ResolveSource::Prefix(_)));
555 }
556
557 #[test]
558 fn test_resolve_source_base_variant() {
559 let r = PrefixResolver::with_base("http://base.org/");
560 let result = r.resolve("relative").expect("should succeed");
561 assert_eq!(result.source, ResolveSource::Base);
562 }
563
564 #[test]
567 fn test_unknown_prefix_display() {
568 let e = ResolveError::UnknownPrefix("xyz".to_string());
569 assert!(format!("{e}").contains("xyz"));
570 }
571
572 #[test]
573 fn test_no_base_iri_display() {
574 let e = ResolveError::NoBaseIri;
575 assert!(format!("{e}").contains("base"));
576 }
577
578 #[test]
579 fn test_invalid_curie_display() {
580 let e = ResolveError::InvalidCurie("bad".to_string());
581 assert!(format!("{e}").contains("bad"));
582 }
583
584 #[test]
585 fn test_resolver_default() {
586 let r = PrefixResolver::default();
587 assert_eq!(r.prefix_count(), 0);
588 assert!(!r.has_base());
589 }
590
591 #[test]
592 fn test_resolve_unknown_curie_prefix() {
593 let r = PrefixResolver::new();
594 let err = r.resolve("ex:Foo");
596 assert!(matches!(err, Err(ResolveError::UnknownPrefix(_))));
597 }
598}