vtcode_commons/paths/
scopes.rs1use super::canonicalize::canonicalize_workspace;
2use super::normalize::{expand_tilde, file_name_from_path, normalize_path};
3use super::safety::{is_safe_relative_path, validate_path_safety};
4use anyhow::Result;
5use std::path::{Path, PathBuf};
6
7pub trait WorkspacePaths: Send + Sync {
9 fn workspace_root(&self) -> &Path;
11
12 fn config_dir(&self) -> PathBuf;
14
15 fn cache_dir(&self) -> Option<PathBuf> {
17 None
18 }
19
20 fn telemetry_dir(&self) -> Option<PathBuf> {
22 None
23 }
24
25 fn scope_for_path(&self, path: &Path) -> PathScope {
34 if path.starts_with(self.workspace_root()) {
35 return PathScope::Workspace;
36 }
37
38 let config_dir = self.config_dir();
39 if path.starts_with(&config_dir) {
40 return PathScope::Config;
41 }
42
43 if let Some(cache_dir) = self.cache_dir()
44 && path.starts_with(&cache_dir)
45 {
46 return PathScope::Cache;
47 }
48
49 if let Some(telemetry_dir) = self.telemetry_dir()
50 && path.starts_with(&telemetry_dir)
51 {
52 return PathScope::Telemetry;
53 }
54
55 PathScope::Cache
56 }
57}
58
59pub trait PathResolver: WorkspacePaths {
61 fn resolve<P>(&self, relative: P) -> PathBuf
63 where
64 P: AsRef<Path>,
65 {
66 self.workspace_root().join(relative)
67 }
68
69 fn resolve_config<P>(&self, relative: P) -> PathBuf
71 where
72 P: AsRef<Path>,
73 {
74 self.config_dir().join(relative)
75 }
76}
77
78impl<T> PathResolver for T where T: WorkspacePaths + ?Sized {}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum PathScope {
83 Workspace,
84 Config,
85 Cache,
86 Telemetry,
87}
88
89impl PathScope {
90 pub fn description(self) -> &'static str {
92 match self {
93 Self::Workspace => "workspace",
94 Self::Config => "configuration",
95 Self::Cache => "cache",
96 Self::Telemetry => "telemetry",
97 }
98 }
99}
100
101pub trait PathExt {
117 fn normalize(&self) -> PathBuf;
119
120 fn canonicalize_or_self(&self) -> PathBuf;
122
123 fn file_name_str(&self) -> String;
129}
130
131impl PathExt for Path {
132 fn normalize(&self) -> PathBuf {
133 normalize_path(self)
134 }
135
136 fn canonicalize_or_self(&self) -> PathBuf {
137 canonicalize_workspace(self)
138 }
139
140 fn file_name_str(&self) -> String {
141 self.file_name()
142 .and_then(|name| name.to_str())
143 .map(|s| s.to_string())
144 .unwrap_or_else(|| self.to_string_lossy().into_owned())
145 }
146}
147
148pub trait StrPathExt {
159 fn expand_tilde(&self) -> PathBuf;
161
162 fn is_safe_path(&self) -> bool;
164
165 fn validate_safety(&self) -> Result<()>;
167
168 fn file_name_str(&self) -> String;
170}
171
172impl StrPathExt for str {
173 fn expand_tilde(&self) -> PathBuf {
174 expand_tilde(self)
175 }
176
177 fn is_safe_path(&self) -> bool {
178 is_safe_relative_path(self)
179 }
180
181 fn validate_safety(&self) -> Result<()> {
182 validate_path_safety(self)
183 }
184
185 fn file_name_str(&self) -> String {
186 file_name_from_path(self)
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::*;
193
194 struct StaticPaths {
195 root: PathBuf,
196 config: PathBuf,
197 }
198
199 impl WorkspacePaths for StaticPaths {
200 fn workspace_root(&self) -> &Path {
201 &self.root
202 }
203
204 fn config_dir(&self) -> PathBuf {
205 self.config.clone()
206 }
207
208 fn cache_dir(&self) -> Option<PathBuf> {
209 Some(self.root.join("cache"))
210 }
211 }
212
213 #[test]
214 fn resolves_relative_paths() {
215 let paths = StaticPaths {
216 root: PathBuf::from("/tmp/project"),
217 config: PathBuf::from("/tmp/project/config"),
218 };
219
220 assert_eq!(PathResolver::resolve(&paths, "subdir/file.txt"), PathBuf::from("/tmp/project/subdir/file.txt"));
221 assert_eq!(
222 PathResolver::resolve_config(&paths, "settings.toml"),
223 PathBuf::from("/tmp/project/config/settings.toml")
224 );
225 assert_eq!(paths.cache_dir(), Some(PathBuf::from("/tmp/project/cache")));
226 }
227}