use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::error::{Error, Result};
pub trait Resolver: fmt::Debug + Send + Sync {
fn resolve(&self, href: &str, base: Option<&str>) -> Result<String>;
fn rebase(&self, href: &str, base: Option<&str>) -> Option<String> {
Some(join(href, base).to_string_lossy().into_owned())
}
}
fn join(href: &str, base: Option<&str>) -> PathBuf {
let path = Path::new(href);
if path.is_absolute() {
return path.to_path_buf();
}
match base.map(Path::new).and_then(Path::parent) {
Some(directory) => directory.join(path),
None => path.to_path_buf(),
}
}
#[derive(Debug, Clone, Default)]
pub struct FileResolver;
impl FileResolver {
#[must_use]
pub fn new() -> Self {
Self
}
}
impl Resolver for FileResolver {
fn resolve(&self, href: &str, base: Option<&str>) -> Result<String> {
if href.starts_with("http://") || href.starts_with("https://") {
return Err(Error::Resolve {
href: href.to_string(),
message: "this resolver does not perform network access; \
fetch the document yourself and supply it through a \
custom Resolver, or vendor it next to the schema"
.to_string(),
});
}
let href = href.strip_prefix("file://").unwrap_or(href);
let path = join(href, base);
std::fs::read_to_string(&path).map_err(|source| Error::Resolve {
href: path.display().to_string(),
message: source.to_string(),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct MemoryResolver {
documents: std::collections::HashMap<String, String>,
}
impl MemoryResolver {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, href: impl Into<String>, source: impl Into<String>) -> Self {
self.documents.insert(href.into(), source.into());
self
}
}
impl Resolver for MemoryResolver {
fn resolve(&self, href: &str, _base: Option<&str>) -> Result<String> {
self.documents
.get(href)
.cloned()
.ok_or_else(|| Error::Resolve {
href: href.to_string(),
message: "not present in this MemoryResolver".to_string(),
})
}
fn rebase(&self, href: &str, _base: Option<&str>) -> Option<String> {
Some(href.to_string())
}
}
pub type SharedResolver = Arc<dyn Resolver>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn file_resolver_refuses_network_uris() {
let error = FileResolver::new()
.resolve("https://example.com/a.sch", None)
.unwrap_err();
assert!(error.to_string().contains("network"), "{error}");
}
#[test]
fn relative_hrefs_join_the_base_directory() {
assert_eq!(
join("b.sch", Some("/schemas/a.sch")),
PathBuf::from("/schemas/b.sch")
);
assert_eq!(
join("sub/b.sch", Some("/schemas/a.sch")),
PathBuf::from("/schemas/sub/b.sch")
);
}
#[test]
fn absolute_hrefs_ignore_the_base() {
assert_eq!(
join("/other/b.sch", Some("/schemas/a.sch")),
PathBuf::from("/other/b.sch")
);
}
#[test]
fn memory_resolver_serves_and_reports_misses() {
let resolver = MemoryResolver::new().with("a", "<x/>");
assert_eq!(resolver.resolve("a", None).unwrap(), "<x/>");
assert!(resolver.resolve("b", None).is_err());
}
}