plantuml_parser/path.rs
1use crate::Error;
2use normalize_path::NormalizePath;
3use std::path::PathBuf;
4
5/// A resolver that keeps relative paths on a stack for recursive include process.
6///
7/// # Examples
8///
9/// ```
10/// use plantuml_parser::PathResolver;
11/// use std::path::PathBuf;
12///
13/// # fn main() -> anyhow::Result<()> {
14/// let resolver_0 = PathResolver::new("./dir-1/x1.puml");
15/// assert_eq!(resolver_0.build()?, PathBuf::from("dir-1/x1.puml"));
16/// assert_eq!(resolver_0.build_without_normalize()?, PathBuf::from("./dir-1/x1.puml"));
17///
18/// // For example, `./dir-1/x1.puml` includes `./dir-2/x2.puml`.
19/// let mut resolver_0_0 = resolver_0.clone();
20/// resolver_0_0.add("./dir-2/x2.puml".into());
21/// assert_eq!(resolver_0_0.build()?, PathBuf::from("dir-1/dir-2/x2.puml"));
22/// assert_eq!(resolver_0_0.build_without_normalize()?, PathBuf::from("./dir-1/dir-2/x2.puml"));
23///
24/// // For example, `./dir-1/x1.puml` includes `./dir-3/x3.puml`.
25/// let mut resolver_0_1 = resolver_0.clone();
26/// resolver_0_1.add("./dir-3/x3.puml".into());
27/// assert_eq!(resolver_0_1.build()?, PathBuf::from("dir-1/dir-3/x3.puml"));
28/// assert_eq!(resolver_0_1.build_without_normalize()?, PathBuf::from("./dir-1/dir-3/x3.puml"));
29///
30/// // For example, `./dir-1/dir-3/x3.puml` includes `../../dir-4/x4.puml`.
31/// let mut resolver_0_1_0 = resolver_0_1.clone();
32/// resolver_0_1_0.add("../../dir-4/x4.puml".into());
33/// assert_eq!(resolver_0_1_0.build()?, PathBuf::from("dir-4/x4.puml"));
34/// assert_eq!(resolver_0_1_0.build_without_normalize()?, PathBuf::from("./dir-1/dir-3/../../dir-4/x4.puml"));
35/// # Ok(())
36/// # }
37/// ```
38#[derive(Clone, Debug)]
39pub struct PathResolver {
40 paths: Vec<PathBuf>,
41}
42
43/// The error type for [`PathResolver`]'s operations.
44#[derive(thiserror::Error, Debug)]
45pub enum PathResolverError {
46 #[error("PathResolver has no paths (unreachable)")]
47 None,
48}
49
50impl PathResolver {
51 /// Creates a new [`PathResolver`].
52 ///
53 /// * `path` - A base path.
54 ///
55 /// # Examples
56 ///
57 /// ```
58 /// # use plantuml_parser::PathResolver;
59 /// # fn main() {
60 /// let _ = PathResolver::new("path");
61 /// # }
62 /// ```
63 pub fn new<T>(path: T) -> Self
64 where
65 T: Into<PathBuf>,
66 {
67 Self {
68 paths: vec![path.into()],
69 }
70 }
71
72 /// Adds a [`PathBuf`] onto an inner stack.
73 ///
74 /// * `path` - A new path.
75 ///
76 /// # Examples
77 ///
78 /// ```
79 /// # use plantuml_parser::PathResolver;
80 /// # use std::path::PathBuf;
81 /// #
82 /// # fn main() -> anyhow::Result<()> {
83 /// let mut resolver = PathResolver::new("dir/file");
84 /// resolver.add("file-2".into());
85 /// assert_eq!(resolver.build()?, PathBuf::from("dir/file-2"));
86 /// # Ok(())
87 /// # }
88 /// ```
89 pub fn add(&mut self, path: PathBuf) {
90 self.paths.push(path);
91 }
92
93 /// Returns the resolved normalized path. All `PathBuf`s in the stack are treated as paths relative to the file.
94 ///
95 /// # Examples
96 ///
97 /// ```
98 /// # use plantuml_parser::PathResolver;
99 /// # use std::path::PathBuf;
100 /// #
101 /// # fn main() -> anyhow::Result<()> {
102 /// let mut resolver = PathResolver::new("dir-1/file-1");
103 /// assert_eq!(resolver.build()?, PathBuf::from("dir-1/file-1"));
104 /// resolver.add("../dir-2/file-2".into());
105 /// assert_eq!(resolver.build()?, PathBuf::from("dir-2/file-2"));
106 /// resolver.add("../dir-3/file-3".into());
107 /// assert_eq!(resolver.build()?, PathBuf::from("dir-3/file-3"));
108 /// # Ok(())
109 /// # }
110 /// ```
111 pub fn build(&self) -> Result<PathBuf, Error> {
112 let ret = self.build_without_normalize()?.normalize();
113 Ok(ret)
114 }
115
116 /// Returns the resolved the path not normalized.
117 ///
118 /// # Examples
119 ///
120 /// ```
121 /// # use plantuml_parser::PathResolver;
122 /// # use std::path::PathBuf;
123 /// #
124 /// # fn main() -> anyhow::Result<()> {
125 /// let mut resolver = PathResolver::new("dir-1/file-1");
126 /// assert_eq!(
127 /// resolver.build_without_normalize()?,
128 /// PathBuf::from("dir-1/file-1")
129 /// );
130 ///
131 /// resolver.add("../dir-2/file-2".into());
132 /// assert_eq!(
133 /// resolver.build_without_normalize()?,
134 /// PathBuf::from("dir-1/../dir-2/file-2")
135 /// );
136 ///
137 /// resolver.add("../dir-3/file-3".into());
138 /// assert_eq!(
139 /// resolver.build_without_normalize()?,
140 /// PathBuf::from("dir-1/../dir-2/../dir-3/file-3")
141 /// );
142 /// # Ok(())
143 /// # }
144 /// ```
145 pub fn build_without_normalize(&self) -> Result<PathBuf, Error> {
146 let mut iter = self.paths.iter();
147
148 let mut last_file_name;
149 let mut ret = iter.next().ok_or(PathResolverError::None)?.clone();
150 last_file_name = ret.file_name().map(|x| x.to_os_string());
151 ret.pop();
152
153 for path in iter {
154 let mut path = path.clone();
155 last_file_name = path.file_name().map(|x| x.to_os_string());
156 path.pop();
157 ret.push(path);
158 }
159
160 if let Some(last_file_name) = last_file_name {
161 ret.push(last_file_name);
162 }
163
164 Ok(ret)
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_path_resolver() -> anyhow::Result<()> {
174 let path_base = PathResolver::new("./base_dir/base_file.puml");
175 let mut path1 = path_base.clone();
176 path1.add("./dir1-1/file1-1.puml".into());
177 println!("path1 = {path1:?}, {:?}", path1.build()?);
178 assert_eq!(
179 path1.build()?,
180 PathBuf::from("base_dir/dir1-1/file1-1.puml")
181 );
182
183 let mut path2 = path1.clone();
184 path1.add("./dir2-1/file2-1.puml".into());
185 println!("path1 = {path1:?}, {:?}", path1.build()?);
186 assert_eq!(
187 path1.build()?,
188 PathBuf::from("base_dir/dir1-1/dir2-1/file2-1.puml")
189 );
190 path2.add("./dir1-1/file1-1.puml".into());
191 println!("path2 = {path2:?}, {:?}", path2.build()?);
192 assert_eq!(
193 path2.build()?,
194 PathBuf::from("base_dir/dir1-1/dir1-1/file1-1.puml")
195 );
196
197 let mut path1 = path_base.clone();
198 path1.add("./dir1-2/file1-2.puml".into());
199 println!("path1 = {path1:?}, {:?}", path1.build()?);
200 assert_eq!(
201 path1.build()?,
202 PathBuf::from("base_dir/dir1-2/file1-2.puml")
203 );
204
205 let mut path2 = path1.clone();
206 path1.add("./dir2-2/file2-2.puml".into());
207 println!("path1 = {path1:?}, {:?}", path1.build()?);
208 assert_eq!(
209 path1.build()?,
210 PathBuf::from("base_dir/dir1-2/dir2-2/file2-2.puml")
211 );
212 path2.add("./dir1-2/file1-2.puml".into());
213 println!("path2 = {path2:?}, {:?}", path2.build()?);
214 assert_eq!(
215 path2.build()?,
216 PathBuf::from("base_dir/dir1-2/dir1-2/file1-2.puml")
217 );
218
219 Ok(())
220 }
221
222 #[test]
223 fn test_absolute_path_resolver() -> anyhow::Result<()> {
224 let path_base = PathResolver::new("/base_dir/base_file.puml");
225 let mut path = path_base.clone();
226 path.add("./dir1-1/file1-1.puml".into());
227 println!("path = {path:?}, {:?}", path.build()?);
228 assert_eq!(
229 path.build()?,
230 PathBuf::from("/base_dir/dir1-1/file1-1.puml")
231 );
232
233 let mut path = path_base.clone();
234 path.add("/dir1-1/file1-1.puml".into());
235 println!("path = {path:?}, {:?}", path.build()?);
236 assert_eq!(path.build()?, PathBuf::from("/dir1-1/file1-1.puml"));
237
238 Ok(())
239 }
240}