pub mod cache;
#[cfg(feature = "https-fetcher")]
mod extract;
pub mod fetcher;
mod path;
pub mod registry;
mod template;
pub mod uri;
use std::path::{Path, PathBuf};
pub use cache::ResolverCache;
pub use fetcher::{FetchError, Fetcher};
pub use registry::{default_fetcher_registry, FetcherRegistry};
pub use uri::{ParsedUri, UriParseError};
#[derive(Debug, Clone)]
pub struct ResolvedNamespace {
pub schema_dir: PathBuf,
pub source_uri: String,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ResolveError {
UnknownScheme { uri: String, scheme: String },
UriParseError { uri: String, source: UriParseError },
PathNotADirectory { path: PathBuf },
RootEscape { path: PathBuf },
Io {
path: PathBuf,
source: std::io::Error,
},
PathUriHasFragmentOrQuery { uri: String },
Fetch { uri: String, source: FetchError },
CacheIo {
path: PathBuf,
source: std::io::Error,
},
}
impl std::fmt::Display for ResolveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResolveError::UnknownScheme { uri, scheme } => {
let user_scheme = uri.split_once(':').map(|(s, _)| s).unwrap_or(uri);
if user_scheme == scheme {
write!(
f,
"namespace URI `{uri}` uses transport scheme `{scheme}:` which has no registered fetcher (known: path:, https:, git:, git+ssh:, plus the github:/gitlab: URL templates)"
)
} else {
write!(
f,
"namespace URI `{uri}` (a `{user_scheme}:` URL template) expands to transport scheme `{scheme}:` which has no registered fetcher (known: path:, https:, git:, git+ssh:)"
)
}
}
ResolveError::UriParseError { uri, source } => {
write!(f, "namespace URI `{uri}` is malformed: {source}")
}
ResolveError::PathNotADirectory { path } => write!(
f,
"namespace URI `path:{}` does not point at an existing directory",
path.display()
),
ResolveError::RootEscape { path } => write!(
f,
"namespace URI `path:{}` escapes the workspace root",
path.display()
),
ResolveError::Io { path, source } => {
write!(f, "{}: namespace resolve io error: {source}", path.display())
}
ResolveError::PathUriHasFragmentOrQuery { uri } => write!(
f,
"namespace URI `{uri}` is a `path:` scheme but carries `#` or `?` — those are remote-only knobs. Drop the fragment/query, or switch to a remote scheme that supports them."
),
ResolveError::Fetch { uri, source } => {
write!(f, "namespace URI `{uri}` fetch failed: {source}")
}
ResolveError::CacheIo { path, source } => write!(
f,
"cache directory `{}` io error: {source}",
path.display()
),
}
}
}
impl std::error::Error for ResolveError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ResolveError::Io { source, .. } => Some(source),
ResolveError::UriParseError { source, .. } => Some(source),
ResolveError::Fetch { source, .. } => Some(source),
ResolveError::CacheIo { source, .. } => Some(source),
_ => None,
}
}
}
pub fn resolve_namespace(
uri: &str,
workspace_root: &Path,
) -> Result<ResolvedNamespace, ResolveError> {
let registry = default_fetcher_registry();
let cache = ResolverCache::user_default().map_err(|source| ResolveError::CacheIo {
path: ResolverCache::default_root(),
source,
})?;
resolve_namespace_with(uri, workspace_root, ®istry, &cache)
}
pub fn resolve_namespace_with(
uri: &str,
workspace_root: &Path,
registry: &FetcherRegistry,
cache: &ResolverCache,
) -> Result<ResolvedNamespace, ResolveError> {
let parsed = ParsedUri::parse(uri).map_err(|source| ResolveError::UriParseError {
uri: uri.to_string(),
source,
})?;
if parsed.scheme == "path" {
return path::resolve(&parsed, uri, workspace_root);
}
let expanded = template::expand(parsed).map_err(|source| ResolveError::UriParseError {
uri: uri.to_string(),
source,
})?;
let fetcher = registry
.get(&expanded.scheme)
.ok_or_else(|| ResolveError::UnknownScheme {
uri: uri.to_string(),
scheme: expanded.scheme.clone(),
})?;
let schema_dir = cache.fetch_or_reuse(&expanded, fetcher.as_ref())?;
Ok(ResolvedNamespace {
schema_dir,
source_uri: uri.to_string(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn fresh_cache() -> (tempfile::TempDir, ResolverCache) {
let tmp = tempfile::tempdir().unwrap();
let cache = ResolverCache::new(tmp.path()).unwrap();
(tmp, cache)
}
#[test]
fn unknown_scheme_yields_typed_error() {
let workspace = tempfile::tempdir().unwrap();
let registry = default_fetcher_registry();
let (_tmp, cache) = fresh_cache();
let err = resolve_namespace_with("ftp:server/path", workspace.path(), ®istry, &cache)
.unwrap_err();
match err {
ResolveError::UnknownScheme { uri, scheme } => {
assert_eq!(uri, "ftp:server/path");
assert_eq!(scheme, "ftp");
let msg = format!(
"{}",
ResolveError::UnknownScheme {
uri,
scheme: scheme.clone()
}
);
assert!(
!msg.contains("expands to"),
"plain transport URI shouldn't use template-expansion phrasing: {msg}"
);
}
other => panic!("expected UnknownScheme, got: {other}"),
}
}
#[test]
fn unknown_scheme_after_template_expansion_names_transport() {
let workspace = tempfile::tempdir().unwrap();
let registry = FetcherRegistry::new(); let (_tmp, cache) = fresh_cache();
let err = resolve_namespace_with("github:acme/repo", workspace.path(), ®istry, &cache)
.unwrap_err();
match err {
ResolveError::UnknownScheme { uri, scheme } => {
assert_eq!(uri, "github:acme/repo");
assert_eq!(scheme, "https", "should report the expanded transport");
let msg = format!(
"{}",
ResolveError::UnknownScheme {
uri: uri.clone(),
scheme: scheme.clone()
}
);
assert!(
msg.contains("expands to") && msg.contains("`https:`"),
"template-expansion diagnostic should name the expanded transport: {msg}"
);
}
other => panic!("expected UnknownScheme, got: {other}"),
}
}
#[test]
fn malformed_uri_yields_parse_error() {
let workspace = tempfile::tempdir().unwrap();
let registry = default_fetcher_registry();
let (_tmp, cache) = fresh_cache();
let err =
resolve_namespace_with("not-a-uri", workspace.path(), ®istry, &cache).unwrap_err();
assert!(matches!(err, ResolveError::UriParseError { .. }));
}
#[test]
fn path_uri_dispatches_to_path_module() {
let workspace = tempfile::tempdir().unwrap();
let dir = workspace.path().join("acme");
std::fs::create_dir(&dir).unwrap();
let registry = default_fetcher_registry();
let (_tmp, cache) = fresh_cache();
let resolved =
resolve_namespace_with("path:acme", workspace.path(), ®istry, &cache).unwrap();
assert_eq!(resolved.schema_dir, dir);
}
#[test]
fn convenience_resolve_namespace_works_for_path() {
let workspace = tempfile::tempdir().unwrap();
let dir = workspace.path().join("acme");
std::fs::create_dir(&dir).unwrap();
let resolved = resolve_namespace("path:acme", workspace.path()).unwrap();
assert_eq!(resolved.schema_dir, dir);
}
}