Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 9 additions & 38 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.normalize() 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 @@ -701,27 +691,8 @@ 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")]
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