rust_config_tree/path.rs
1//! Lexical path normalization and include path resolution.
2//!
3//! These helpers normalize paths without consulting the file system. They are
4//! used by both tree loading and template target collection.
5
6use std::path::{Component, Path, PathBuf};
7
8use crate::{ConfigTreeError, Result};
9
10/// Converts a path to an absolute path and normalizes it lexically.
11///
12/// The path does not need to exist. `.` and `..` components are simplified
13/// without resolving symbolic links.
14///
15/// # Arguments
16///
17/// - `path`: Path to convert to an absolute normalized path.
18///
19/// # Returns
20///
21/// Returns the normalized absolute path.
22///
23/// # Examples
24///
25/// ```
26/// use std::path::Path;
27/// use rust_config_tree::absolutize_lexical;
28///
29/// let path = absolutize_lexical("config/../config.yaml")?;
30///
31/// assert!(path.is_absolute());
32/// assert!(path.ends_with(Path::new("config.yaml")));
33/// # Ok::<(), rust_config_tree::ConfigTreeError>(())
34/// ```
35pub fn absolutize_lexical(path: impl AsRef<Path>) -> Result<PathBuf> {
36 let path = path.as_ref();
37 let path = if path.is_absolute() {
38 path.to_path_buf()
39 } else {
40 std::env::current_dir()
41 .map_err(|source| ConfigTreeError::CurrentDir { source })?
42 .join(path)
43 };
44
45 Ok(normalize_lexical(path))
46}
47
48/// Resolves an include path relative to the file that declared it.
49///
50/// Absolute include paths are only normalized. Relative include paths are joined
51/// to the parent directory of `parent_path` and then normalized.
52///
53/// # Arguments
54///
55/// - `parent_path`: Path of the config file that declared the include.
56/// - `include_path`: Include path declared by `parent_path`.
57///
58/// # Returns
59///
60/// Returns the normalized resolved include path.
61///
62/// # Examples
63///
64/// ```
65/// use std::path::PathBuf;
66/// use rust_config_tree::resolve_include_path;
67///
68/// let path = resolve_include_path("/app/config/root.yaml", "child/server.yaml");
69///
70/// assert_eq!(path, PathBuf::from("/app/config/child/server.yaml"));
71/// ```
72pub fn resolve_include_path(
73 parent_path: impl AsRef<Path>,
74 include_path: impl AsRef<Path>,
75) -> PathBuf {
76 let parent_path = parent_path.as_ref();
77 let include_path = include_path.as_ref();
78
79 if include_path.is_absolute() {
80 return normalize_lexical(include_path);
81 }
82
83 let base_dir = parent_path.parent().unwrap_or_else(|| Path::new("."));
84 normalize_lexical(base_dir.join(include_path))
85}
86
87/// Normalizes a path by removing lexical `.` and `..` components.
88///
89/// This function does not touch the file system and does not resolve symbolic
90/// links.
91///
92/// # Arguments
93///
94/// - `path`: Path to normalize.
95///
96/// # Returns
97///
98/// Returns `path` with lexical current-directory and parent-directory
99/// components simplified.
100///
101/// # Examples
102///
103/// ```
104/// use std::path::PathBuf;
105/// use rust_config_tree::normalize_lexical;
106///
107/// let path = normalize_lexical("config/./server/../app.yaml");
108///
109/// assert_eq!(path, PathBuf::from("config/app.yaml"));
110/// ```
111pub fn normalize_lexical(path: impl AsRef<Path>) -> PathBuf {
112 let mut normalized = PathBuf::new();
113
114 for component in path.as_ref().components() {
115 match component {
116 Component::CurDir => {}
117 Component::ParentDir => {
118 normalized.pop();
119 }
120 Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
121 normalized.push(component.as_os_str());
122 }
123 }
124 }
125
126 normalized
127}
128
129#[cfg(test)]
130#[path = "unit_tests/path.rs"]
131mod unit_tests;