Skip to content

Commit

Permalink
port(turborepo): New Turbopath (vercel/turborepo#4439)
Browse files Browse the repository at this point in the history
### Description

This is meant as a very very simple port of turbopath to Rust. It's
basically the Go code but the Rust version.

The goal here is to make something that works, and that we can extend
with a little ChatGPT and copilot help.

### Testing Instructions

There are doc tests on relevant functions and some regular tests too.
  • Loading branch information
NicholasLYang authored Apr 6, 2023
1 parent 82286fc commit 9acc4b8
Show file tree
Hide file tree
Showing 6 changed files with 579 additions and 0 deletions.
13 changes: 13 additions & 0 deletions crates/turbopath/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "turbopath"
version = "0.1.0"
license = "MPL-2.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
path-slash = "0.2.1"
# TODO: Make this a crate feature
serde = { workspace = true }
thiserror = { workspace = true }
225 changes: 225 additions & 0 deletions crates/turbopath/src/absolute_system_path_buf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
use std::{
borrow::Cow,
ffi::OsStr,
fmt,
path::{Components, Path, PathBuf},
};

use serde::Serialize;

use crate::{AnchoredSystemPathBuf, IntoSystem, PathValidationError, RelativeSystemPathBuf};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize)]
pub struct AbsoluteSystemPathBuf(PathBuf);

impl AbsoluteSystemPathBuf {
/// Create a new AbsoluteSystemPathBuf from `unchecked_path`.
/// Confirms that `unchecked_path` is absolute and converts it to a system
/// path.
///
/// # Arguments
///
/// * `unchecked_path`: The path to be validated and converted to an
/// `AbsoluteSystemPathBuf`.
///
/// returns: Result<AbsoluteSystemPathBuf, PathValidationError>
///
/// # Examples
///
/// ```
/// use std::path::{Path, PathBuf};
/// use turbopath::AbsoluteSystemPathBuf;
/// #[cfg(windows)]
/// let path = PathBuf::from("C:/Users/user");
/// #[cfg(not(windows))]
/// let path = PathBuf::from("/Users/user");
///
/// let absolute_path = AbsoluteSystemPathBuf::new(path).unwrap();
///
/// #[cfg(windows)]
/// assert_eq!(absolute_path.as_path(), Path::new("C:\\Users\\user"));
/// #[cfg(not(windows))]
/// assert_eq!(absolute_path.as_path(), Path::new("/Users/user"));
/// ```
pub fn new(unchecked_path: impl Into<PathBuf>) -> Result<Self, PathValidationError> {
let unchecked_path = unchecked_path.into();
if !unchecked_path.is_absolute() {
return Err(PathValidationError::NotAbsolute(unchecked_path));
}

let system_path = unchecked_path.into_system()?;
Ok(AbsoluteSystemPathBuf(system_path))
}

/// Anchors `path` at `self`.
///
/// # Arguments
///
/// * `path`: The path to be anchored at `self`
///
/// returns: Result<AnchoredSystemPathBuf, PathValidationError>
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use turbopath::{AbsoluteSystemPathBuf, AnchoredSystemPathBuf};
/// #[cfg(not(windows))]
/// {
/// let base = AbsoluteSystemPathBuf::new("/Users/user").unwrap();
/// let anchored_path = AbsoluteSystemPathBuf::new("/Users/user/Documents").unwrap();
/// let anchored_path = base.anchor(&anchored_path).unwrap();
/// assert_eq!(anchored_path.as_path(), Path::new("Documents"));
/// }
///
/// #[cfg(windows)]
/// {
/// let base = AbsoluteSystemPathBuf::new("C:\\Users\\user").unwrap();
/// let anchored_path = AbsoluteSystemPathBuf::new("C:\\Users\\user\\Documents").unwrap();
/// let anchored_path = base.anchor(&anchored_path).unwrap();
/// assert_eq!(anchored_path.as_path(), Path::new("Documents"));
/// }
/// ```
pub fn anchor(
&self,
path: &AbsoluteSystemPathBuf,
) -> Result<AnchoredSystemPathBuf, PathValidationError> {
AnchoredSystemPathBuf::new(self, path)
}

/// Resolves `path` with `self` as anchor.
///
/// # Arguments
///
/// * `path`: The path to be anchored at `self`
///
/// returns: AbsoluteSystemPathBuf
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use turbopath::{AbsoluteSystemPathBuf, AnchoredSystemPathBuf};
/// #[cfg(not(windows))]
/// let absolute_path = AbsoluteSystemPathBuf::new("/Users/user").unwrap();
/// #[cfg(windows)]
/// let absolute_path = AbsoluteSystemPathBuf::new("C:\\Users\\user").unwrap();
///
/// let anchored_path = Path::new("Documents").try_into().unwrap();
/// let resolved_path = absolute_path.resolve(&anchored_path);
///
/// #[cfg(not(windows))]
/// assert_eq!(resolved_path.as_path(), Path::new("/Users/user/Documents"));
/// #[cfg(windows)]
/// assert_eq!(resolved_path.as_path(), Path::new("C:\\Users\\user\\Documents"));
/// ```
pub fn resolve(&self, path: &AnchoredSystemPathBuf) -> AbsoluteSystemPathBuf {
AbsoluteSystemPathBuf(self.0.join(path.as_path()))
}

pub fn as_path(&self) -> &Path {
self.0.as_path()
}

pub fn components(&self) -> Components<'_> {
self.0.components()
}

