paved 0.6.0

A simple platform agnostic path representation
Documentation
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use crate::*;

#[test]
fn test_new_default_types() {
    let abs = PavedPath::new_absolute();
    assert!(abs.is_absolute());
    assert!(!abs.is_relative());
    assert_eq!(abs.get_type(), PathType::Absolute);

    let rel = PavedPath::new_relative();
    assert!(rel.is_relative());
    assert!(!rel.is_absolute());
    assert_eq!(rel.get_type(), PathType::Relative);
}

#[test]
fn test_push_and_get_dirs() {
    let path = PavedPath::new_relative()
        .with_dir("a")
        .with_dir("b")
        .with_dir("c");

    let dirs = path.get_dirs();
    assert_eq!(dirs.len(), 3);
    assert_eq!(dirs[0], "a");
    assert_eq!(dirs[2], "c");
}

#[test]
fn test_push_dir_normalization() {
    let path = PavedPath::new_relative()
        .with_dir("a/b")
        .with_dir(".")
        .with_dir("..")
        .with_dir("c");

    let dirs = path.get_dirs();
    assert_eq!(dirs, &[OsString::from("a"), OsString::from("c")]);
}

#[test]
fn test_file_handling() {
    let path = PavedPath::new_relative()
        .with_dir("foo")
        .with_file("bar.txt");

    assert!(path.has_file());
    assert_eq!(path.get_file(), Some(&OsString::from("bar.txt")));

    let no_file = path.clone().without_file();
    assert!(!no_file.has_file());
    assert_eq!(no_file.get_file(), None);
}

#[test]
fn test_path_in_style_unix() {
    let path = PavedPath::new_absolute()
        .with_dir("usr")
        .with_dir("bin")
        .with_file("bash");

    let unix_path = path.path_in_style(PlatformStyle::Unix);
    assert_eq!(unix_path, Path::new("/usr/bin/bash"));
}

#[test]
fn test_path_in_style_windows() {
    let path = PavedPath::new_absolute()
        .with_prefix(PathPrefix::Drive('C'))
        .with_dir("Users")
        .with_dir("Test")
        .with_file("file.txt");

    let win_path = path.path_in_style(PlatformStyle::Windows);
    let s = win_path.to_string_lossy();
    assert!(
        s.starts_with("C:\\") || s.starts_with("C:/"),
        "unexpected prefix: {s}"
    );
    assert!(s.ends_with("file.txt"));
}

#[test]
fn test_remove_prefix() {
    let path = PavedPath::new_absolute()
        .with_prefix(PathPrefix::Drive('D'))
        .without_prefix();

    assert!(path.get_prefix().is_none());
}

#[test]
fn test_pop_last_dir() {
    let mut path = PavedPath::new_relative().with_dir("one").with_dir("two");
    let last = path.pop_dir();

    assert_eq!(last, Some(OsString::from("two")));
    assert_eq!(path.last(), Some(&OsString::from("one")));
}

#[test]
fn test_conversion_from_and_to_path() {
    let std_path = PathBuf::from("/usr/local/bin/bash");
    let paved = PavedPath::from(std_path.clone());

    assert!(paved.is_absolute());
    assert_eq!(paved.get_file(), Some(&OsString::from("bash")));
    assert_eq!(
        paved.get_dirs(),
        &vec![
            OsString::from("usr"),
            OsString::from("local"),
            OsString::from("bin")
        ]
    );

    let unix_path = paved.path_in_style(PlatformStyle::Unix);
    assert_eq!(unix_path, std_path);

    let unix_string = unix_path.to_string_lossy().to_string();
    assert_eq!(unix_string, "/usr/local/bin/bash");
}

#[test]
fn test_from_dirs_path_moves_file_to_dir() {
    let original = PavedPath::from(Path::new("foo/bar/baz.txt"));
    let dirs_only = PavedPath::from_dirs_path("foo/bar/baz.txt");

    assert!(dirs_only.get_file().is_none());
    assert_eq!(
        dirs_only.get_dirs(),
        &vec![
            OsString::from("foo"),
            OsString::from("bar"),
            OsString::from("baz.txt")
        ]
    );
    assert_ne!(original.get_dirs(), dirs_only.get_dirs());
}

