use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspacePaths {
root: PathBuf,
}
impl WorkspacePaths {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self { root: root.into() }
}
pub fn current() -> Self {
Self::new(current_dir())
}
pub fn root(&self) -> &Path {
&self.root
}
pub fn resolve_path(&self, path: PathBuf) -> PathBuf {
resolve_path(&self.root, path)
}
pub fn resolve_file(&self, raw: &str) -> Result<PathBuf, EmptyFilePathError> {
resolve_file(&self.root, raw)
}
pub fn resolve_dir(&self, raw: Option<&str>) -> PathBuf {
resolve_dir(&self.root, raw)
}
pub fn relative_path(&self, path: &str) -> String {
relative_path(&self.root, path)
}
}
pub fn current_dir() -> PathBuf {
std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
}
pub fn resolve_path(root: &Path, path: PathBuf) -> PathBuf {
if path.is_absolute() { path } else { root.join(path) }
}
pub fn relative_path(root: &Path, path: &str) -> String {
Path::new(path)
.strip_prefix(root)
.map_or_else(|_| path.to_string(), |relative| relative.to_string_lossy().to_string())
}
#[derive(Debug, thiserror::Error)]
#[error("file path is required and cannot be empty")]
pub struct EmptyFilePathError;
pub fn resolve_file(root: &Path, raw: &str) -> Result<PathBuf, EmptyFilePathError> {
if raw.trim().is_empty() {
return Err(EmptyFilePathError);
}
Ok(resolve_path(root, PathBuf::from(raw)))
}
pub fn resolve_dir(root: &Path, raw: Option<&str>) -> PathBuf {
match raw {
Some(raw) if !raw.trim().is_empty() => resolve_path(root, PathBuf::from(raw)),
_ => root.to_path_buf(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn workspace_paths_resolve_against_root() {
let workspace = WorkspacePaths::new("/workspace");
assert_eq!(workspace.root(), Path::new("/workspace"));
assert_eq!(workspace.resolve_path(PathBuf::from("src/main.rs")), PathBuf::from("/workspace/src/main.rs"));
assert_eq!(workspace.resolve_dir(None), PathBuf::from("/workspace"));
assert_eq!(workspace.relative_path("/workspace/src/main.rs"), "src/main.rs");
assert_eq!(workspace.relative_path("/elsewhere/lib.rs"), "/elsewhere/lib.rs");
}
#[test]
fn resolves_relative_path_against_root() {
assert_eq!(
resolve_path(Path::new("/workspace"), PathBuf::from("src/main.rs")),
PathBuf::from("/workspace/src/main.rs")
);
}
#[test]
fn leaves_absolute_path_untouched() {
assert_eq!(resolve_path(Path::new("/workspace"), PathBuf::from("/etc/passwd")), PathBuf::from("/etc/passwd"));
}
#[test]
fn preserves_non_empty_file_path_text() {
let resolved = resolve_file(Path::new("/workspace"), " leading space.txt ").unwrap();
assert_eq!(resolved, PathBuf::from("/workspace/ leading space.txt "));
}
#[test]
fn rejects_whitespace_only_required_file_path() {
let err = resolve_file(Path::new("/workspace"), " \t ").unwrap_err();
assert!(err.to_string().contains("file path is required"));
}
#[test]
fn optional_empty_dir_resolves_to_root() {
assert_eq!(resolve_dir(Path::new("/workspace"), Some(" ")), PathBuf::from("/workspace"));
assert_eq!(resolve_dir(Path::new("/workspace"), None), PathBuf::from("/workspace"));
}
}