pub fn parent(&self) -> Option<Self> {
self.0
.parent()
.map(|p| AbsoluteSystemPathBuf(p.to_path_buf()))
}

pub fn starts_with<P: AsRef<Path>>(&self, base: P) -> bool {
self.0.starts_with(base.as_ref())
}

pub fn ends_with<P: AsRef<Path>>(&self, child: P) -> bool {
self.0.ends_with(child.as_ref())
}

pub fn join_relative(&self, path: RelativeSystemPathBuf) -> AbsoluteSystemPathBuf {
AbsoluteSystemPathBuf(self.0.join(path.as_path()))
}

pub fn to_str(&self) -> Result<&str, PathValidationError> {
self.0
.to_str()
.ok_or_else(|| PathValidationError::InvalidUnicode(self.0.clone()))
}

pub fn to_string_lossy(&self) -> Cow<'_, str> {
self.0.to_string_lossy()
}

pub fn file_name(&self) -> Option<&OsStr> {
self.0.file_name()
}

pub fn exists(&self) -> bool {
self.0.exists()
}

pub fn extension(&self) -> Option<&OsStr> {
self.0.extension()
}
}

impl Into<PathBuf> for AbsoluteSystemPathBuf {
fn into(self) -> PathBuf {
self.0
}
}

impl fmt::Display for AbsoluteSystemPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.display().fmt(f)
}
}

impl AsRef<Path> for AbsoluteSystemPathBuf {
fn as_ref(&self) -> &Path {
self.0.as_path()
}
}

#[cfg(test)]
mod tests {
use std::assert_matches::assert_matches;

use crate::{AbsoluteSystemPathBuf, PathValidationError};

#[cfg(not(windows))]
#[test]
fn test_absolute_system_path_buf_on_unix() {
assert!(AbsoluteSystemPathBuf::new("/Users/user").is_ok());
assert_matches!(
AbsoluteSystemPathBuf::new("./Users/user/"),
Err(PathValidationError::NotAbsolute(_))
);

assert_matches!(
AbsoluteSystemPathBuf::new("Users"),
Err(PathValidationError::NotAbsolute(_))
);
}

#[cfg(windows)]
#[test]
fn test_absolute_system_path_buf_on_windows() {
assert!(AbsoluteSystemPathBuf::new("C:\\Users\\user").is_ok());
assert_matches!(
AbsoluteSystemPathBuf::new(".\\Users\\user\\"),
Err(PathValidationError::NotAbsolute(_))
);
assert_matches!(
AbsoluteSystemPathBuf::new("Users"),
Err(PathValidationError::NotAbsolute(_))
);
assert_matches!(
AbsoluteSystemPathBuf::new("/Users/home"),
Err(PathValidationError::NotAbsolute(_))
)
}
}
51 changes: 51 additions & 0 deletions crates/turbopath/src/anchored_system_path_buf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::path::{Path, PathBuf};

