Skip to main content

arui_core/tree/
root.rs

1//! # 项目树入口
2//! 用于初始化操作和启动目录树分析
3use crate::tree::config::ProjectConfig;
4use crate::tree::node::TreeNode;
5use crate::utils::{check_path, generate_id};
6use std::fs;
7use std::io::Result;
8use std::path::{Path, PathBuf};
9
10/// 项目目录树根节点
11/// 用于初始化操作和启动目录树分析
12pub struct ProjectTree {
13    /// 项目 ID,自动生成
14    pub id: String,
15    /// 项目别名,自定义
16    pub name: String,
17    /// 项目根路径,自定义
18    pub path: String,
19    /// 根节点,在 `build` 方法中生成,作为整个项目树迭代入口
20    pub root: Option<TreeNode>,
21    /// 项目树迭代配置,用于过滤、仅包含等等
22    pub config: Option<ProjectConfig>,
23}
24
25/// 初始化项目及构建属性
26impl ProjectTree {
27    /// 通过一系列参数初始化项目树
28    /// - name:项目别名
29    /// - path:根路径
30    /// - config:配置
31    ///
32    /// 但是需要注意,该操作仅初始化一个基础的项目树对象,可以通过该对象启动项目树的构建和分析:
33    /// - build: 构建项目树,生成各个节点,但不包含总结信息
34    /// - summarize: 遍历已有项目树,获取各个节点的总结信息
35    ///
36    /// # Example
37    ///
38    /// ```rust
39    /// use arui_core::tree::root::ProjectTree;
40    /// const NAME: &str = "test";
41    /// const PATH: &str = ".";
42    /// // 需要使用可变类型来初始化
43    /// let mut project = ProjectTree::new(NAME, PATH, None);
44    /// // 需要通过 build 和 summary 方法来启动项目树的构建和分析
45    /// project.build().unwrap();
46    /// project.summarize().unwrap();
47    /// ```
48    pub fn new<S, I>(name: S, path: I, config: Option<ProjectConfig>) -> Self
49    where
50        S: Into<String>,
51        I: Into<String>,
52    {
53        ProjectTree {
54            id: generate_id(),
55            name: name.into(),
56            path: path.into(),
57            root: None,
58            config,
59        }
60    }
61
62    /// 动态检查节点路径是否合法
63    ///
64    /// # Example
65    ///
66    /// ```rust
67    /// use arui_core::tree::root::ProjectTree;
68    /// // 只有在路径合法的情况下,才能进行项目树构建,但可正常初始化项目
69    /// let can_build = ProjectTree::new("test", "/@not_exist", None).is_valid();
70    /// println!("{}", can_build); // should be false
71    /// ```
72    pub fn is_valid(&self) -> bool {
73        check_path(&self.path).is_ok()
74    }
75
76    /// "种植"一棵项目树
77    /// - name:项目别名
78    /// - path:根路径
79    /// - config:配置
80    ///
81    /// 该操作和 `new` 的不同的地方在于,`new` 方法仅初始化项目树入口,并不完成后续操作:
82    /// - build: 从 `path` 启动,遍历并生成项目树
83    /// - summary: 从 `root` 启动,遍历并生成项目树各节点的总结信息
84    ///
85    /// # Example
86    ///
87    /// ```rust
88    /// use arui_core::tree::root::ProjectTree;
89    /// const NAME: &str = "test";
90    /// const PATH: &str = ".";
91    /// // 如果只用 `plant`,后续也不需要更新项目树,则可不使用 `mut`
92    /// let project = ProjectTree::plant(NAME, PATH, None);
93    /// ```
94    pub fn plant<S, I>(name: S, path: I, config: Option<ProjectConfig>) -> Self
95    where
96        S: Into<String>,
97        I: Into<String>,
98    {
99        let mut tree = ProjectTree::new(name, path, config);
100        tree.build().expect("project build panic");
101        tree.summarize().expect("project summarize panic");
102        tree
103    }
104
105    // ------------------------- 遍历构建节点 -------------------------
106
107    /// 构建项目文件树(不包含summary信息)
108    /// 通过 `path` 启动,遍历并生成项目树,但仅初始化各级树结构:
109    /// - path:节点对应文件/目录的路径
110    /// - is_dir:是否是文件夹
111    /// - children:子节点(is_dir为true时有值)
112    /// 其中并不包含 `summary` 字段的获取,需要单独调用 `summarize` 方法来获取
113    ///
114    /// # Example
115    ///
116    /// ```rust
117    /// use arui_core::tree::root::ProjectTree;
118    /// let mut project = ProjectTree::new("test", "./src", None);
119    /// project.build().unwrap();
120    /// ```
121    pub fn build(&mut self) -> Result<()> {
122        // 如果路径不合法,返回错误
123        // TODO: 修改为自定义错误
124        if !self.is_valid() {
125            return Err(std::io::Error::new(
126                std::io::ErrorKind::InvalidInput,
127                "invalid path",
128            ));
129        }
130        let root_path = PathBuf::from(&self.path);
131        // 尝试遍历构建项目树,生成各个节点
132        self.root = Some(Self::build_tree_node(&root_path)?);
133        Ok(())
134    }
135
136    /// 递归构建树节点
137    fn build_tree_node(path: &Path) -> Result<TreeNode> {
138        // 获取文件元数据
139        // TODO: 自定义错误处理
140        let metadata = fs::metadata(path)?;
141        let is_dir = metadata.is_dir();
142        // 创建节点
143        let mut node = TreeNode::new(path.to_string_lossy().into_owned(), is_dir);
144        // 如果是目录,递归构建该节点的子节点
145        // TODO:自定义错误
146        if is_dir {
147            let mut children = Vec::new();
148            for entry in fs::read_dir(path)? {
149                let entry = entry?;
150                children.push(Self::build_tree_node(&entry.path())?);
151            }
152            node.children = Some(children);
153        }
154
155        Ok(node)
156    }
157
158    // ------------------------- 生成总结信息 -------------------------
159
160    /// 生成项目树的总结信息
161    /// 从 `root` 启动,遍历并生成项目树各节点的总结信息
162    ///
163    /// # Example
164    ///
165    /// ```rust
166    /// use arui_core::tree::root::ProjectTree;
167    /// let mut project = ProjectTree::new("test", "./src", None);
168    /// project.build().unwrap();
169    /// project.summarize().unwrap();
170    /// println!("{}", project.root.as_ref().unwrap());
171    /// ```
172    pub fn summarize(&mut self) -> Result<()> {
173        // 如果根节点不存在,返回错误
174        if self.root.is_none() {
175            return Err(std::io::Error::new(
176                std::io::ErrorKind::InvalidInput,
177                "root is none, please build by `build()` first",
178            ));
179        }
180        // 如果根路径不合法,返回错误
181        if !self.is_valid() {
182            return Err(std::io::Error::new(
183                std::io::ErrorKind::InvalidInput,
184                "invalid root path, please check it",
185            ));
186        }
187        // 递归获取总结信息
188        self.root.as_mut().unwrap().upsert_summary();
189        Ok(())
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::tree::visible::ProjectTreeVisible;
197
198    #[test]
199    fn test_new() {
200        let name = "test";
201        let path = ".".to_string();
202        let tree = ProjectTree::new(name, path, None);
203        assert_eq!(tree.id.len(), 36);
204        assert_eq!(tree.name, name.to_string());
205        assert_eq!(tree.path, ".".to_string());
206    }
207
208    #[test]
209    fn test_get_valid() {
210        let valid_tree = ProjectTree::new("test".to_string(), "./src".to_string(), None);
211        assert_eq!(valid_tree.is_valid(), true);
212        let invalid_tree = ProjectTree::new("test".to_string(), "/not_exist".to_string(), None);
213        assert_eq!(invalid_tree.is_valid(), false);
214    }
215
216    #[test]
217    fn test_build_project_tree() {
218        let name = "test";
219        let path = "./src";
220        let mut tree = ProjectTree::new(name, path, None);
221        tree.build().expect("panic");
222        assert_eq!(tree.root.is_some(), true);
223        assert_eq!(tree.name, "test".to_string());
224        assert_eq!(tree.path, "./src".to_string());
225        // 打印一下看看结构是否正确
226        tree.print_tree();
227    }
228
229    #[test]
230    fn test_plant() {
231        let name = "test";
232        let path = "./src";
233        let tree = ProjectTree::plant(name, path, None);
234        assert_eq!(tree.root.is_some(), true);
235        assert_eq!(tree.name, "test".to_string());
236        assert_eq!(tree.path, "./src".to_string());
237        // 打印一下看看结构是否正确
238        tree.print_tree();
239        println!("{}", tree.root.unwrap());
240    }
241}