Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Single-trait library: `SugarPath` trait (`src/sugar_path.rs`) adds path manipula

- **`src/sugar_path.rs`** — Trait definition with doc examples
- **`src/impl_sugar_path.rs`** — All implementations. Two impl blocks: one for `Path`, one for `T: Deref<Target = str>`. Contains `normalize_inner()`, `needs_normalization()`, `relative_str()` and helper functions
- **`src/utils.rs`**`IntoCowPath` trait for flexible base-path input in `absolutize_with`
- **`src/utils.rs`**`get_current_dir()` helper for `absolutize()`

Key patterns:
- `Cow<'_, Path>` return types to avoid allocation when the input is already clean
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,13 @@ assert_eq!(
[`absolutize()`] resolves a relative path against the current working directory. [`absolutize_with()`] lets you supply a custom base.

```rust
use std::borrow::Cow;
use sugar_path::SugarPath;

#[cfg(target_family = "unix")]
{
assert_eq!("./world".absolutize_with("/hello"), "/hello/world".as_path());
assert_eq!("../world".absolutize_with("/hello"), "/world".as_path());
assert_eq!("./world".absolutize_with(Cow::Borrowed("/hello".as_path())), "/hello/world".as_path());
assert_eq!("../world".absolutize_with(Cow::Borrowed("/hello".as_path())), "/world".as_path());
}
```

Expand Down
7 changes: 4 additions & 3 deletions benches/absolutize.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::borrow::Cow;
use std::hint::black_box;
use std::path::Path;

Expand All @@ -21,7 +22,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("absolutize_with", |b| {
b.iter(|| {
for absolute_path in ABSOLUTE_PATHS {
black_box(absolute_path.absolutize_with(&cwd));
black_box(absolute_path.absolutize_with(Cow::Borrowed(cwd.as_path())));
}
})
});
Expand All @@ -37,7 +38,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("absolutize_with_already_clean_absolute", |b| {
b.iter(|| {
for path in ABSOLUTE_PATHS {
black_box(Path::new(path).absolutize_with(&cwd));
black_box(Path::new(path).absolutize_with(Cow::Borrowed(cwd.as_path())));
}
})
});
Expand All @@ -53,7 +54,7 @@ fn criterion_benchmark(c: &mut Criterion) {
c.bench_function("absolutize_with_relative_paths", |b| {
b.iter(|| {
for path in RELATIVE_CLEAN {
black_box(Path::new(path).absolutize_with(&cwd));
black_box(Path::new(path).absolutize_with(Cow::Borrowed(cwd.as_path())));
}
})
});
Expand Down
50 changes: 11 additions & 39 deletions src/impl_sugar_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ use std::{
use memchr::{memchr, memrchr};
use smallvec::SmallVec;

use crate::{
SugarPath,
utils::{IntoCowPath, get_current_dir},
};
use crate::{SugarPath, utils::get_current_dir};

type StrVec<'a> = SmallVec<[&'a str; 8]>;

Expand All @@ -28,23 +25,16 @@ impl SugarPath for Path {
self.absolutize_with(get_current_dir())
}

// Using `Cow` is on purpose.
// - Users could choose to pass a reference or an owned value depending on their use case.
// - If we accept `PathBuf` only, it may cause unnecessary allocations on case that `self` is already absolute.
// - If we accept `&Path` only, it may cause unnecessary cloning that users already have an owned value.
//
// NOTE: we intentionally keep the return lifetime tied to `&self` (not `'a`).
// Unifying them (`&'a self, impl IntoCowPath<'a>) -> Cow<'a, ...>`) would allow
// NOTE: the return lifetime is tied to `&self` (not `'a`).
// Unifying them (`&'a self, Cow<'a, Path>) -> Cow<'a, ...>`) would allow
// borrowing from `base` for noop cases ("", "."), but it constrains callers:
// base's borrowed data must outlive self. That's a semver-breaking trade-off
// for a narrow benefit — callers needing "".absolutize_with(base) can just
// call base.normalize() directly.
fn absolutize_with<'a>(&self, base: impl IntoCowPath<'a>) -> Cow<'_, Path> {
// base's borrowed data must outlive self. Callers needing "".absolutize_with(base)
// can just call base.absolutize() directly.
fn absolutize_with<'a>(&self, base: Cow<'a, Path>) -> Cow<'_, Path> {
if self.is_absolute() {
return self.normalize();
}

let base: Cow<'a, Path> = base.into_cow_path();
let mut base =
if base.is_absolute() { base } else { Cow::Owned(base.absolutize().into_owned()) };

Expand Down Expand Up @@ -431,7 +421,7 @@ impl<T: Deref<Target = str>> SugarPath for T {
self.as_path().absolutize()
}

fn absolutize_with<'a>(&self, base: impl IntoCowPath<'a>) -> Cow<'_, Path> {
fn absolutize_with<'a>(&self, base: Cow<'a, Path>) -> Cow<'_, Path> {
self.as_path().absolutize_with(base)
}

