wowlab_docgen_cli/
workspace.rs1use 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}