assemble_core/startup/initialization/
descriptor.rs

1use 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/// A project descriptor is used to define projects.
17#[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    /// creates a new project descriptor
31    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    /// if this only has the directory known, this sets the file name
37    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    /// Gets the name of the project
48    pub fn name(&self) -> &str {
49        &self.name
50    }
51
52    /// Sets the name of the project
53    pub fn set_name(&mut self, name: impl AsRef<str>) {
54        self.name = name.as_ref().to_string();
55    }
56
57    /// Gets the build file associated with this project, if known
58    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    /// Gets the directory this project is contained in
66    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    /// Checks if this project descriptor is contained in this directory
74    pub fn matches_dir(&self, path: impl AsRef<Path>) -> bool {
75        let path = path.as_ref();
76        // info!("checking if {:?} matches this {:?}", path, self.build_file);
77        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/// Describes the project graph
107#[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    /// Creates a new project graph with a pre-initialized root project
117    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    /// Gets the root project descriptor
145    pub fn root_project(&self) -> &ProjectDescriptor {
146        &self.graph[self.root_project]
147    }
148
149    /// Gets a mutable reference to the root project descriptor
150    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    /// Adds a child project to the root project
169    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    /// Adds a child project to some other project
182    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    /// Find a project by path
204    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    /// Find a project by path
212    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    /// Gets the child project of a given project
220    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
264/// Helps to build a project
265pub 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    /// Sets the name of this project
285    pub fn set_name(&mut self, name: impl AsRef<str>) {
286        self.name = name.as_ref().to_string();
287    }
288
289    /// Sets the directory of this project. by default, the directory is the parent projects
290    /// directory + name
291    pub fn set_dir(&mut self, path: impl AsRef<Path>) {
292        self.dir = path.as_ref().to_path_buf();
293    }
294
295    /// Adds a child project to this project. `path` is relative to this project, and should
296    /// be written as a simple identifier
297    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}