use petgraph::prelude::*;
use std::collections::VecDeque;
use crate::file_collection::FileCollection;
use crate::identifier::Id;
use crate::prelude::ProjectId;
use crate::unstable::text_factory::graph::PrettyGraph;
use ptree::{IndentChars, PrintConfig};
use std::fmt;
use std::fmt::Write as _;
use std::fmt::{Debug, Display, Formatter};
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::str::FromStr;
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ProjectDescriptor {
build_file: ProjectDescriptorLocation,
name: String,
}
impl Display for ProjectDescriptor {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{} ({:?})", self.name, self.build_file)
}
}
impl ProjectDescriptor {
fn new<S: AsRef<str>>(name: S, build_file: ProjectDescriptorLocation) -> Self {
let name = name.as_ref().to_string();
Self { build_file, name }
}
fn set_file_name(&mut self, file_name: &str) {
let file_path = if let ProjectDescriptorLocation::KnownDirectory(dir) = &self.build_file {
dir.join(file_name)
} else {
return;
};
self.build_file = ProjectDescriptorLocation::KnownFile(file_path);
}
pub fn name(&self) -> &str {
&self.name
}
pub fn set_name(&mut self, name: impl AsRef<str>) {
self.name = name.as_ref().to_string();
}
pub fn build_file(&self) -> Option<&Path> {
match &self.build_file {
ProjectDescriptorLocation::KnownFile(f) => Some(f),
ProjectDescriptorLocation::KnownDirectory(_) => None,
}
}
pub fn directory(&self) -> &Path {
match &self.build_file {
ProjectDescriptorLocation::KnownFile(file) => file.parent().unwrap(),
ProjectDescriptorLocation::KnownDirectory(dir) => dir.as_ref(),
}
}
pub fn matches_dir(&self, path: impl AsRef<Path>) -> bool {
let path = path.as_ref();
match &self.build_file {
ProjectDescriptorLocation::KnownFile(f) => match f.parent() {
Some(parent) => parent.ends_with(path),
None => path == Path::new(""),
},
ProjectDescriptorLocation::KnownDirectory(d) => d.ends_with(path),
}
}
}
#[derive(Clone, Eq, PartialEq)]
enum ProjectDescriptorLocation {
KnownFile(PathBuf),
KnownDirectory(PathBuf),
}
impl Debug for ProjectDescriptorLocation {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
ProjectDescriptorLocation::KnownFile(file) => {
write!(f, "{:?}", file)
}
ProjectDescriptorLocation::KnownDirectory(d) => {
write!(f, "{:?}", d.join("?"))
}
}
}
}
#[derive(Debug)]
pub struct ProjectGraph {
graph: DiGraph<ProjectDescriptor, ()>,
project_dir: PathBuf,
root_project: NodeIndex,
default_build_script_file: Option<String>,
}
impl ProjectGraph {
pub(crate) fn new<P: AsRef<Path>>(project_dir: P) -> Self {
let project_name = project_dir
.as_ref()
.file_name()
.unwrap_or_else(|| panic!("{:?} has no file name", project_dir.as_ref()))
.to_str()
.unwrap_or_else(|| {
panic!(
"{:?} file name can not be represented as a utf-8 string",
project_dir.as_ref()
)
});
let root_project = ProjectDescriptor::new(
project_name,
ProjectDescriptorLocation::KnownDirectory(project_dir.as_ref().to_path_buf()),
);
let mut graph = DiGraph::new();
let idx = graph.add_node(root_project);
Self {
graph,
project_dir: project_dir.as_ref().to_path_buf(),
root_project: idx,
default_build_script_file: None,
}
}
pub fn root_project(&self) -> &ProjectDescriptor {
&self.graph[self.root_project]
}
pub fn root_project_mut(&mut self) -> &mut ProjectDescriptor {
&mut self.graph[self.root_project]
}
pub fn set_default_build_file_name(&mut self, name: &str) {
if self.default_build_script_file.is_some() {
panic!(
"default build script file name already set to {:?}",
self.default_build_script_file.as_ref().unwrap()
);
}
self.default_build_script_file = Some(name.to_string());
for node in self.graph.node_indices() {
self.graph[node].set_file_name(name);
}
}
pub fn project<S: AsRef<str>, F: FnOnce(&mut ProjectBuilder)>(
&mut self,
path: S,
configure: F,
) {
let path = path.as_ref();
trace!("adding project with path {:?}", path);
let mut builder = ProjectBuilder::new(&self.project_dir, path.to_string());
(configure)(&mut builder);
self.add_project_from_builder(self.root_project, builder);
}
fn add_project_from_builder(&mut self, parent: NodeIndex, builder: ProjectBuilder) {
let ProjectBuilder {
name,
dir,
children,
} = builder;
let location = match &self.default_build_script_file {
None => ProjectDescriptorLocation::KnownDirectory(dir),
Some(s) => ProjectDescriptorLocation::KnownFile(dir.join(s)),
};
let pd = ProjectDescriptor::new(name, location);
let node = self.graph.add_node(pd);
self.graph.add_edge(parent, node, ());
for child_builder in children {
self.add_project_from_builder(node, child_builder);
}
}
pub fn find_project<P: AsRef<Path>>(&self, path: P) -> Option<&ProjectDescriptor> {
self.graph
.node_indices()
.find(|&idx| self.graph[idx].matches_dir(&path))
.map(|idx| &self.graph[idx])
}
pub fn find_project_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut ProjectDescriptor> {
self.graph
.node_indices()
.find(|&idx| self.graph[idx].matches_dir(&path))
.map(|idx| &mut self.graph[idx])
}
pub fn children_projects(
&self,
proj: &ProjectDescriptor,
) -> impl IntoIterator<Item = &ProjectDescriptor> {
self.graph
.node_indices()
.find(|&idx| &self.graph[idx] == proj)
.into_iter()
.map(|index| {
self.graph
.neighbors(index)
.into_iter()
.map(|neighbor| &self.graph[neighbor])
})
.flatten()
}
pub fn get_project_id(&self, desc: &ProjectDescriptor) -> ProjectId {
let start = self
.graph
.node_indices()
.find(|&idx| &self.graph[idx] == desc)
.unwrap();
let mut queue = VecDeque::new();
queue.push_front(self.graph[start].name.clone());
let mut ptr = start;
while let Some(parent) = self.graph.edges_directed(ptr, Direction::Incoming).next() {
ptr = parent.source();
queue.push_front(self.graph[ptr].name.clone());
}
Id::from_iter(queue).unwrap().into()
}
}
impl Display for ProjectGraph {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let pretty = PrettyGraph::new(&self.graph, self.root_project);
write!(f, "{}", pretty)
}
}
pub struct ProjectBuilder {
name: String,
dir: PathBuf,
children: Vec<ProjectBuilder>,
}
impl ProjectBuilder {
fn new(parent_dir: &Path, name: String) -> Self {
let mut dir = parent_dir.join(&name);
trace!("parent dir: {parent_dir:?}");
trace!("project name: {name:?}");
trace!("using dir: {dir:?}");
Self {
name,
dir,
children: vec![],
}
}
pub fn set_name(&mut self, name: impl AsRef<str>) {
self.name = name.as_ref().to_string();
}
pub fn set_dir(&mut self, path: impl AsRef<Path>) {
self.dir = path.as_ref().to_path_buf();
}
pub fn project<S: AsRef<str>, F: FnOnce(&mut ProjectBuilder)>(
&mut self,
path: S,
configure: F,
) {
let path = path.as_ref();
let mut builder = ProjectBuilder::new(&self.dir, path.to_string());
(configure)(&mut builder);
self.children.push(builder);
}
}
#[cfg(test)]
mod tests {
use crate::startup::initialization::ProjectGraph;
use std::path::PathBuf;
#[test]
fn print_graph() {
let path = PathBuf::from("assemble");
let mut graph = ProjectGraph::new(path);
graph.project("list", |builder| {
builder.project("linked", |_| {});
builder.project("array", |_| {});
});
graph.project("map", |_| {});
println!("{}", graph);
}
#[test]
fn can_set_default_build_name() {
let path = PathBuf::from("assemble");
let mut graph = ProjectGraph::new(path);
graph.set_default_build_file_name("build.assemble");
println!("{}", graph);
assert_eq!(
graph.root_project().build_file(),
Some(&*PathBuf::from_iter(["assemble", "build.assemble"]))
)
}
#[test]
fn can_find_project() {
let path = PathBuf::from("assemble");
let mut graph = ProjectGraph::new(path);
graph.project("list", |builder| {
builder.project("linked", |_| {});
builder.project("array", |_| {});
});
graph.project("map", |b| {
b.project("set", |_| {});
b.project("ordered", |_| {});
b.project("hashed", |_| {});
});
println!("graph: {:#}", graph);
assert!(graph.find_project("assemble/map/hashed").is_some());
assert!(graph.find_project("assemble/list/array").is_some());
assert!(graph.find_project("assemble/list/garfunkle").is_none());
}
}