1use std::ffi::OsStr;
4use std::hash::{Hash, Hasher};
5use std::io;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8
9use serde::de::{DeserializeSeed, Deserializer, MapAccess, Visitor};
10use serde::ser::{Serialize, SerializeStruct, Serializer};
11
12use crate::crate_name::CrateName;
13
14#[derive(Debug, Clone)]
36pub struct WorkspaceFilePath {
37 relative: PathBuf,
38 workspace_root: Arc<Path>,
39 crate_name: CrateName,
40}
41
42impl WorkspaceFilePath {
43 pub(crate) fn new_unchecked(
45 relative: PathBuf,
46 workspace_root: Arc<Path>,
47 crate_name: CrateName,
48 ) -> Self {
49 Self {
50 relative,
51 workspace_root,
52 crate_name,
53 }
54 }
55
56 #[cfg(any(test, feature = "test-utils"))]
58 pub fn new_for_test(
59 relative: impl Into<PathBuf>,
60 workspace_root: impl Into<PathBuf>,
61 crate_name: impl AsRef<str>,
62 ) -> Self {
63 Self {
64 relative: relative.into(),
65 workspace_root: Arc::from(workspace_root.into()),
66 crate_name: CrateName::new_for_test(crate_name.as_ref()),
67 }
68 }
69
70 pub fn as_relative(&self) -> &Path {
72 &self.relative
73 }
74
75 pub fn workspace_root(&self) -> &Path {
77 &self.workspace_root
78 }
79
80 pub fn crate_name(&self) -> &CrateName {
82 &self.crate_name
83 }
84
85 pub fn to_absolute(&self) -> PathBuf {
87 self.workspace_root.join(&self.relative)
88 }
89
90 pub fn canonicalize(&self) -> io::Result<PathBuf> {
92 std::fs::canonicalize(self.to_absolute())
93 }
94
95 pub fn file_name(&self) -> Option<&OsStr> {
97 self.relative.file_name()
98 }
99
100 pub fn extension(&self) -> Option<&OsStr> {
102 self.relative.extension()
103 }
104
105 pub fn parent(&self) -> Option<&Path> {
107 self.relative.parent()
108 }
109
110 pub fn is_rust_file(&self) -> bool {
112 self.extension().is_some_and(|ext| ext == "rs")
113 }
114
115 pub fn is_binary_entry(&self) -> bool {
127 let path_str = self.relative.to_string_lossy();
128
129 if path_str.ends_with("/main.rs") || path_str == "main.rs" {
131 return true;
132 }
133
134 if path_str.contains("/bin/") && path_str.ends_with(".rs") {
136 return true;
137 }
138
139 false
140 }
141
142 pub fn with_context(&self, workspace_root: Arc<Path>, crate_name: CrateName) -> Self {
147 Self {
148 relative: self.relative.clone(),
149 workspace_root,
150 crate_name,
151 }
152 }
153
154 pub fn with_relative(&self, relative: impl Into<PathBuf>) -> Self {
166 Self {
167 relative: relative.into(),
168 workspace_root: self.workspace_root.clone(),
169 crate_name: self.crate_name.clone(),
170 }
171 }
172
173 pub fn sibling(&self, file_name: &str) -> Self {
185 let parent = self.relative.parent().unwrap_or(Path::new(""));
186 let new_relative = parent.join(file_name);
187 self.with_relative(new_relative)
188 }
189
190 pub fn write(&self, content: impl AsRef<[u8]>) -> io::Result<()> {
203 write_with_parents(self.to_absolute(), content)
204 }
205
206 pub fn read(&self) -> io::Result<String> {
208 std::fs::read_to_string(self.to_absolute())
209 }
210
211 pub fn read_bytes(&self) -> io::Result<Vec<u8>> {
213 std::fs::read(self.to_absolute())
214 }
215
216 pub fn exists(&self) -> bool {
218 self.to_absolute().exists()
219 }
220}
221
222pub fn write_with_parents(path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> io::Result<()> {
234 let path = path.as_ref();
235 if let Some(parent) = path.parent() {
236 if !parent.as_os_str().is_empty() && !parent.exists() {
237 std::fs::create_dir_all(parent)?;
238 }
239 }
240 std::fs::write(path, content)
241}
242
243impl PartialEq for WorkspaceFilePath {
247 fn eq(&self, other: &Self) -> bool {
248 self.relative == other.relative && self.crate_name == other.crate_name
249 }
250}
251
252impl Eq for WorkspaceFilePath {}
253
254impl Hash for WorkspaceFilePath {
255 fn hash<H: Hasher>(&self, state: &mut H) {
256 self.relative.hash(state);
257 self.crate_name.hash(state);
258 }
259}
260
261impl AsRef<Path> for WorkspaceFilePath {
262 fn as_ref(&self) -> &Path {
263 &self.relative
264 }
265}
266
267impl std::fmt::Display for WorkspaceFilePath {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 write!(f, "{}", self.relative.display())
270 }
271}
272
273impl Serialize for WorkspaceFilePath {
279 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
280 where
281 S: Serializer,
282 {
283 let mut state = serializer.serialize_struct("WorkspaceFilePath", 2)?;
284 let posix_path = self.relative.to_string_lossy().replace('\\', "/");
286 state.serialize_field("path", &posix_path)?;
287 state.serialize_field("crate_name", &self.crate_name)?;
288 state.end()
289 }
290}
291
292#[allow(dead_code)]
311pub struct WorkspaceFilePathSeed {
312 workspace_root: Arc<Path>,
313}
314
315impl WorkspaceFilePathSeed {
316 #[allow(dead_code)]
318 pub fn new(workspace_root: Arc<Path>) -> Self {
319 Self { workspace_root }
320 }
321}
322
323impl<'de> DeserializeSeed<'de> for WorkspaceFilePathSeed {
324 type Value = WorkspaceFilePath;
325
326 fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
327 where
328 D: Deserializer<'de>,
329 {
330 struct WorkspaceFilePathVisitor {
331 workspace_root: Arc<Path>,
332 }
333
334 impl<'de> Visitor<'de> for WorkspaceFilePathVisitor {
335 type Value = WorkspaceFilePath;
336
337 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
338 formatter.write_str("a struct with 'path' and 'crate_name' fields")
339 }
340
341 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
342 where
343 M: MapAccess<'de>,
344 {
345 let mut path: Option<String> = None;
346 let mut crate_name: Option<CrateName> = None;
347
348 while let Some(key) = map.next_key::<&str>()? {
349 match key {
350 "path" => path = Some(map.next_value()?),
351 "crate_name" => crate_name = Some(map.next_value()?),
352 _ => {
353 let _ = map.next_value::<serde::de::IgnoredAny>()?;
354 }
355 }
356 }
357
358 let path = path.ok_or_else(|| serde::de::Error::missing_field("path"))?;
359 let crate_name =
360 crate_name.ok_or_else(|| serde::de::Error::missing_field("crate_name"))?;
361
362 Ok(WorkspaceFilePath::new_unchecked(
363 PathBuf::from(path),
364 self.workspace_root,
365 crate_name,
366 ))
367 }
368 }
369
370 deserializer.deserialize_struct(
371 "WorkspaceFilePath",
372 &["path", "crate_name"],
373 WorkspaceFilePathVisitor {
374 workspace_root: self.workspace_root,
375 },
376 )
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383
384 #[test]
385 fn test_basic_operations() {
386 let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
387
388 assert_eq!(path.as_relative(), Path::new("src/lib.rs"));
389 assert_eq!(path.workspace_root(), Path::new("/workspace"));
390 assert_eq!(path.crate_name().as_str(), "my_crate");
391 assert_eq!(path.to_absolute(), PathBuf::from("/workspace/src/lib.rs"));
392 assert_eq!(path.file_name(), Some(OsStr::new("lib.rs")));
393 assert_eq!(path.extension(), Some(OsStr::new("rs")));
394 assert!(path.is_rust_file());
395 }
396
397 #[test]
398 fn test_equality_considers_crate_name() {
399 let path1 = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace1", "crate1");
400 let path2 = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace2", "crate1");
401 let path3 = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace1", "crate2");
402 let path4 = WorkspaceFilePath::new_for_test("src/main.rs", "/workspace1", "crate1");
403
404 assert_eq!(path1, path2);
406 assert_ne!(path1, path3);
408 assert_ne!(path1, path4);
410 }
411
412 #[test]
413 fn test_serialization() {
414 let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
415 let json = serde_json::to_string(&path).unwrap();
416 assert_eq!(json, r#"{"path":"src/lib.rs","crate_name":"my_crate"}"#);
417 }
418
419 #[test]
420 fn test_deserialization_with_seed() {
421 use serde::de::DeserializeSeed;
422
423 let json = r#"{"path":"src/lib.rs","crate_name":"my_crate"}"#;
424 let workspace_root = Arc::from(Path::new("/workspace"));
425 let seed = WorkspaceFilePathSeed::new(workspace_root);
426
427 let mut de = serde_json::Deserializer::from_str(json);
428 let path = seed.deserialize(&mut de).unwrap();
429
430 assert_eq!(path.as_relative(), Path::new("src/lib.rs"));
431 assert_eq!(path.crate_name().as_str(), "my_crate");
432 assert_eq!(path.workspace_root(), Path::new("/workspace"));
433 }
434
435 #[test]
436 fn test_with_context() {
437 let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/old", "old_crate");
438 let new_path = path.with_context(
439 Arc::from(Path::new("/new")),
440 CrateName::new_for_test("new_crate"),
441 );
442 assert_eq!(new_path.workspace_root(), Path::new("/new"));
443 assert_eq!(new_path.crate_name().as_str(), "new_crate");
444 }
445
446 #[test]
447 fn test_write_creates_parent_directories() {
448 use tempfile::tempdir;
449
450 let temp = tempdir().unwrap();
451 let workspace_root = temp.path();
452
453 let path = WorkspaceFilePath::new_for_test(
455 "src/deep/nested/module/lib.rs",
456 workspace_root.to_str().unwrap(),
457 "test_crate",
458 );
459
460 assert!(!path.to_absolute().parent().unwrap().exists());
462
463 path.write("// test content").unwrap();
465
466 assert!(path.exists());
468 assert_eq!(path.read().unwrap(), "// test content");
469 }
470
471 #[test]
472 fn test_write_with_parents_utility() {
473 use tempfile::tempdir;
474
475 let temp = tempdir().unwrap();
476 let file_path = temp.path().join("a/b/c/file.txt");
477
478 assert!(!file_path.parent().unwrap().exists());
480
481 write_with_parents(&file_path, "hello").unwrap();
483
484 assert!(file_path.exists());
486 assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "hello");
487 }
488}