Skip to main content

wowlab_docgen_cli/
workspace.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    sync::Arc,
4};
5
6use minijinja::Value;
7use wowlab_fs::path::{Component, Path, PathBuf};
8
9use crate::infra::metadata::{self, Metadata};
10
11type IndexedFiles = BTreeMap<PathBuf, Option<Box<str>>>;
12
13#[derive(Debug)]
14pub struct WorkspaceFile {
15    pub path: PathBuf,
16    pub contents: Option<Box<str>>,
17}
18
19#[derive(Clone, Debug)]
20pub struct WorkspaceIndex {
21    root: PathBuf,
22    files: Arc<IndexedFiles>,
23    directories: Arc<BTreeSet<PathBuf>>,
24    metadata: Arc<BTreeMap<PathBuf, Metadata>>,
25    dependency_graph: Arc<str>,
26    tidy_rules: Option<Value>,
27}
28
29impl WorkspaceIndex {
30    #[must_use]
31    pub fn new(root: PathBuf, entries: Vec<WorkspaceFile>) -> Self {
32        let mut files = BTreeMap::new();
33        let mut directories = BTreeSet::from([root.clone()]);
34
35        for entry in entries {
36            if let Some(parent) = entry.path.parent() {
37                let mut current = Some(parent);
38
39                while let Some(dir) = current {
40                    if !dir.starts_with(&root) {
41                        break;
42                    }
43
44                    directories.insert(dir.to_path_buf());
45                    current = dir.parent();
46                }
47            }
48
49            files.insert(entry.path, entry.contents);
50        }
51
52        let workspace_deps = metadata::workspace_cargo_deps(&root, &files);
53        let metadata = directories
54            .iter()
55            .map(|dir| {
56                (
57                    dir.clone(),
58                    metadata::parse(dir, &files, &directories, &workspace_deps),
59                )
60            })
61            .collect();
62
63        Self {
64            root,
65            files: Arc::new(files),
66            directories: Arc::new(directories),
67            metadata: Arc::new(metadata),
68            dependency_graph: Arc::from(""),
69            tidy_rules: None,
70        }
71    }
72
73    #[must_use]
74    pub fn with_dependency_graph(mut self, graph: String) -> Self {
75        self.dependency_graph = Arc::from(graph);
76
77        self
78    }
79
80    #[must_use]
81    pub fn with_tidy_rules(mut self, rules: Option<Value>) -> Self {
82        self.tidy_rules = rules;
83
84        self
85    }
86
87    #[must_use]
88    pub fn root(&self) -> &Path {
89        &self.root
90    }
91
92    #[must_use]
93    pub fn contains(&self, path: &Path) -> bool {
94        self.files.contains_key(path) || self.directories.contains(path)
95    }
96
97    #[must_use]
98    pub fn is_dir(&self, path: &Path) -> bool {
99        self.directories.contains(path)
100    }
101
102    #[must_use]
103    pub fn contents(&self, path: &Path) -> Option<&str> {
104        self.files.get(path)?.as_deref()
105    }
106
107    #[must_use]
108    pub fn metadata(&self, dir: &Path) -> Option<&Metadata> {
109        self.metadata.get(&normalize(dir))
110    }
111
112    pub fn child_dirs(&self, dir: &Path) -> impl Iterator<Item = &Path> {
113        self.directories
114            .iter()
115            .filter(move |candidate| candidate.parent() == Some(dir))
116            .map(PathBuf::as_path)
117    }
118
119    pub fn files_in(&self, dir: &Path) -> impl Iterator<Item = &Path> {
120        self.files
121            .keys()
122            .filter(move |candidate| candidate.parent() == Some(dir))
123            .map(PathBuf::as_path)
124    }
125
126    #[must_use]
127    pub fn templates(&self, filters: &[Box<str>]) -> Vec<PathBuf> {
128        let mut templates = self
129            .files
130            .keys()
131            .filter(|path| {
132                path.file_name()
133                    .is_some_and(|name| name.to_string_lossy().ends_with(".md.in"))
134            })
135            .filter(|path| {
136                filters.is_empty()
137                    || path.strip_prefix(&self.root).is_ok_and(|relative| {
138                        let relative = relative.to_string_lossy();
139
140                        filters
141                            .iter()
142                            .any(|filter| relative.contains(filter.as_ref()))
143                    })
144            })
145            .cloned()
146            .collect::<Vec<_>>();
147
148        templates.sort();
149
150        templates
151    }
152
153    #[must_use]
154    pub fn dependency_graph(&self) -> &str {
155        &self.dependency_graph
156    }
157
158    #[must_use]
159    pub fn tidy_rules(&self) -> Option<&Value> {
160        self.tidy_rules.as_ref()
161    }
162
163    #[must_use]
164    pub fn has_files_below(&self, dir: &Path) -> bool {
165        self.files.keys().any(|path| path.starts_with(dir))
166    }
167}
168
169pub(crate) fn normalize(path: &Path) -> PathBuf {
170    let mut normalized = PathBuf::new();
171
172    for component in path.components() {
173        match component {
174            Component::Parent => {
175                normalized.pop();
176            }
177            Component::Current => {}
178            component => normalized.push(component.as_os_str()),
179        }
180    }
181
182    normalized
183}