use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LazyIri {
Resolved(Arc<String>),
Prefixed {
prefix: String,
local: String,
prefixes: Arc<HashMap<String, String>>,
},
Relative {
iri: String,
base: Arc<Option<String>>,
},
}
impl LazyIri {
pub fn from_resolved(iri: impl Into<String>) -> Self {
Self::Resolved(Arc::new(iri.into()))
}
pub fn from_prefixed(
prefix: impl Into<String>,
local: impl Into<String>,
prefixes: HashMap<String, String>,
) -> Self {
Self::Prefixed {
prefix: prefix.into(),
local: local.into(),
prefixes: Arc::new(prefixes),
}
}
pub fn from_relative(iri: impl Into<String>, base: Option<String>) -> Self {
Self::Relative {
iri: iri.into(),
base: Arc::new(base),
}
}
pub fn resolve(&self) -> Result<Cow<'_, str>, IriResolutionError> {
match self {
Self::Resolved(iri) => Ok(Cow::Borrowed(iri.as_str())),
Self::Prefixed {
prefix,
local,
prefixes,
} => {
let namespace = prefixes
.get(prefix)
.ok_or_else(|| IriResolutionError::UndefinedPrefix(prefix.clone()))?;
Ok(Cow::Owned(format!("{}{}", namespace, local)))
}
Self::Relative { iri, base } => {
let base_iri = base
.as_ref()
.as_ref()
.ok_or(IriResolutionError::MissingBase)?;
Ok(Cow::Owned(Self::resolve_relative(base_iri, iri)))
}
}
}
fn resolve_relative(base: &str, relative: &str) -> String {
if relative.contains("://") {
return relative.to_string();
}
let (base_scheme, base_authority, base_path, _base_query, _base_fragment) =
Self::parse_iri(base);
if relative.starts_with("//") {
return format!("{}:{}", base_scheme, relative);
}
if relative.starts_with('/') {
let authority_str = if base_authority.is_empty() {
String::new()
} else {
format!("//{}", base_authority)
};
return format!("{}:{}{}", base_scheme, authority_str, relative);
}
if relative.starts_with('?') || relative.starts_with('#') {
let authority_str = if base_authority.is_empty() {
String::new()
} else {
format!("//{}", base_authority)
};
return format!("{}:{}{}{}", base_scheme, authority_str, base_path, relative);
}
let merged_path = Self::merge_paths(&base_path, relative, !base_authority.is_empty());
let normalized_path = Self::remove_dot_segments(&merged_path);
let (rel_path_only, rel_suffix) = if let Some(pos) = relative.find('?') {
(&relative[..pos], &relative[pos..])
} else if let Some(pos) = relative.find('#') {
(&relative[..pos], &relative[pos..])
} else {
(relative, "")
};
let _ = rel_path_only;
let authority_str = if base_authority.is_empty() {
String::new()
} else {
format!("//{}", base_authority)
};
let final_path = if !base_authority.is_empty() && !normalized_path.starts_with('/') {
format!("/{}", normalized_path)
} else {
normalized_path
};
format!(
"{}:{}{}{}",
base_scheme, authority_str, final_path, rel_suffix
)
}
fn parse_iri(iri: &str) -> (String, String, String, String, String) {
let (scheme, rest) = if let Some(pos) = iri.find("://") {
(&iri[..pos], &iri[pos + 3..])
} else {
("", iri)
};
let (authority, rest) = if !scheme.is_empty() {
if let Some(pos) = rest.find('/') {
(&rest[..pos], &rest[pos..])
} else if let Some(pos) = rest.find(['?', '#']) {
(&rest[..pos], &rest[pos..])
} else {
(rest, "")
}
} else {
("", rest)
};
let (path_query, fragment) = if let Some(pos) = rest.find('#') {
(&rest[..pos], &rest[pos..])
} else {
(rest, "")
};
let (path, query) = if let Some(pos) = path_query.find('?') {
(&path_query[..pos], &path_query[pos..])
} else {
(path_query, "")
};
(
scheme.to_string(),
authority.to_string(),
path.to_string(),
query.to_string(),
fragment.to_string(),
)
}
fn merge_paths(base_path: &str, relative_path: &str, has_authority: bool) -> String {
if has_authority && base_path.is_empty() {
format!("/{}", relative_path)
} else if let Some(pos) = base_path.rfind('/') {
format!("{}/{}", &base_path[..pos], relative_path)
} else {
relative_path.to_string()
}
}
fn remove_dot_segments(path: &str) -> String {
let mut output = Vec::new();
let segments: Vec<&str> = path.split('/').collect();
for segment in segments {
match segment {
"" | "." => {
}
".." => {
output.pop();
}
_ => {
output.push(segment);
}
}
}
if path.starts_with('/') {
format!("/{}", output.join("/"))
} else if output.is_empty() {
String::new()
} else {
output.join("/")
}
}
pub fn is_resolved(&self) -> bool {
matches!(self, Self::Resolved(_))
}
pub fn compact_form(&self) -> String {
match self {
Self::Resolved(iri) => format!("<{}>", iri),
Self::Prefixed { prefix, local, .. } => format!("{}:{}", prefix, local),
Self::Relative { iri, .. } => format!("<{}>", iri),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IriResolutionError {
UndefinedPrefix(String),
MissingBase,
InvalidFormat(String),
}
impl std::fmt::Display for IriResolutionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UndefinedPrefix(prefix) => write!(f, "Undefined prefix: {}", prefix),
Self::MissingBase => write!(f, "Missing base IRI for relative resolution"),
Self::InvalidFormat(msg) => write!(f, "Invalid IRI format: {}", msg),
}
}
}
impl std::error::Error for IriResolutionError {}
#[derive(Debug, Clone)]
pub struct CachedIriResolver {
cache: HashMap<String, Arc<String>>,
stats: ResolverStats,
}
#[derive(Debug, Clone, Default)]
pub struct ResolverStats {
pub total_requests: usize,
pub cache_hits: usize,
pub cache_misses: usize,
}
impl CachedIriResolver {
pub fn new() -> Self {
Self::with_capacity(512)
}
pub fn with_capacity(capacity: usize) -> Self {
Self {
cache: HashMap::with_capacity(capacity),
stats: ResolverStats::default(),
}
}
pub fn resolve_prefixed(
&mut self,
prefix: &str,
local: &str,
prefixes: &HashMap<String, String>,
) -> Result<Arc<String>, IriResolutionError> {
self.stats.total_requests += 1;
let cache_key = format!("{}:{}", prefix, local);
if let Some(cached) = self.cache.get(&cache_key) {
self.stats.cache_hits += 1;
return Ok(cached.clone());
}
self.stats.cache_misses += 1;
let namespace = prefixes
.get(prefix)
.ok_or_else(|| IriResolutionError::UndefinedPrefix(prefix.to_string()))?;
let resolved = Arc::new(format!("{}{}", namespace, local));
self.cache.insert(cache_key, resolved.clone());
Ok(resolved)
}
pub fn resolve_relative(
&mut self,
relative: &str,
base: &str,
) -> Result<Arc<String>, IriResolutionError> {
self.stats.total_requests += 1;
let cache_key = format!("{}<{}>", base, relative);
if let Some(cached) = self.cache.get(&cache_key) {
self.stats.cache_hits += 1;
return Ok(cached.clone());
}
self.stats.cache_misses += 1;
let resolved = Arc::new(LazyIri::resolve_relative(base, relative));
self.cache.insert(cache_key, resolved.clone());
Ok(resolved)
}
pub fn resolve_lazy(&mut self, iri: &LazyIri) -> Result<Arc<String>, IriResolutionError> {
match iri {
LazyIri::Resolved(resolved) => {
self.stats.total_requests += 1;
self.stats.cache_hits += 1; Ok(resolved.clone())
}
LazyIri::Prefixed {
prefix,
local,
prefixes,
} => self.resolve_prefixed(prefix, local, prefixes),
LazyIri::Relative { iri: rel_iri, base } => {
let base_str = base
.as_ref()
.as_ref()
.ok_or(IriResolutionError::MissingBase)?;
self.resolve_relative(rel_iri, base_str)
}
}
}
pub fn cache_hit_rate(&self) -> f64 {
if self.stats.total_requests == 0 {
return 0.0;
}
self.stats.cache_hits as f64 / self.stats.total_requests as f64
}
pub fn stats(&self) -> &ResolverStats {
&self.stats
}
pub fn clear_cache(&mut self) {
self.cache.clear();
self.stats = ResolverStats::default();
}
pub fn cache_size(&self) -> usize {
self.cache.len()
}
pub fn shrink_to_fit(&mut self) {
self.cache.shrink_to_fit();
}
}
impl Default for CachedIriResolver {
fn default() -> Self {
Self::new()
}
}
impl ResolverStats {
pub fn report(&self) -> String {
let hit_rate = if self.total_requests > 0 {
(self.cache_hits as f64 / self.total_requests as f64) * 100.0
} else {
0.0
};
format!(
"IRI Resolver Statistics:\n\
- Total requests: {}\n\
- Cache hits: {} ({:.1}%)\n\
- Cache misses: {}",
self.total_requests, self.cache_hits, hit_rate, self.cache_misses
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lazy_iri_resolved() {
let iri = LazyIri::from_resolved("http://example.org/");
assert!(iri.is_resolved());
assert_eq!(iri.resolve().expect("valid IRI"), "http://example.org/");
}
#[test]
fn test_lazy_iri_prefixed() {
let mut prefixes = HashMap::new();
prefixes.insert("ex".to_string(), "http://example.org/".to_string());
let iri = LazyIri::from_prefixed("ex", "Person", prefixes);
assert!(!iri.is_resolved());
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/Person"
);
}
#[test]
fn test_lazy_iri_undefined_prefix() {
let prefixes = HashMap::new();
let iri = LazyIri::from_prefixed("ex", "Person", prefixes);
assert!(iri.resolve().is_err());
}
#[test]
fn test_lazy_iri_relative() {
let iri =
LazyIri::from_relative("relative/path", Some("http://example.org/base".to_string()));
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/relative/path"
);
}
#[test]
fn test_rfc3986_dot_segments() {
let iri =
LazyIri::from_relative("../sibling", Some("http://example.org/a/b/c".to_string()));
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/a/sibling"
);
let iri = LazyIri::from_relative("./current", Some("http://example.org/a/".to_string()));
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/a/current"
);
let iri =
LazyIri::from_relative("../../up", Some("http://example.org/a/b/c/d".to_string()));
assert_eq!(iri.resolve().expect("valid IRI"), "http://example.org/a/up");
let iri = LazyIri::from_relative("c", Some("http://example.org/a/b".to_string()));
assert_eq!(iri.resolve().expect("valid IRI"), "http://example.org/a/c");
let iri = LazyIri::from_relative("c", Some("http://example.org/a/b/".to_string()));
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/a/b/c"
);
}
#[test]
fn test_rfc3986_absolute_path() {
let iri = LazyIri::from_relative("/absolute", Some("http://example.org/a/b/c".to_string()));
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/absolute"
);
}
#[test]
fn test_rfc3986_query_fragment() {
let iri = LazyIri::from_relative("?query", Some("http://example.org/path".to_string()));
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/path?query"
);
let iri = LazyIri::from_relative("#fragment", Some("http://example.org/path".to_string()));
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://example.org/path#fragment"
);
}
#[test]
fn test_rfc3986_network_path() {
let iri = LazyIri::from_relative(
"//other.example.org/path",
Some("http://example.org/base".to_string()),
);
assert_eq!(
iri.resolve().expect("valid IRI"),
"http://other.example.org/path"
);
}
#[test]
fn test_rfc3986_already_absolute() {
let iri = LazyIri::from_relative(
"https://absolute.org/path",
Some("http://example.org/base".to_string()),
);
assert_eq!(
iri.resolve().expect("valid IRI"),
"https://absolute.org/path"
);
}
#[test]
fn test_lazy_iri_relative_no_base() {
let iri = LazyIri::from_relative("relative/path", None);
assert!(iri.resolve().is_err());
}
#[test]
fn test_cached_resolver_basic() {
let mut resolver = CachedIriResolver::new();
let mut prefixes = HashMap::new();
prefixes.insert("ex".to_string(), "http://example.org/".to_string());
let iri1 = resolver
.resolve_prefixed("ex", "Person", &prefixes)
.expect("resolution should succeed");
assert_eq!(*iri1, "http://example.org/Person");
assert_eq!(resolver.stats().cache_misses, 1);
let iri2 = resolver
.resolve_prefixed("ex", "Person", &prefixes)
.expect("resolution should succeed");
assert_eq!(*iri2, "http://example.org/Person");
assert_eq!(resolver.stats().cache_hits, 1);
assert!(Arc::ptr_eq(&iri1, &iri2));
}
#[test]
fn test_cached_resolver_hit_rate() {
let mut resolver = CachedIriResolver::new();
let mut prefixes = HashMap::new();
prefixes.insert("ex".to_string(), "http://example.org/".to_string());
resolver
.resolve_prefixed("ex", "Person", &prefixes)
.expect("resolution should succeed");
resolver
.resolve_prefixed("ex", "Person", &prefixes)
.expect("resolution should succeed");
assert_eq!(resolver.cache_hit_rate(), 0.5); }
#[test]
fn test_cached_resolver_relative() {
let mut resolver = CachedIriResolver::new();
let iri1 = resolver
.resolve_relative("path", "http://example.org/")
.expect("resolution should succeed");
assert_eq!(*iri1, "http://example.org/path");
let iri2 = resolver
.resolve_relative("path", "http://example.org/")
.expect("resolution should succeed");
assert!(Arc::ptr_eq(&iri1, &iri2));
}
#[test]
fn test_cached_resolver_clear() {
let mut resolver = CachedIriResolver::new();
let mut prefixes = HashMap::new();
prefixes.insert("ex".to_string(), "http://example.org/".to_string());
resolver
.resolve_prefixed("ex", "Person", &prefixes)
.expect("resolution should succeed");
assert_eq!(resolver.cache_size(), 1);
resolver.clear_cache();
assert_eq!(resolver.cache_size(), 0);
assert_eq!(resolver.stats().total_requests, 0);
}
#[test]
fn test_compact_form() {
let mut prefixes = HashMap::new();
prefixes.insert("ex".to_string(), "http://example.org/".to_string());
let iri1 = LazyIri::from_resolved("http://example.org/test");
assert_eq!(iri1.compact_form(), "<http://example.org/test>");
let iri2 = LazyIri::from_prefixed("ex", "Person", prefixes);
assert_eq!(iri2.compact_form(), "ex:Person");
}
#[test]
fn test_resolver_stats_report() {
let mut resolver = CachedIriResolver::new();
let mut prefixes = HashMap::new();
prefixes.insert("ex".to_string(), "http://example.org/".to_string());
resolver
.resolve_prefixed("ex", "Person", &prefixes)
.expect("resolution should succeed");
resolver
.resolve_prefixed("ex", "Person", &prefixes)
.expect("resolution should succeed");
let report = resolver.stats().report();
assert!(report.contains("Total requests: 2"));
assert!(report.contains("Cache hits: 1"));
assert!(report.contains("50.0%"));
}
#[test]
fn test_resolve_lazy_resolved() {
let mut resolver = CachedIriResolver::new();
let iri = LazyIri::from_resolved("http://example.org/test");
let resolved = resolver.resolve_lazy(&iri).expect("valid IRI");
assert_eq!(*resolved, "http://example.org/test");
assert_eq!(resolver.cache_hit_rate(), 1.0); }
#[test]
fn test_resolve_lazy_prefixed() {
let mut resolver = CachedIriResolver::new();
let mut prefixes = HashMap::new();
prefixes.insert("ex".to_string(), "http://example.org/".to_string());
let iri = LazyIri::from_prefixed("ex", "Person", prefixes);
let resolved = resolver.resolve_lazy(&iri).expect("valid IRI");
assert_eq!(*resolved, "http://example.org/Person");
}
}