use crate::parser::IncludeResolver;
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
pub fn split_version_selector(path: &str) -> (&str, Option<&str>) {
let Some(index) = path.rfind('@').filter(|index| *index > 0) else {
return (path, None);
};
let (target, selector) = path.split_at(index);
let selector = selector.trim_start_matches('@');
if selector.is_empty() {
return (path, None);
}
(target, Some(selector))
}
pub struct FsIncludeResolver;
impl IncludeResolver for FsIncludeResolver {
fn resolve(
&mut self,
parent_path: Option<&str>,
path: &str,
) -> crate::error::ParserResult<(String, String)> {
let parent = parent_path.map(Path::new).unwrap_or_else(|| Path::new("."));
let base = parent.parent().unwrap_or_else(|| Path::new("."));
let resolved_path = normalize_path(&base.join(path));
read_include(&resolved_path)
}
}
pub struct MountIncludeResolver {
mounts: BTreeMap<String, PathBuf>,
}
impl MountIncludeResolver {
pub fn new(mounts: BTreeMap<String, PathBuf>) -> Self {
Self { mounts }
}
fn mount_for(&self, path: &str) -> Option<(&str, &Path)> {
self.mounts
.iter()
.filter(|(name, _)| {
path.strip_prefix(name.as_str())
.is_some_and(|rest| rest.is_empty() || rest.starts_with('/'))
})
.max_by_key(|(name, _)| name.len())
.map(|(name, root)| (name.as_str(), root.as_path()))
}
}
impl IncludeResolver for MountIncludeResolver {
fn resolve(
&mut self,
parent_path: Option<&str>,
path: &str,
) -> crate::error::ParserResult<(String, String)> {
let Some((name, root)) = self.mount_for(path) else {
return FsIncludeResolver.resolve(parent_path, path);
};
let rest = path[name.len()..].trim_start_matches('/');
let resolved_path = normalize_path(&root.join(rest));
if !resolved_path.is_file() {
return Err(crate::error::ParseError::Message(format!(
"dependency include '{path}' does not exist: the '{name}' mount at '{}' \
resolves it to '{}'",
root.display(),
resolved_path.display()
)));
}
read_include(&resolved_path).map_err(|err| {
crate::error::ParseError::Message(format!(
"dependency include '{path}' could not be read through the '{name}' mount \
at '{}': {err}",
root.display()
))
})
}
}
fn read_include(resolved_path: &Path) -> crate::error::ParserResult<(String, String)> {
if !resolved_path.is_file() {
return Err(crate::error::ParseError::Message(format!(
"include path '{}' does not exist",
resolved_path.display()
)));
}
let content = fs::read_to_string(resolved_path).map_err(|err| {
crate::error::ParseError::Message(format!(
"failed to read include '{}': {err}",
resolved_path.display()
))
})?;
Ok((resolved_path.display().to_string(), content))
}
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
let path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(path)
};
fs::canonicalize(&path).unwrap_or(path)
}