use super::canonicalize::canonicalize_workspace;
use super::normalize::{expand_tilde, file_name_from_path, normalize_path};
use super::safety::{is_safe_relative_path, validate_path_safety};
use anyhow::Result;
use std::path::{Path, PathBuf};
pub trait WorkspacePaths: Send + Sync {
fn workspace_root(&self) -> &Path;
fn config_dir(&self) -> PathBuf;
fn cache_dir(&self) -> Option<PathBuf> {
None
}
fn telemetry_dir(&self) -> Option<PathBuf> {
None
}
fn scope_for_path(&self, path: &Path) -> PathScope {
if path.starts_with(self.workspace_root()) {
return PathScope::Workspace;
}
let config_dir = self.config_dir();
if path.starts_with(&config_dir) {
return PathScope::Config;
}
if let Some(cache_dir) = self.cache_dir()
&& path.starts_with(&cache_dir)
{
return PathScope::Cache;
}
if let Some(telemetry_dir) = self.telemetry_dir()
&& path.starts_with(&telemetry_dir)
{
return PathScope::Telemetry;
}
PathScope::Cache
}
}
pub trait PathResolver: WorkspacePaths {
fn resolve<P>(&self, relative: P) -> PathBuf
where
P: AsRef<Path>,
{
self.workspace_root().join(relative)
}
fn resolve_config<P>(&self, relative: P) -> PathBuf
where
P: AsRef<Path>,
{
self.config_dir().join(relative)
}
}
impl<T> PathResolver for T where T: WorkspacePaths + ?Sized {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathScope {
Workspace,
Config,
Cache,
Telemetry,
}
impl PathScope {
pub fn description(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Config => "configuration",
Self::Cache => "cache",
Self::Telemetry => "telemetry",
}
}
}
pub trait PathExt {
fn normalize(&self) -> PathBuf;
fn canonicalize_or_self(&self) -> PathBuf;
fn file_name_str(&self) -> String;
}
impl PathExt for Path {
fn normalize(&self) -> PathBuf {
normalize_path(self)
}
fn canonicalize_or_self(&self) -> PathBuf {
canonicalize_workspace(self)
}
fn file_name_str(&self) -> String {
self.file_name()
.and_then(|name| name.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| self.to_string_lossy().into_owned())
}
}
pub trait StrPathExt {
fn expand_tilde(&self) -> PathBuf;
fn is_safe_path(&self) -> bool;
fn validate_safety(&self) -> Result<()>;
fn file_name_str(&self) -> String;
}
impl StrPathExt for str {
fn expand_tilde(&self) -> PathBuf {
expand_tilde(self)
}
fn is_safe_path(&self) -> bool {
is_safe_relative_path(self)
}
fn validate_safety(&self) -> Result<()> {
validate_path_safety(self)
}
fn file_name_str(&self) -> String {
file_name_from_path(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct StaticPaths {
root: PathBuf,
config: PathBuf,
}
impl WorkspacePaths for StaticPaths {
fn workspace_root(&self) -> &Path {
&self.root
}
fn config_dir(&self) -> PathBuf {
self.config.clone()
}
fn cache_dir(&self) -> Option<PathBuf> {
Some(self.root.join("cache"))
}
}
#[test]
fn resolves_relative_paths() {
let paths = StaticPaths {
root: PathBuf::from("/tmp/project"),
config: PathBuf::from("/tmp/project/config"),
};
assert_eq!(PathResolver::resolve(&paths, "subdir/file.txt"), PathBuf::from("/tmp/project/subdir/file.txt"));
assert_eq!(
PathResolver::resolve_config(&paths, "settings.toml"),
PathBuf::from("/tmp/project/config/settings.toml")
);
assert_eq!(paths.cache_dir(), Some(PathBuf::from("/tmp/project/cache")));
}
}