#[test]
fn test_add_and_add_assign() {
    let left = PavedPath::new_relative().with_dir("a").with_dir("b");
    let right = PavedPath::new_relative().with_dir("c").with_file("d.txt");

    let combined = left.clone() + right.clone();
    assert_eq!(
        combined.get_dirs(),
        &vec![
            OsString::from("a"),
            OsString::from("b"),
            OsString::from("c")
        ]
    );
    assert_eq!(combined.get_file(), Some(&OsString::from("d.txt")));

    let mut left_mut = left.clone();
    left_mut += right;
    assert_eq!(
        left_mut.get_dirs(),
        &vec![
            OsString::from("a"),
            OsString::from("b"),
            OsString::from("c")
        ]
    );
    assert_eq!(left_mut.get_file(), Some(&OsString::from("d.txt")));
}

#[test]
fn test_extend_trait() {
    let mut path = PavedPath::new_relative().with_dir("a");
    path.extend(["b", "c", "d"]);

    assert_eq!(
        path.get_dirs(),
        &vec![
            OsString::from("a"),
            OsString::from("b"),
            OsString::from("c"),
            OsString::from("d")
        ]
    );
}

#[test]
fn test_from_string_and_str() {
    let p1 = PavedPath::from("/tmp/test.txt".to_string());
    let p2 = PavedPath::from_str("/tmp/test.txt").unwrap();

    let unix1 = p1.path_in_style(PlatformStyle::Unix);
    let unix2 = p2.path_in_style(PlatformStyle::Unix);

    assert_eq!(unix1, unix2);
    assert_eq!(p1.get_file(), Some(&OsString::from("test.txt")));
}

#[test]
fn test_into_iterator_combines_dirs_and_file() {
    let path = PavedPath::new_relative()
        .with_dir("foo")
        .with_dir("bar")
        .with_file("baz.txt");

    let parts: Vec<_> = path.into_iter().collect();
    assert_eq!(
        parts,
        vec![
            OsString::from("foo"),
            OsString::from("bar"),
            OsString::from("baz.txt")
        ]
    );
}

#[test]
fn absolutize_combines_root_and_relative() {
    let root = PavedPath::from("/usr/local/");
    let rel = PavedPath::from("bin/test");

    let result = rel.absolutize(&root).expect("should succeed");

    assert!(result.is_absolute());
    assert_eq!(
        result.build_unix_pathbuf(),
        Path::new("/usr/local/bin/test")
    );
    assert_eq!(
        result.build_windows_pathbuf(),
        Path::new(r"\usr\local\bin\test")
    );
}

#[test]
fn absolutize_fails_if_self_is_already_absolute() {
    let root = PavedPath::from("/usr");
    let already_abs = PavedPath::from("/bin");

    let err = already_abs.absolutize(&root).expect_err("should fail");
    assert_eq!(err, PavedAbsolutizeError::PathIsAbsolute);
}

#[test]
fn absolutize_fails_if_root_is_relative() {
    let root = PavedPath::from("relative/root");
    let rel = PavedPath::from("child/path");

    let err = rel.absolutize(&root).expect_err("should fail");
    assert_eq!(err, PavedAbsolutizeError::RootIsRelative);
}

#[test]
fn absolutize_preserves_windows_drive_prefix() {
    let root = PavedPath::new_absolute()
        .with_prefix(PathPrefix::Drive('C'))
        .with_dir("Program Files")
        .with_dir("MyApp");

    let rel = PavedPath::from("data/config.ini");
    let result = rel.absolutize(&root).expect("should succeed");

    assert!(result.is_absolute());
    assert_eq!(
        result.build_windows_pathbuf(),
        Path::new("C:\\Program Files\\MyApp\\data\\config.ini")
    );
}

#[test]
fn test_set_dirs() {
    let mut paved = PavedPath::from("/a/b/c/");
    paved.set_dirs(["d", "e"]);

    assert_eq!(paved.build_unix_pathbuf(), Path::new("/d/e/"));
}