use serde::Serialize;

use crate::{AbsoluteSystemPathBuf, IntoSystem, PathValidationError};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize)]
pub struct AnchoredSystemPathBuf(PathBuf);

impl TryFrom<&Path> for AnchoredSystemPathBuf {
type Error = PathValidationError;

fn try_from(path: &Path) -> Result<Self, Self::Error> {
if path.is_absolute() {
return Err(PathValidationError::NotRelative(path.to_path_buf()));
}

Ok(AnchoredSystemPathBuf(path.into_system()?))
}
}

impl AnchoredSystemPathBuf {
pub fn new(
root: &AbsoluteSystemPathBuf,
path: &AbsoluteSystemPathBuf,
) -> Result<Self, PathValidationError> {
let stripped_path = path
.as_path()
.strip_prefix(root.as_path())
.map_err(|_| PathValidationError::NotParent(root.to_string(), path.to_string()))?
.to_path_buf();

Ok(AnchoredSystemPathBuf(stripped_path))
}

pub fn as_path(&self) -> &Path {
self.0.as_path()
}

pub fn to_str(&self) -> Result<&str, PathValidationError> {
self.0
.to_str()
.ok_or_else(|| PathValidationError::InvalidUnicode(self.0.clone()))
}
}

impl Into<PathBuf> for AnchoredSystemPathBuf {
fn into(self) -> PathBuf {
self.0
}
}
59 changes: 59 additions & 0 deletions crates/turbopath/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#![feature(assert_matches)]

mod absolute_system_path_buf;
mod anchored_system_path_buf;
mod relative_system_path_buf;
mod relative_unix_path_buf;

use std::path::{Path, PathBuf};

pub use absolute_system_path_buf::AbsoluteSystemPathBuf;
pub use anchored_system_path_buf::AnchoredSystemPathBuf;
use path_slash::{PathBufExt, PathExt};
pub use relative_system_path_buf::RelativeSystemPathBuf;
pub use relative_unix_path_buf::RelativeUnixPathBuf;
use thiserror::Error;

// Custom error type for path validation errors
#[derive(Debug, Error)]
pub enum PathValidationError {
#[error("Path is non-UTF-8: {0}")]
InvalidUnicode(PathBuf),
#[error("Path is not absolute: {0}")]
NotAbsolute(PathBuf),
#[error("Path is not relative: {0}")]
NotRelative(PathBuf),
#[error("Path {0} is not parent of {1}")]
NotParent(String, String),
}

trait IntoSystem {
fn into_system(self) -> Result<PathBuf, PathValidationError>;
}

trait IntoUnix {
fn into_unix(self) -> Result<PathBuf, PathValidationError>;
}

impl IntoSystem for &Path {
fn into_system(self) -> Result<PathBuf, PathValidationError> {
let path_str = self
.to_str()
.ok_or_else(|| PathValidationError::InvalidUnicode(self.to_owned()))?;

Ok(PathBuf::from_slash(path_str))
}
}

impl IntoUnix for &Path {
/// NOTE: `into_unix` *only* converts Windows paths to Unix paths *on* a
/// Windows system. Do not pass a Windows path on a Unix system and
/// assume it'll be converted.
fn into_unix(self) -> Result<PathBuf, PathValidationError> {
Ok(PathBuf::from(
self.to_slash()
.ok_or_else(|| PathValidationError::InvalidUnicode(self.to_owned()))?
.as_ref(),
))
}
}
Loading

0 comments on commit 9acc4b8

Please sign in to comment.