Expand Down Expand Up @@ -672,7 +662,7 @@ fn replace_main_separator(input: &str) -> Option<String> {

#[cfg(test)]
mod tests {
use std::{borrow::Cow, path::Path, path::PathBuf};
use std::{borrow::Cow, path::PathBuf};

use super::SugarPath;

Expand Down Expand Up @@ -701,32 +691,14 @@ mod tests {
#[test]
fn _test_absolutize_with() {
let tmp = "";

let str = "";
tmp.absolutize_with(str);

let string = String::new();
tmp.absolutize_with(string);

let ref_string = &String::new();
tmp.absolutize_with(ref_string);

let path = Path::new("");
tmp.absolutize_with(path);

let path_buf = PathBuf::new();
tmp.absolutize_with(path_buf);

let cow_path = Cow::Borrowed(Path::new(""));
tmp.absolutize_with(cow_path);

let cow_str = Cow::Borrowed("");
tmp.absolutize_with(cow_str);
tmp.absolutize_with(Cow::Borrowed("".as_path()));
tmp.absolutize_with(Cow::Owned(PathBuf::new()));
}

#[cfg(target_family = "unix")]
#[test]
fn normalize() {
use std::path::Path;
assert_eq_str!(Path::new("/foo/../../../bar").normalize(), "/bar");
assert_eq_str!(Path::new("a//b//../b").normalize(), "a/b");
assert_eq_str!(Path::new("/foo/../../../bar").normalize(), "/bar");
Expand Down
9 changes: 5 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,17 @@
//! - [SugarPath::absolutize_with] allows you to absolutize the given path with the base path.
//!
//! ```rust
//! use std::borrow::Cow;
//! use sugar_path::SugarPath;
//! #[cfg(target_family = "unix")]
//! {
//! assert_eq!("./world".absolutize_with("/hello"), "/hello/world".as_path());
//! assert_eq!("../world".absolutize_with("/hello"), "/world".as_path());
//! assert_eq!("./world".absolutize_with(Cow::Borrowed("/hello".as_path())), "/hello/world".as_path());
//! assert_eq!("../world".absolutize_with(Cow::Borrowed("/hello".as_path())), "/world".as_path());
//! }
//! #[cfg(target_family = "windows")]
//! {
//! assert_eq!(".\\world".absolutize_with("C:\\hello"), "C:\\hello\\world".as_path());
//! assert_eq!("..\\world".absolutize_with("C:\\hello"), "C:\\world".as_path());
//! assert_eq!(".\\world".absolutize_with(Cow::Borrowed("C:\\hello".as_path())), "C:\\hello\\world".as_path());
//! assert_eq!("..\\world".absolutize_with(Cow::Borrowed("C:\\hello".as_path())), "C:\\world".as_path());
//! }
//! ```
//!
Expand Down
13 changes: 6 additions & 7 deletions src/sugar_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ use std::{
path::{Path, PathBuf},
};

use crate::utils::IntoCowPath;

pub trait SugarPath {
/// Normalizes the given path, resolving `'..'` and `'.'` segments.
///
Expand Down Expand Up @@ -55,19 +53,20 @@ pub trait SugarPath {
/// ## Examples
///
/// ```rust
/// use std::borrow::Cow;
/// use sugar_path::SugarPath;
/// #[cfg(target_family = "unix")]
/// {
/// assert_eq!("./world".absolutize_with("/hello"), "/hello/world".as_path());
/// assert_eq!("../world".absolutize_with("/hello"), "/world".as_path());
/// assert_eq!("./world".absolutize_with(Cow::Borrowed("/hello".as_path())), "/hello/world".as_path());
/// assert_eq!("../world".absolutize_with(Cow::Borrowed("/hello".as_path())), "/world".as_path());
/// }
/// #[cfg(target_family = "windows")]
/// {
/// assert_eq!(".\\world".absolutize_with("C:\\hello"), "C:\\hello\\world".as_path());
/// assert_eq!("..\\world".absolutize_with("C:\\hello"), "C:\\world".as_path());
/// assert_eq!(".\\world".absolutize_with(Cow::Borrowed("C:\\hello".as_path())), "C:\\hello\\world".as_path());
/// assert_eq!("..\\world".absolutize_with(Cow::Borrowed("C:\\hello".as_path())), "C:\\world".as_path());
/// }
/// ```
fn absolutize_with<'a>(&self, base: impl IntoCowPath<'a>) -> Cow<'_, Path>;
fn absolutize_with<'a>(&self, base: Cow<'a, Path>) -> Cow<'_, Path>;

///
/// ```rust
Expand Down
58 changes: 0 additions & 58 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,61 +14,3 @@ pub fn get_current_dir() -> Cow<'static, Path> {
Cow::Owned(std::env::current_dir().unwrap())
}
}

pub trait IntoCowPath<'a> {
fn into_cow_path(self) -> Cow<'a, Path>;
}

impl<'a> IntoCowPath<'a> for &'a Path {
fn into_cow_path(self) -> Cow<'a, Path> {
Cow::Borrowed(self)
}
}

impl<'a> IntoCowPath<'a> for PathBuf {
fn into_cow_path(self) -> Cow<'a, Path> {
Cow::Owned(self)
}
}

impl<'a> IntoCowPath<'a> for &'a PathBuf {
fn into_cow_path(self) -> Cow<'a, Path> {
Cow::Borrowed(self.as_path())
}
}

impl<'a> IntoCowPath<'a> for &'a str {
fn into_cow_path(self) -> Cow<'a, Path> {
Cow::Borrowed(Path::new(self))
}
}

impl<'a> IntoCowPath<'a> for String {
fn into_cow_path(self) -> Cow<'a, Path> {
Cow::Owned(PathBuf::from(self))
}
}

impl<'a> IntoCowPath<'a> for &'a String {
fn into_cow_path(self) -> Cow<'a, Path> {
Cow::Borrowed(Path::new(self))
}
}

impl<'a> IntoCowPath<'a> for Cow<'a, Path> {
fn into_cow_path(self) -> Cow<'a, Path> {
match self {
Cow::Borrowed(path) => Cow::Borrowed(path),
Cow::Owned(path) => Cow::Owned(path),
}
}
}

impl<'a> IntoCowPath<'a> for Cow<'a, str> {
fn into_cow_path(self) -> Cow<'a, Path> {
match self {
Cow::Borrowed(s) => s.into_cow_path(),
Cow::Owned(s) => s.into_cow_path(),
}
}
}
Loading
Loading