Skip to content

A Rust utility DSL and macro to help construct and modify Paths.

License

Notifications You must be signed in to change notification settings

cwfitzgerald/path-dsl-rs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

64 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

path-dsl

Latest version Documentation License

Utility DSL and macro to help deal with Paths.

PathDSL provides a simple and zero-overhead abstraction for creating paths and appending to existing Path-like things.

Overview

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

// PathBuf::push() only called once with consecutive literals:
let literals: PathBuf = path!("dir1" | "dir2" | "dir3");
// Type annotation for illustration purposes; not needed

// Does not copy data if first path segment is a owning value:
let moving = path!(literals | "dir4");

// Mixing and matching is easy:
let start = path!("some" | "dir");
let end = path!("my_folder" | "my_file.txt");
// Can borrow as normal
let result = path!(start | "middle_folder" | &end);

// Works with PathBuf, Path, and String-likes
let file = Path::new("file.txt");
let folder = PathBuf::from("folder");
let middle: &str = "other_middle";
let combined = path!(folder | middle | "middle_folder" | file);

PathDSL Macro and Type

PathDSL's path! macro allows for the creation of a PathBuf in the most efficent way possible in the situation.

note the use of | instead of / due to rust's macro rules

use path_dsl::path;
// Type annotation for illustration only, not needed
let path: PathBuf = path!("dir1" | "dir2" | "dir3" | "file.txt");

PathDSL

You can also generate a PathDSL directly, though this is discouraged. PathDSL will pretend to be a PathBuf as best it can, but it is almost always more efficent to use the path! macro to generate a PathBuf directly.

use path_dsl::PathDSL;
let path = PathDSL::from("dir1") / "dir2" / "dir3" / "file.txt";

Adding Path-Like Structures

As well as using regular string literals, you can use anything that can be passed to PathBuf::push as a part of the DSL.

Note the borrow on other: as these types are not Copy, they will be moved into the path unless you borrow them. This matches behavior with PathBuf::push, but can be surprising when used in a infix expression.

use path_dsl::{path, PathDSL};

let other = PathBuf::from("some_dir");
let filename: &str = "my_file.txt";

let mac: PathBuf  = path!("dir1" | "dir2" | &other | filename); // Preferred
let path: PathDSL = PathDSL::from("dir1") / "dir2" / other / filename; // Also works

Moving vs Borrowing

Both the macro and the DSL type behave the same with regard to borrowing vs moving. If a reference is provided, it will borrow the provided value. However, if a value is provided it will move it, making the value unusable afterwards. While these are the normal rules for rust, infix operators are normally used with Copy types, so this may be surprising.

Both mutable and immutable borrows are supported, though they will never actually mutate anything.

use path_dsl::path;
# use std::path::PathBuf;

let value = PathBuf::from("some_dir");
let borrow: &str = "my_file.txt";

let mac  = path!(value | borrow);
let path = path!(value | borrow); // Will not compile because `value` was moved

You must manually borrow it:

let mac  = path!(&value | borrow); // Borrow value so it can be used later
let path = PathDSL::new() / value / borrow; // Not used afterwards, so doesn't need a borrow

PathDSL <=> PathBuf

PathDSL is designed to be a drop-in replacement for PathBuf, including trivial conversions between the two. In any situation where you would be able to use PathBuf you can use PathDSL. PathDSL includes an implementation of Deref to a PathBuf (and by proxy Path) and re-implements all functions that take self, so is fully api compatable. However there are some situations where you must have a PathBuf. Obtaining a &PathBuf is trivial through dereferencing and obtaining a PathBuf is possible through the PathDSL::into_pathbuf function.

PathDSL is #[repr(transparent)] over PathBuf and all functions are force-inlined so conversions and operations should be cost-free compared to the equivalent PathBuf operation. If they aren't, please file a bug.

Some known issues are:

Equality

use path_dsl::path;

let dsl = path!("file.txt");
let buf = PathBuf::from("file.txt");

assert!(dsl == buf);
// Must de-reference to PathBuf can't implement `Eq` for `PathBuf`
assert!(buf == *dsl);

Function Calls

use path_dsl::path;

fn func(p: PathBuf) {
}

let dsl = path!("file.txt");
let buf = PathBuf::from("file.txt");

func(buf);
// Must convert into `PathBuf`
// Dereferencing doesn't work because `func` moves.
func(dsl.to_path_buf());
func(dsl.into()) // also works

Macro Optimizations

As previously mentioned, the macro contains some optimizations over using raw PathDSL and should always be used over manually using PathDSL. These optimizations happen at compile time, and are guaranteed. Further details on these can be found on the path! macro documentation.

String Literal Concatenation:

While it is ill-advised to use string literals with slashes in a Path, The path! macro takes slashes into account, and automatically constructs a single string literal from multiple consecutive string literals. This can potentially save an allocation or two in the underlying OsString.

use path_dsl::path;

let p = path!("this" | "is" | "combined");
if cfg!(windows) {
    assert_eq!(p, PathBuf::from("this\\is\\combined"));
} else {
    assert_eq!(p, PathBuf::from("this/is/combined"));
}

First-Argument Optimization:

When the very first argument of the path! macro is a owning PathBuf, OsString or PathDSL passed by value (moved), instead of copying everything into a new PathDSL, it will just steal the buffer from that moved-in value. This allows you to use the path! macro fearlessly when appending to already existing variables.

use path_dsl::path;

let first = PathBuf::from("a_very_long_folder_name");
let p = path!(first); // Does not copy anything.

Why Use A Crate?

You may be wondering why you should use a crate for this when you can easily wrap PathBuf and add some Div implementations. This is basically what I thought as well until I actually went to go implement this crate. There is a surprising amount of very tedious and particular code to try to emulate PathBuf directly, as well as to test the functionality.

With this in mind, I have made path_dsl completely dependency free, choosing to lean on declarative macros over proc macros as to not depend on things like syn. Additionally, everything is contained within this one file, I have thorough tests, and I have added #[deny(unsafe_code)] for good measure. Hopefully this makes this crate light enough and easily-auditable enough to be an acceptable dependency.

License: MIT