assemble_core/startup/initialization/
descriptor.rs1use petgraph::prelude::*;
2use std::collections::VecDeque;
3
4use crate::file_collection::FileCollection;
5use crate::identifier::Id;
6use crate::prelude::ProjectId;
7use crate::unstable::text_factory::graph::PrettyGraph;
8use ptree::{IndentChars, PrintConfig};
9use std::fmt;
10use std::fmt::Write as _;
11use std::fmt::{Debug, Display, Formatter};
12use std::io::Write as _;
13use std::path::{Path, PathBuf};
14use std::str::FromStr;
15
16#[derive(Debug, Clone, Eq, PartialEq)]
18pub struct ProjectDescriptor {
19 build_file: ProjectDescriptorLocation,
20 name: String,
21}
22
23impl Display for ProjectDescriptor {
24 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
25 write!(f, "{} ({:?})", self.name, self.build_file)
26 }
27}
28
29impl ProjectDescriptor {
30 fn new<S: AsRef<str>>(name: S, build_file: ProjectDescriptorLocation) -> Self {
32 let name = name.as_ref().to_string();
33 Self { build_file, name }
34 }
35
36 fn set_file_name(&mut self, file_name: &str) {
38 let file_path = if let ProjectDescriptorLocation::KnownDirectory(dir) = &self.build_file {
39 dir.join(file_name)
40 } else {
41 return;
42 };
43
44 self.build_file = ProjectDescriptorLocation::KnownFile(file_path);
45 }
46
47 pub fn name(&self) -> &str {
49 &self.name
50 }
51
52 pub fn set_name(&mut self, name: impl AsRef<str>) {
54 self.name = name.as_ref().to_string();
55 }
56
57 pub fn build_file(&self) -> Option<&Path> {
59 match &self.build_file {
60 ProjectDescriptorLocation::KnownFile(f) => Some(f),
61 ProjectDescriptorLocation::KnownDirectory(_) => None,
62 }
63 }
64
65 pub fn directory(&self) -> &Path {
67 match &self.build_file {
68 ProjectDescriptorLocation::KnownFile(file) => file.parent().unwrap(),
69 ProjectDescriptorLocation::KnownDirectory(dir) => dir.as_ref(),
70 }
71 }
72
73 pub fn matches_dir(&self, path: impl AsRef<Path>) -> bool {
75 let path = path.as_ref();
76 match &self.build_file {
78 ProjectDescriptorLocation::KnownFile(f) => match f.parent() {
79 Some(parent) => parent.ends_with(path),
80 None => path == Path::new(""),
81 },
82 ProjectDescriptorLocation::KnownDirectory(d) => d.ends_with(path),
83 }
84 }
85}
86
87#[derive(Clone, Eq, PartialEq)]
88enum ProjectDescriptorLocation {
89 KnownFile(PathBuf),
90 KnownDirectory(PathBuf),
91}
92
93impl Debug for ProjectDescriptorLocation {
94 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
95 match self {
96 ProjectDescriptorLocation::KnownFile(file) => {
97 write!(f, "{:?}", file)
98 }
99 ProjectDescriptorLocation::KnownDirectory(d) => {
100 write!(f, "{:?}", d.join("?"))
101 }
102 }
103 }
104}
105
106#[derive(Debug)]
108pub struct ProjectGraph {
109 graph: DiGraph<ProjectDescriptor, ()>,
110 project_dir: PathBuf,
111 root_project: NodeIndex,
112 default_build_script_file: Option<String>,
113}
114
115impl ProjectGraph {
116 pub(crate) fn new<P: AsRef<Path>>(project_dir: P) -> Self {
118 let project_name = project_dir
119 .as_ref()
120 .file_name()
121 .unwrap_or_else(|| panic!("{:?} has no file name", project_dir.as_ref()))
122 .to_str()
123 .unwrap_or_else(|| {
124 panic!(
125 "{:?} file name can not be represented as a utf-8 string",
126 project_dir.as_ref()
127 )
128 });
129
130 let root_project = ProjectDescriptor::new(
131 project_name,
132 ProjectDescriptorLocation::KnownDirectory(project_dir.as_ref().to_path_buf()),
133 );
134 let mut graph = DiGraph::new();
135 let idx = graph.add_node(root_project);
136 Self {
137 graph,
138 project_dir: project_dir.as_ref().to_path_buf(),
139 root_project: idx,
140 default_build_script_file: None,
141 }
142 }
143
144 pub fn root_project(&self) -> &ProjectDescriptor {
146 &self.graph[self.root_project]
147 }
148
149 pub fn root_project_mut(&mut self) -> &mut ProjectDescriptor {
151 &mut self.graph[self.root_project]
152 }
153
154 pub fn set_default_build_file_name(&mut self, name: &str) {
155 if self.default_build_script_file.is_some() {
156 panic!(
157 "default build script file name already set to {:?}",
158 self.default_build_script_file.as_ref().unwrap()
159 );
160 }
161
162 self.default_build_script_file = Some(name.to_string());
163 for node in self.graph.node_indices() {
164 self.graph[node].set_file_name(name);
165 }
166 }
167
168 pub fn project<S: AsRef<str>, F: FnOnce(&mut ProjectBuilder)>(
170 &mut self,
171 path: S,
172 configure: F,
173 ) {
174 let path = path.as_ref();
175 trace!("adding project with path {:?}", path);
176 let mut builder = ProjectBuilder::new(&self.project_dir, path.to_string());
177 (configure)(&mut builder);
178 self.add_project_from_builder(self.root_project, builder);
179 }
180
181 fn add_project_from_builder(&mut self, parent: NodeIndex, builder: ProjectBuilder) {
183 let ProjectBuilder {
184 name,
185 dir,
186 children,
187 } = builder;
188
189 let location = match &self.default_build_script_file {
190 None => ProjectDescriptorLocation::KnownDirectory(dir),
191 Some(s) => ProjectDescriptorLocation::KnownFile(dir.join(s)),
192 };
193 let pd = ProjectDescriptor::new(name, location);
194
195 let node = self.graph.add_node(pd);
196 self.graph.add_edge(parent, node, ());
197
198 for child_builder in children {
199 self.add_project_from_builder(node, child_builder);
200 }
201 }
202
203 pub fn find_project<P: AsRef<Path>>(&self, path: P) -> Option<&ProjectDescriptor> {
205 self.graph
206 .node_indices()
207 .find(|&idx| self.graph[idx].matches_dir(&path))
208 .map(|idx| &self.graph[idx])
209 }
210
211 pub fn find_project_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut ProjectDescriptor> {
213 self.graph
214 .node_indices()
215 .find(|&idx| self.graph[idx].matches_dir(&path))
216 .map(|idx| &mut self.graph[idx])
217 }
218
219 pub fn children_projects(
221 &self,
222 proj: &ProjectDescriptor,
223 ) -> impl IntoIterator<Item = &ProjectDescriptor> {
224 self.graph
225 .node_indices()
226 .find(|&idx| &self.graph[idx] == proj)
227 .into_iter()
228 .map(|index| {
229 self.graph
230 .neighbors(index)
231 .into_iter()
232 .map(|neighbor| &self.graph[neighbor])
233 })
234 .flatten()
235 }
236
237 pub fn get_project_id(&self, desc: &ProjectDescriptor) -> ProjectId {
238 let start = self
239 .graph
240 .node_indices()
241 .find(|&idx| &self.graph[idx] == desc)
242 .unwrap();
243
244 let mut queue = VecDeque::new();
245 queue.push_front(self.graph[start].name.clone());
246 let mut ptr = start;
247
248 while let Some(parent) = self.graph.edges_directed(ptr, Direction::Incoming).next() {
249 ptr = parent.source();
250 queue.push_front(self.graph[ptr].name.clone());
251 }
252
253 Id::from_iter(queue).unwrap().into()
254 }
255}
256
257impl Display for ProjectGraph {
258 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
259 let pretty = PrettyGraph::new(&self.graph, self.root_project);
260 write!(f, "{}", pretty)
261 }
262}
263
264pub struct ProjectBuilder {
266 name: String,
267 dir: PathBuf,
268 children: Vec<ProjectBuilder>,
269}
270
271impl ProjectBuilder {
272 fn new(parent_dir: &Path, name: String) -> Self {
273 let mut dir = parent_dir.join(&name);
274 trace!("parent dir: {parent_dir:?}");
275 trace!("project name: {name:?}");
276 trace!("using dir: {dir:?}");
277 Self {
278 name,
279 dir,
280 children: vec![],
281 }
282 }
283
284 pub fn set_name(&mut self, name: impl AsRef<str>) {
286 self.name = name.as_ref().to_string();
287 }
288
289 pub fn set_dir(&mut self, path: impl AsRef<Path>) {
292 self.dir = path.as_ref().to_path_buf();
293 }
294
295 pub fn project<S: AsRef<str>, F: FnOnce(&mut ProjectBuilder)>(
298 &mut self,
299 path: S,
300 configure: F,
301 ) {
302 let path = path.as_ref();
303 let mut builder = ProjectBuilder::new(&self.dir, path.to_string());
304 (configure)(&mut builder);
305 self.children.push(builder);
306 }
307}
308
309#[cfg(test)]
310mod tests {
311 use crate::startup::initialization::ProjectGraph;
312
313 use std::path::PathBuf;
314
315 #[test]
316 fn print_graph() {
317 let path = PathBuf::from("assemble");
318 let mut graph = ProjectGraph::new(path);
319
320 graph.project("list", |builder| {
321 builder.project("linked", |_| {});
322 builder.project("array", |_| {});
323 });
324 graph.project("map", |_| {});
325
326 println!("{}", graph);
327 }
328
329 #[test]
330 fn can_set_default_build_name() {
331 let path = PathBuf::from("assemble");
332 let mut graph = ProjectGraph::new(path);
333 graph.set_default_build_file_name("build.assemble");
334
335 println!("{}", graph);
336 assert_eq!(
337 graph.root_project().build_file(),
338 Some(&*PathBuf::from_iter(["assemble", "build.assemble"]))
339 )
340 }
341
342 #[test]
343 fn can_find_project() {
344 let path = PathBuf::from("assemble");
345 let mut graph = ProjectGraph::new(path);
346
347 graph.project("list", |builder| {
348 builder.project("linked", |_| {});
349 builder.project("array", |_| {});
350 });
351 graph.project("map", |b| {
352 b.project("set", |_| {});
353 b.project("ordered", |_| {});
354 b.project("hashed", |_| {});
355 });
356
357 println!("graph: {:#}", graph);
358
359 assert!(graph.find_project("assemble/map/hashed").is_some());
360 assert!(graph.find_project("assemble/list/array").is_some());
361 assert!(graph.find_project("assemble/list/garfunkle").is_none());
362 }
363}