camino_anchored/lib.rs
1// Copyright (c) The camino-anchored Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! UTF-8 paths with explicit resolution and base-relative display.
5//!
6//! # Motivation
7//!
8//! Command-line tools often want to accept and display paths in a principled
9//! manner. In general:
10//!
11//! * Relative paths provided over the command line should be resolved against
12//! the current working directory.
13//! * Internally, one might wish to always use absolute paths.
14//! * When displaying paths, one might wish to preserve the spelling of
15//! relative paths the user provided, display paths within a chosen base
16//! directory relative to it, and fall back to absolute paths otherwise.
17//!
18//! This crate provides a set of helper types to aid in handling paths
19//! correctly.
20//!
21//! ## Relative and absolute paths
22//!
23//! To represent paths, this crate provides two newtype wrappers around
24//! [`Utf8PathBuf`]:
25//!
26//! * [`AbsUtf8PathBuf`] represents an absolute path such as
27//! `/home/user` or `C:\Users\user`.
28//! * [`RelUtf8PathBuf`] represents a relative path such as
29//! `foo/bar` or `../foo`.
30//!
31//! (On Windows, there are paths that are neither entirely absolute nor entirely
32//! relative, such as `C:foo` and `\Users\user`. These are accepted by neither
33//! [`AbsUtf8PathBuf`] nor [`RelUtf8PathBuf`], but can be processed via
34//! [`PathAnchor::resolve_input`].)
35//!
36//! ## Path resolution
37//!
38//! For resolving and displaying paths, this crate introduces
39//! [`PathAnchor`]. Some examples of path anchors a project
40//! might use are: the cwd, a workspace root, or a repository root.
41//!
42//! A [`PathAnchor`] can be used to turn a path into an [`AnchoredPath`].
43//! An [`AnchoredPath`] carries the path in both absolute and relative
44//! forms, if available, and has a [display helper][AnchoredPath::display] to
45//! format the path for display.
46//!
47//! # Examples
48//!
49//! Let's say you have the following directory structure:
50//!
51//! ```text
52//! project/ <- workspace root
53//! ├── .config/
54//! │ └── myproject.toml
55//! └── crates/
56//! ├── custom.toml
57//! └── widget/ <- invocation directory
58//! ```
59//!
60//! Let's say the user cds to `project/crates/widget` (here, called the
61//! _invocation directory_) and invokes your tool with `--config-file
62//! ../custom.toml`.
63//!
64//! * The `../custom.toml` on the command line should be resolved relative
65//! to the invocation directory.
66//! * The default config file should be resolved relative to the workspace root.
67//! * The explicit input should retain its `../custom.toml` spelling for display.
68//! * The discovered config should be displayed as an absolute path. One might
69//! imagine synthesizing the right number of `..` when possible, but the
70//! presence of symlinks makes that ambiguous.
71//!
72//! ```
73//! use camino_anchored::{AbsUtf8PathBuf, PathAnchor, RelUtf8PathBuf};
74//! use std::fs;
75//!
76//! // Set up the directory layout mentioned above.
77//! let temp_dir = camino_tempfile::tempdir()?;
78//! let workspace_dir = temp_dir.path().join("project");
79//! let invocation_dir = workspace_dir.join("crates/widget");
80//! fs::create_dir_all(workspace_dir.join(".config"))?;
81//! fs::create_dir_all(&invocation_dir)?;
82//! fs::write(
83//! workspace_dir.join(".config/myproject.toml"),
84//! "source = 'repository'",
85//! )?;
86//! fs::write(
87//! workspace_dir.join("crates/custom.toml"),
88//! "source = 'explicit'",
89//! )?;
90//!
91//! // Create PathAnchor instances for the workspace and invocation
92//! // directories.
93//! let workspace_base = PathAnchor::new(AbsUtf8PathBuf::new(&workspace_dir)?);
94//! let invocation_base = PathAnchor::new(AbsUtf8PathBuf::new(&invocation_dir)?);
95//!
96//! // Turn the command line input into an AnchoredPath by resolving it against the
97//! // invocation directory.
98//! let explicit_config = invocation_base.resolve_input("../custom.toml")?;
99//! assert_eq!(
100//! explicit_config.absolute().as_path(),
101//! invocation_dir.join("../custom.toml"),
102//! );
103//! // `explicit_config.display()` preserves the user's `..`, since that was
104//! // provided as input.
105//! assert_eq!(explicit_config.display().to_string(), "../custom.toml");
106//!
107//! // Locate the default config, which is relative to the workspace root.
108//! let default_config_relative = RelUtf8PathBuf::new(".config/myproject.toml")?;
109//! let default_config_absolute = workspace_base.directory().join(&default_config_relative);
110//!
111//! // Turn the default config path into an AnchoredPath against the invocation
112//! // directory.
113//! let default_config = invocation_base.resolve_absolute(default_config_absolute);
114//! assert_eq!(
115//! default_config.absolute().as_path(),
116//! workspace_dir.join(".config/myproject.toml"),
117//! );
118//!
119//! // The default config's path doesn't start with the invocation directory's
120//! // path, so we display it as an absolute path. (See
121//! // `AbsUtf8PathBuf::strip_prefix` for the exact rules.)
122//! assert_eq!(
123//! default_config.display().to_string(),
124//! default_config.absolute().as_path().as_str(),
125//! );
126//!
127//! for config_path in [&explicit_config, &default_config] {
128//! // To access a file using its absolute path, use `AnchoredPath::absolute`.
129//! let contents = fs::read_to_string(config_path.absolute())?;
130//! // To display a path, use `AnchoredPath::display`.
131//! println!("read {}: {contents}", config_path.display());
132//! }
133//!
134//! // Paths mentioned inside a config file are typically relative to the file's
135//! // own directory. Use `AbsUtf8PathBuf::parent` to derive that anchor.
136//! let config_dir = explicit_config
137//! .absolute()
138//! .parent()
139//! .expect("config file has a parent directory");
140//! let config_base = PathAnchor::new(config_dir);
141//! let data_file = config_base.resolve_relative(RelUtf8PathBuf::new("data/input.txt")?);
142//! assert_eq!(
143//! data_file.absolute().as_path(),
144//! invocation_dir.join("../data/input.txt"),
145//! );
146//! # Ok::<(), Box<dyn std::error::Error>>(())
147//! ```
148//!
149//! # Minimum supported Rust version (MSRV)
150//!
151//! This crate's MSRV is **Rust 1.86**. In general we aim for 6 months of Rust
152//! compatibility.
153//!
154//! [`Utf8PathBuf`]: camino::Utf8PathBuf
155
156mod errors;
157mod paths;
158mod resolution;
159#[cfg(test)]
160mod test_helpers;
161
162pub use errors::{
163 AbsUtf8PathError, AbsUtf8PathErrorKind, CurrentDirError, NativePathErrorKind, RelUtf8PathError,
164 RelUtf8PathErrorKind, ResolvePathError, ResolvePathErrorKind, TryFromPathBufError,
165};
166pub use paths::{AbsUtf8PathBuf, RelUtf8PathBuf};
167pub use resolution::{AnchoredPath, DisplayPath, PathAnchor};