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}