use std::ffi::OsStr;
use std::hash::{Hash, Hasher};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use serde::de::{DeserializeSeed, Deserializer, MapAccess, Visitor};
use serde::ser::{Serialize, SerializeStruct, Serializer};
use crate::crate_name::CrateName;
#[derive(Debug, Clone)]
pub struct WorkspaceFilePath {
relative: PathBuf,
workspace_root: Arc<Path>,
crate_name: CrateName,
}
impl WorkspaceFilePath {
pub(crate) fn new_unchecked(
relative: PathBuf,
workspace_root: Arc<Path>,
crate_name: CrateName,
) -> Self {
Self {
relative,
workspace_root,
crate_name,
}
}
#[cfg(any(test, feature = "test-utils"))]
pub fn new_for_test(
relative: impl Into<PathBuf>,
workspace_root: impl Into<PathBuf>,
crate_name: impl AsRef<str>,
) -> Self {
Self {
relative: relative.into(),
workspace_root: Arc::from(workspace_root.into()),
crate_name: CrateName::new_for_test(crate_name.as_ref()),
}
}
pub fn as_relative(&self) -> &Path {
&self.relative
}
pub fn workspace_root(&self) -> &Path {
&self.workspace_root
}
pub fn crate_name(&self) -> &CrateName {
&self.crate_name
}
pub fn to_absolute(&self) -> PathBuf {
self.workspace_root.join(&self.relative)
}
pub fn canonicalize(&self) -> io::Result<PathBuf> {
std::fs::canonicalize(self.to_absolute())
}
pub fn file_name(&self) -> Option<&OsStr> {
self.relative.file_name()
}
pub fn extension(&self) -> Option<&OsStr> {
self.relative.extension()
}
pub fn parent(&self) -> Option<&Path> {
self.relative.parent()
}
pub fn is_rust_file(&self) -> bool {
self.extension().is_some_and(|ext| ext == "rs")
}
pub fn is_binary_entry(&self) -> bool {
let path_str = self.relative.to_string_lossy();
if path_str.ends_with("/main.rs") || path_str == "main.rs" {
return true;
}
if path_str.contains("/bin/") && path_str.ends_with(".rs") {
return true;
}
false
}
pub fn with_context(&self, workspace_root: Arc<Path>, crate_name: CrateName) -> Self {
Self {
relative: self.relative.clone(),
workspace_root,
crate_name,
}
}
pub fn with_relative(&self, relative: impl Into<PathBuf>) -> Self {
Self {
relative: relative.into(),
workspace_root: self.workspace_root.clone(),
crate_name: self.crate_name.clone(),
}
}
pub fn sibling(&self, file_name: &str) -> Self {
let parent = self.relative.parent().unwrap_or(Path::new(""));
let new_relative = parent.join(file_name);
self.with_relative(new_relative)
}
pub fn write(&self, content: impl AsRef<[u8]>) -> io::Result<()> {
write_with_parents(self.to_absolute(), content)
}
pub fn read(&self) -> io::Result<String> {
std::fs::read_to_string(self.to_absolute())
}
pub fn read_bytes(&self) -> io::Result<Vec<u8>> {
std::fs::read(self.to_absolute())
}
pub fn exists(&self) -> bool {
self.to_absolute().exists()
}
}
pub fn write_with_parents(path: impl AsRef<Path>, content: impl AsRef<[u8]>) -> io::Result<()> {
let path = path.as_ref();
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
std::fs::write(path, content)
}
impl PartialEq for WorkspaceFilePath {
fn eq(&self, other: &Self) -> bool {
self.relative == other.relative && self.crate_name == other.crate_name
}
}
impl Eq for WorkspaceFilePath {}
impl Hash for WorkspaceFilePath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.relative.hash(state);
self.crate_name.hash(state);
}
}
impl AsRef<Path> for WorkspaceFilePath {
fn as_ref(&self) -> &Path {
&self.relative
}
}
impl std::fmt::Display for WorkspaceFilePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.relative.display())
}
}
impl Serialize for WorkspaceFilePath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut state = serializer.serialize_struct("WorkspaceFilePath", 2)?;
let posix_path = self.relative.to_string_lossy().replace('\\', "/");
state.serialize_field("path", &posix_path)?;
state.serialize_field("crate_name", &self.crate_name)?;
state.end()
}
}
#[allow(dead_code)]
pub struct WorkspaceFilePathSeed {
workspace_root: Arc<Path>,
}
impl WorkspaceFilePathSeed {
#[allow(dead_code)]
pub fn new(workspace_root: Arc<Path>) -> Self {
Self { workspace_root }
}
}
impl<'de> DeserializeSeed<'de> for WorkspaceFilePathSeed {
type Value = WorkspaceFilePath;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
struct WorkspaceFilePathVisitor {
workspace_root: Arc<Path>,
}
impl<'de> Visitor<'de> for WorkspaceFilePathVisitor {
type Value = WorkspaceFilePath;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a struct with 'path' and 'crate_name' fields")
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: MapAccess<'de>,
{
let mut path: Option<String> = None;
let mut crate_name: Option<CrateName> = None;
while let Some(key) = map.next_key::<&str>()? {
match key {
"path" => path = Some(map.next_value()?),
"crate_name" => crate_name = Some(map.next_value()?),
_ => {
let _ = map.next_value::<serde::de::IgnoredAny>()?;
}
}
}
let path = path.ok_or_else(|| serde::de::Error::missing_field("path"))?;
let crate_name =
crate_name.ok_or_else(|| serde::de::Error::missing_field("crate_name"))?;
Ok(WorkspaceFilePath::new_unchecked(
PathBuf::from(path),
self.workspace_root,
crate_name,
))
}
}
deserializer.deserialize_struct(
"WorkspaceFilePath",
&["path", "crate_name"],
WorkspaceFilePathVisitor {
workspace_root: self.workspace_root,
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_operations() {
let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
assert_eq!(path.as_relative(), Path::new("src/lib.rs"));
assert_eq!(path.workspace_root(), Path::new("/workspace"));
assert_eq!(path.crate_name().as_str(), "my_crate");
assert_eq!(path.to_absolute(), PathBuf::from("/workspace/src/lib.rs"));
assert_eq!(path.file_name(), Some(OsStr::new("lib.rs")));
assert_eq!(path.extension(), Some(OsStr::new("rs")));
assert!(path.is_rust_file());
}
#[test]
fn test_equality_considers_crate_name() {
let path1 = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace1", "crate1");
let path2 = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace2", "crate1");
let path3 = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace1", "crate2");
let path4 = WorkspaceFilePath::new_for_test("src/main.rs", "/workspace1", "crate1");
assert_eq!(path1, path2);
assert_ne!(path1, path3);
assert_ne!(path1, path4);
}
#[test]
fn test_serialization() {
let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/workspace", "my_crate");
let json = serde_json::to_string(&path).unwrap();
assert_eq!(json, r#"{"path":"src/lib.rs","crate_name":"my_crate"}"#);
}
#[test]
fn test_deserialization_with_seed() {
use serde::de::DeserializeSeed;
let json = r#"{"path":"src/lib.rs","crate_name":"my_crate"}"#;
let workspace_root = Arc::from(Path::new("/workspace"));
let seed = WorkspaceFilePathSeed::new(workspace_root);
let mut de = serde_json::Deserializer::from_str(json);
let path = seed.deserialize(&mut de).unwrap();
assert_eq!(path.as_relative(), Path::new("src/lib.rs"));
assert_eq!(path.crate_name().as_str(), "my_crate");
assert_eq!(path.workspace_root(), Path::new("/workspace"));
}
#[test]
fn test_with_context() {
let path = WorkspaceFilePath::new_for_test("src/lib.rs", "/old", "old_crate");
let new_path = path.with_context(
Arc::from(Path::new("/new")),
CrateName::new_for_test("new_crate"),
);
assert_eq!(new_path.workspace_root(), Path::new("/new"));
assert_eq!(new_path.crate_name().as_str(), "new_crate");
}
#[test]
fn test_write_creates_parent_directories() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let workspace_root = temp.path();
let path = WorkspaceFilePath::new_for_test(
"src/deep/nested/module/lib.rs",
workspace_root.to_str().unwrap(),
"test_crate",
);
assert!(!path.to_absolute().parent().unwrap().exists());
path.write("// test content").unwrap();
assert!(path.exists());
assert_eq!(path.read().unwrap(), "// test content");
}
#[test]
fn test_write_with_parents_utility() {
use tempfile::tempdir;
let temp = tempdir().unwrap();
let file_path = temp.path().join("a/b/c/file.txt");
assert!(!file_path.parent().unwrap().exists());
write_with_parents(&file_path, "hello").unwrap();
assert!(file_path.exists());
assert_eq!(std::fs::read_to_string(&file_path).unwrap(), "hello");
}
}