Skip to main content

doorstop_rs/doorstop/
document.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::file;
20use serde::{Deserialize, Serialize};
21use std::collections::{BTreeMap, HashMap};
22use std::error::Error;
23use std::path::PathBuf;
24use std::rc::Rc;
25use std::str::FromStr;
26use walkdir::WalkDir;
27
28#[derive(Debug, PartialEq, Serialize, Deserialize)]
29pub struct DocumentSettings {
30    pub digits: i32,
31    pub parent: Option<String>,
32    pub prefix: String,
33    pub sep: String,
34}
35
36#[derive(Debug, PartialEq, Serialize, Deserialize)]
37pub struct Attributes {
38    pub publish: Option<Vec<String>>,
39}
40
41#[derive(Debug, PartialEq, Serialize, Deserialize)]
42pub struct DocumentConfig {
43    pub settings: DocumentSettings,
44    pub attributes: Option<Attributes>,
45}
46
47#[derive(Debug, PartialEq, Serialize, Deserialize)]
48pub struct Item {
49    pub id: Option<String>,
50    pub active: Option<bool>,
51    pub derived: Option<bool>,
52    pub header: Option<String>,
53    pub level: Option<String>,
54    pub normative: Option<bool>,
55    pub reviewed: Option<String>,
56    pub text: Option<String>,
57}
58
59#[derive(Debug, PartialEq)]
60pub enum LevelRelation {
61    SameLevel,
62    OutLevel(i32),
63    InLevel(i32),
64}
65
66impl Item {
67    pub fn level_relation(&self, other: &Self) -> LevelRelation {
68        let mut self_level = self.get_level_key();
69        let mut other_level = other.get_level_key();
70        //remove trailings 0
71        while self_level.last().is_some_and(|x| *x == 0) {
72            self_level.pop();
73        }
74        while other_level.last().is_some_and(|x| *x == 0) {
75            other_level.pop();
76        }
77        let self_len = self_level.len();
78        let other_len = other_level.len();
79
80        match self_len.cmp(&other_len) {
81            std::cmp::Ordering::Less => LevelRelation::OutLevel((other_len - self_len) as i32),
82            std::cmp::Ordering::Equal => LevelRelation::SameLevel,
83            std::cmp::Ordering::Greater => LevelRelation::InLevel((self_len - other_len) as i32),
84        }
85    }
86
87    ///Return the nesting level (depth)
88    ///depth is 0 based.
89    /// if last element is 0 does not count as doorstop convention uses that as a mark for headers.
90    pub fn get_depth(&self) -> i32 {
91        let mut level = self.get_level_key();
92        while level.last().is_some_and(|l| 0 == *l) {
93            level.pop();
94        }
95        (level.len() - 1) as i32
96    }
97
98    pub fn new(path: PathBuf) -> Result<Self, Box<dyn Error>> {
99        let reader = std::fs::File::open(&path)?;
100        let mut item: Item = serde_yaml::from_reader(reader).unwrap();
101        let id = path
102            .file_stem()
103            .ok_or("Error on stem")?
104            .to_str()
105            .ok_or("Error on convert to str")?
106            .to_string();
107        item.id = Some(id);
108        Ok(item)
109    }
110
111    pub fn get_level(&self) -> &str {
112        match &self.level {
113            Some(s) => s,
114            None => "1",
115        }
116    }
117
118    pub fn get_level_key(&self) -> Vec<i32> {
119        let level = self.get_level();
120        level
121            .split(".")
122            .filter_map(|level| i32::from_str(level).ok())
123            .collect()
124    }
125}
126
127#[derive(Debug)]
128pub struct Document {
129    pub config: DocumentConfig,
130    pub root_path: PathBuf,
131    pub items: HashMap<Rc<String>, Rc<Item>>,
132    pub items_sorted_by_level: BTreeMap<Vec<i32>, Rc<Item>>,
133}
134
135impl Document {
136    pub fn new(path: PathBuf) -> Result<Self, Box<dyn Error>> {
137        let reader = std::fs::File::open(&path)?;
138        let document_config: DocumentConfig = serde_yaml::from_reader(reader).unwrap();
139        let id_item: HashMap<Rc<String>, Rc<Item>> = HashMap::new();
140
141        let mut doc = Document {
142            config: document_config,
143            root_path: path,
144            items: id_item,
145            items_sorted_by_level: BTreeMap::new(),
146        };
147
148        for each_item_path in doc.get_items_files() {
149            let i = Rc::new(Item::new(each_item_path)?);
150            let a = Rc::new(i.id.as_ref().unwrap().clone());
151            doc.items.insert(a.clone(), i.clone());
152            doc.items_sorted_by_level.insert(i.get_level_key(), i);
153        }
154        Ok(doc)
155    }
156
157    fn get_items_files(&self) -> Vec<PathBuf> {
158        let d = self.root_path.parent().unwrap();
159        let into_iter = WalkDir::new(d).into_iter();
160
161        let only_reqs_yml = into_iter
162            .filter_map(|e| e.ok())
163            .filter(|e| file::is_yml_with_prefix(&e, &self.config.settings.prefix));
164
165        only_reqs_yml.filter_map(|e| Some(e.into_path())).collect()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::doorstop::document::LevelRelation;
172
173    use super::{Document, Item};
174    use std::{path::Path, rc::Rc};
175
176    #[test]
177    fn test_new_document() {
178        let p = Path::new("resources/reqs/.doorstop.yml");
179        let d = Document::new(p.to_path_buf());
180        let k = Rc::new("REQ012".to_string());
181        assert!(d.is_ok());
182        assert!(
183            d.unwrap().items.get(k.as_ref()).is_some(),
184            "Getting one item"
185        );
186    }
187
188    #[test]
189    fn test_new_document_ext() {
190        let p = Path::new("resources/reqs/ext/.doorstop.yml");
191        let d = Document::new(p.to_path_buf());
192        assert!(d.is_ok());
193    }
194
195    #[test]
196    fn test_get_items() {
197        let p = Path::new("resources/reqs/.doorstop.yml");
198        let d = Document::new(p.to_path_buf()).unwrap();
199        let files = d.get_items_files();
200        assert_eq!(18, files.len(), "Wrong amount of files detected")
201    }
202
203    #[test]
204    fn test_deserialize_item() {
205        let p = Path::new("resources/reqs/REQ004.yml");
206        let item = Item::new(p.to_path_buf()).ok().unwrap();
207        assert!(item.active.unwrap());
208        assert!(!item.derived.unwrap());
209        assert_eq!("REQ004", item.id.unwrap());
210    }
211
212    #[test]
213    fn test_item_get_depth_default() {
214        let item = Item {
215            id: None,
216            active: None,
217            derived: None,
218            header: None,
219            level: None,
220            normative: None,
221            reviewed: None,
222            text: None,
223        };
224        assert_eq!(0, item.get_depth());
225    }
226
227    #[test]
228    fn test_item_get_depth_1() {
229        let item = Item {
230            id: None,
231            active: None,
232            derived: None,
233            header: None,
234            level: Some("2.10".to_string()),
235            normative: None,
236            reviewed: None,
237            text: None,
238        };
239        assert_eq!(1, item.get_depth());
240    }
241
242    #[test]
243    fn test_item_get_depth_0() {
244        let item = Item {
245            id: None,
246            active: None,
247            derived: None,
248            header: None,
249            level: Some("2.0".to_string()),
250            normative: None,
251            reviewed: None,
252            text: None,
253        };
254        assert_eq!(0, item.get_depth());
255    }
256
257    #[test]
258    fn test_sort_by_level() {
259        let p = Path::new("resources/reqs/.doorstop.yml");
260        let d = Document::new(p.to_path_buf()).unwrap();
261        let level_stack = 0;
262        for (_, each_item) in d.items_sorted_by_level.iter() {
263            println!("{},{:?}-{:?}", level_stack, each_item.level, each_item.id);
264        }
265    }
266    #[test]
267    fn test_level_relation_same() {
268        let a = Item {
269            level: Some("1.0".to_string()),
270            id: None,
271            active: None,
272            derived: None,
273            header: None,
274            normative: None,
275            reviewed: None,
276            text: None,
277        };
278        let b = Item {
279            level: Some("2.0".to_string()),
280            id: None,
281            active: None,
282            derived: None,
283            header: None,
284            normative: None,
285            reviewed: None,
286            text: None,
287        };
288        assert_eq!(a.level_relation(&b), LevelRelation::SameLevel);
289    }
290
291    #[test]
292    fn test_level_relation_level_out() {
293        let a = Item {
294            level: Some("1.2".to_string()),
295            id: None,
296            active: None,
297            derived: None,
298            header: None,
299            normative: None,
300            reviewed: None,
301            text: None,
302        };
303        let b = Item {
304            level: Some("2.0".to_string()),
305            id: None,
306            active: None,
307            derived: None,
308            header: None,
309            normative: None,
310            reviewed: None,
311            text: None,
312        };
313        assert_eq!(b.level_relation(&a), LevelRelation::OutLevel(1));
314    }
315
316    #[test]
317    fn test_level_relation_level_out_2() {
318        let a = Item {
319            level: Some("1.2.1".to_string()),
320            id: None,
321            active: None,
322            derived: None,
323            header: None,
324            normative: None,
325            reviewed: None,
326            text: None,
327        };
328        let b = Item {
329            level: Some("2.0".to_string()),
330            id: None,
331            active: None,
332            derived: None,
333            header: None,
334            normative: None,
335            reviewed: None,
336            text: None,
337        };
338        assert_eq!(b.level_relation(&a), LevelRelation::OutLevel(2));
339    }
340
341    #[test]
342    fn test_level_relation_level_in() {
343        let a = Item {
344            level: Some("1.0".to_string()),
345            id: None,
346            active: None,
347            derived: None,
348            header: None,
349            normative: None,
350            reviewed: None,
351            text: None,
352        };
353        let b = Item {
354            level: Some("1.2".to_string()),
355            id: None,
356            active: None,
357            derived: None,
358            header: None,
359            normative: None,
360            reviewed: None,
361            text: None,
362        };
363        assert_eq!(b.level_relation(&a), LevelRelation::InLevel(1));
364    }
365}