Skip to main content

doorstop_rs/doorstop/
document_tree.rs

1// doorstop-rs: Help library to read doorstop documents implemented in Rust.
2// Copyright (C) <2024>  INVAP S.E.
3//
4// This file is part of doorstop-rs.
5//
6// doorstop-rs is free software: you can redistribute it and/or modify
7// it under the terms of the GNU Affero General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10//
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU Affero General Public License for more details.
15//
16// You should have received a copy of the GNU Affero General Public License
17// along with this program.  If not, see <https://www.gnu.org/licenses/>.
18
19use crate::doorstop::document::Document;
20use crate::doorstop::file;
21use thiserror::Error;
22use walkdir::WalkDir;
23
24use std::{cell::RefCell, collections::HashMap, error::Error, path::PathBuf, rc::Rc};
25
26#[derive(Debug)]
27pub struct DocumentTree {
28    pub document: Rc<Document>,
29    pub children: Vec<Rc<RefCell<DocumentTree>>>,
30    ///Complete document tree index, k= document prefix, v= document tree
31    pub prefix_index: Rc<RefCell<HashMap<String, Rc<RefCell<DocumentTree>>>>>,
32}
33
34#[derive(Error, Debug)]
35#[error("{msg}")]
36pub struct LoadError {
37    msg: String,
38}
39
40impl DocumentTree {
41    fn add_child(&mut self, child: Rc<RefCell<DocumentTree>>) {
42        self.children.push(child);
43    }
44
45    pub fn load(path: &str) -> Result<Rc<RefCell<DocumentTree>>, Box<dyn Error>> {
46        let ymls = find_document_yml(path);
47        if ymls.len() == 0 {
48            let s = "No documents found in ".to_string() + path;
49            return Err(Box::new(LoadError { msg: s }));
50        }
51        build_doc_tree(ymls)
52    }
53}
54
55fn build_doctree_index(
56    paths: Vec<PathBuf>,
57) -> Result<Rc<RefCell<HashMap<String, Rc<RefCell<DocumentTree>>>>>, Box<dyn Error>> {
58    let prefix_doc_map: Rc<RefCell<HashMap<String, Rc<RefCell<DocumentTree>>>>> =
59        Rc::new(RefCell::new(HashMap::new()));
60
61    for each_path in paths {
62        let doc = Document::new(each_path)?;
63
64        let each_tree = DocumentTree {
65            document: Rc::new(doc),
66            children: Vec::new(),
67            prefix_index: Rc::clone(&prefix_doc_map),
68        };
69
70        prefix_doc_map.borrow_mut().insert(
71            each_tree.document.config.settings.prefix.clone(),
72            Rc::new(RefCell::new(each_tree)),
73        );
74    }
75
76    Ok(prefix_doc_map)
77}
78
79fn build_doc_tree(paths: Vec<PathBuf>) -> Result<Rc<RefCell<DocumentTree>>, Box<dyn Error>> {
80    let mut root: Option<Rc<RefCell<DocumentTree>>> = None;
81    let prefix_doc_map = build_doctree_index(paths)?;
82
83    //Go over all the documents if no parent is the root
84    for (_, value) in prefix_doc_map.borrow().iter() {
85        let doc_tree = (**value).borrow();
86        let parent_prefix = &doc_tree.document.config.settings.parent;
87
88        match parent_prefix {
89            Some(p) => {
90                let h = prefix_doc_map.borrow();
91                let parent = h.get(p).unwrap();
92                (**parent).borrow_mut().add_child(value.clone());
93            }
94            None => {
95                root = Some(value.clone());
96            }
97        }
98    }
99    Ok(root.unwrap())
100}
101
102pub fn find_document_yml(path: &str) -> Vec<PathBuf> {
103    let into_iter = WalkDir::new(path).into_iter();
104
105    let only_doorstop_yml = into_iter.filter_entry(|e| !file::is_hidden_dir(e));
106
107    let iterator = only_doorstop_yml.filter(|e| e.as_ref().is_ok_and(file::is_doorstop_config));
108
109    let res: Vec<PathBuf> = iterator
110        .filter_map(|e| e.ok())
111        .filter_map(|e| Some(e.into_path()))
112        .collect();
113    res
114}
115
116#[cfg(test)]
117mod tests {
118
119    use crate::doorstop::document_tree::DocumentTree;
120
121    use super::find_document_yml;
122
123    #[test]
124    fn test_find_document_yml() {
125        let ymls = find_document_yml("resources/reqs");
126        assert_eq!(3, ymls.len());
127    }
128
129    #[test]
130    fn test_build_tree() {
131        let document_tree = DocumentTree::load("resources/reqs").unwrap();
132        let document_tree = document_tree.borrow();
133
134        assert_eq!(
135            String::from("REQ"),
136            document_tree.document.config.settings.prefix,
137            "testing REQ is the root prefix"
138        );
139
140        assert_eq!(2, document_tree.children.len(), "Wrong amount of children");
141    }
142
143    #[test]
144    fn get_document_by_index() {
145        let document_tree = DocumentTree::load("resources/reqs").unwrap();
146        let document_tree = document_tree.borrow();
147
148        let h = document_tree.prefix_index.borrow();
149        let doc = h.get("TUT");
150        assert_eq!(
151            String::from("TUT"),
152            doc.unwrap().borrow().document.config.settings.prefix,
153            "testing REQ is the root prefix"
154        );
155
156    }
157
158    #[test]
159    fn test_err_build_tree() {
160        let document_tree = DocumentTree::load("nonexistent-resources/reqs");
161        assert!(document_tree.is_err());
162    }
163}