use crate::{
fs::{config_dir, home_dir},
Error, Result,
};
use std::{
collections::{HashMap, HashSet, VecDeque},
ffi::OsString,
path::PathBuf,
};
use toml_edit::{Array, DocumentMut, InlineTable, Item, Key, Table, Value};
use tracing::{debug, instrument, warn};
#[derive(Clone, Default, Debug)]
pub struct Cluster {
pub root: RootEntry,
pub nodes: HashMap<String, NodeEntry>,
document: DocumentMut,
}
impl Cluster {
pub fn new() -> Self {
Cluster::default()
}
pub fn get_node(&self, name: impl AsRef<str>) -> Result<&NodeEntry> {
self.nodes.get(name.as_ref()).ok_or(Error::EntryNotFound { name: name.as_ref().into() })
}
pub fn add_node(
&mut self,
name: impl AsRef<str>,
node: NodeEntry,
) -> Result<Option<NodeEntry>> {
let (key, item) = node.to_toml(name.as_ref());
let table = if let Some(item) = self.document.get_mut("nodes") {
item.as_table_mut().ok_or(Error::EntryNotTable { name: "nodes".into() })?
} else {
let mut new_table = Table::new();
new_table.set_implicit(true);
self.document.insert("nodes", Item::Table(new_table));
self.document["nodes"].as_table_mut().unwrap()
};
table.insert_formatted(&key, item);
Ok(self.nodes.insert(name.as_ref().into(), node))
}
pub fn remove_node(&mut self, node: impl AsRef<str>) -> Result<NodeEntry> {
self.document
.get_mut("nodes")
.and_then(|item| item.as_table_mut())
.ok_or(Error::EntryNotFound { name: "nodes".into() })?
.remove(node.as_ref())
.ok_or(Error::EntryNotFound { name: node.as_ref().into() })?;
self.nodes.remove(node.as_ref()).ok_or(Error::EntryNotFound { name: node.as_ref().into() })
}
pub fn dependency_iter(&self, node: impl Into<String>) -> DependencyIter<'_> {
let mut stack = VecDeque::new();
stack.push_front(node.into());
DependencyIter { graph: &self.nodes, visited: HashSet::new(), stack }
}
fn dependency_existence_check(&self) -> Result<()> {
let mut results = Vec::new();
for node in self.nodes.values() {
for dependency in node.dependencies.iter().flatten() {
if !self.nodes.contains_key(dependency) {
results.push(Err(Error::DependencyNotFound { name: dependency.clone() }));
} else {
results.push(Ok(()));
}
}
}
results.into_iter().collect::<_>()
}
#[instrument(skip(self), level = "debug")]
fn acyclic_check(&self) -> Result<()> {
let mut in_degree: HashMap<String, usize> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new();
let mut visited: HashSet<String> = HashSet::new();
for (name, node) in &self.nodes {
in_degree.entry(name.clone()).or_insert(0);
for dependency in node.dependencies.iter().flatten() {
*in_degree.entry(dependency.clone()).or_insert(0) += 1;
}
}
for (name, degree) in &in_degree {
if *degree == 0 {
queue.push_back(name.clone());
}
}
while let Some(current) = queue.pop_front() {
for dependency in self.nodes[¤t].dependencies.iter().flatten() {
*in_degree.get_mut(dependency).unwrap() -= 1;
if *in_degree.get(dependency).unwrap() == 0 {
queue.push_back(dependency.clone());
}
}
visited.insert(current);
}
if visited.len() != self.nodes.len() {
let cycle: Vec<String> =
self.nodes.keys().filter(|key| !visited.contains(*key)).cloned().collect();
return Err(Error::CircularDependencies { cycle });
}
debug!("Topological sort of cluster nodes: {visited:?}");
Ok(())
}
fn expand_dir_aliases(&mut self) -> Result<()> {
for node in self.nodes.values_mut() {
if let DeploymentKind::BareAlias(dir_alias) = &node.deployment {
node.deployment = DeploymentKind::BareAlias(DirAlias::new(
shellexpand::full(&dir_alias.to_string())?.into_owned(),
));
}
}
Ok(())
}
}
impl std::fmt::Display for Cluster {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.document)
}
}
impl std::str::FromStr for Cluster {
type Err = Error;
fn from_str(data: &str) -> Result<Self, Self::Err> {
let document: DocumentMut = data.parse()?;
let root = RootEntry::try_from(document.as_table())?;
let nodes = if let Some(entries) = document.get("nodes").and_then(|node| node.as_table()) {
let mut nodes: HashMap<String, NodeEntry> = HashMap::new();
for (key, value) in entries.iter() {
nodes.insert(key.into(), NodeEntry::try_from(value)?);
}
nodes
} else {
HashMap::new()
};
let mut cluster = Self { root, nodes, document };
cluster.dependency_existence_check()?;
cluster.acyclic_check()?;
cluster.expand_dir_aliases()?;
Ok(cluster)
}
}
#[derive(Debug)]
pub struct DependencyIter<'cluster> {
graph: &'cluster HashMap<String, NodeEntry>,
visited: HashSet<String>,
stack: VecDeque<String>,
}
impl<'cluster> Iterator for DependencyIter<'cluster> {
type Item = (&'cluster str, &'cluster NodeEntry);
fn next(&mut self) -> Option<Self::Item> {
if let Some(node) = self.stack.pop_front() {
let (name, node) = self.graph.get_key_value(&node)?;
for dependency in node.dependencies.iter().flatten() {
if !self.visited.contains(dependency) {
self.stack.push_front(dependency.clone());
self.visited.insert(dependency.clone());
}
}
return Some((name.as_ref(), node));
}
None
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct RootEntry {
pub dir_alias: DirAlias,
pub excluded: Option<Vec<String>>,
}
impl RootEntry {
pub fn new() -> Self {
RootEntry::default()
}
}
impl<'toml> TryFrom<&'toml Table> for RootEntry {
type Error = Error;
fn try_from(table: &'toml Table) -> Result<Self, Self::Error> {
let mut root = RootEntry::new();
let dir_alias = if let Some(entry) = table.get("dir_alias") {
if let Some(alias) = entry.as_str() {
if alias == "config_dir" {
config_dir()?
} else if alias == "home_dir" {
home_dir()?
} else {
warn!("Invalid value {alias:?} for \"root.dir_alias\", using default");
config_dir()?
}
} else {
config_dir()?
}
} else {
config_dir()?
};
root.dir_alias = DirAlias::new(dir_alias);
root.excluded = table.get("excluded").and_then(|rules| {
rules.as_array().map(|arr| {
arr.into_iter().map(|rule| rule.as_str().unwrap_or_default().into()).collect()
})
});
Ok(root)
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct NodeEntry {
pub deployment: DeploymentKind,
pub url: String,
pub excluded: Option<Vec<String>>,
pub dependencies: Option<Vec<String>>,
}
impl NodeEntry {
pub fn new() -> Self {
NodeEntry::default()
}
pub fn to_toml(&self, name: impl AsRef<str>) -> (Key, Item) {
let mut node = Table::new();
match &self.deployment {
DeploymentKind::Normal => {
node.insert("deployment", Item::Value(Value::from("normal")));
}
DeploymentKind::BareAlias(alias) => {
if alias.is_home_dir() {
node.insert("deployment", Item::Value(Value::from("bare_alias")));
} else {
let mut inline = InlineTable::new();
inline.insert("kind", Value::from("bare_alias"));
inline.insert("dir_alias", Value::from(alias.to_string()));
node.insert("deployment", Item::Value(Value::from(inline)));
}
}
}
node.insert("url", Item::Value(Value::from(&self.url)));
if let Some(excluded) = &self.excluded {
node.insert("excluded", Item::Value(Value::Array(Array::from_iter(excluded))));
}
if let Some(dependencies) = &self.dependencies {
node.insert("dependencies", Item::Value(Value::Array(Array::from_iter(dependencies))));
}
let key = Key::new(name.as_ref());
let value = Item::Table(node);
(key, value)
}
}
impl<'toml> TryFrom<&'toml Item> for NodeEntry {
type Error = Error;
fn try_from(item: &'toml Item) -> Result<Self, Self::Error> {
let mut node = NodeEntry::new();
node.deployment = if let Some(deployment) = item.get("deployment") {
if let Some(entry) = deployment.as_str() {
match entry {
"normal" => DeploymentKind::Normal,
"bare_alias" => DeploymentKind::BareAlias(DirAlias::new(home_dir()?)),
&_ => DeploymentKind::default(),
}
} else {
let kind =
deployment.get("kind").and_then(|kind| kind.as_str()).unwrap_or_default();
let alias = deployment
.get("dir_alias")
.and_then(|alias| alias.as_str().map(Into::into))
.unwrap_or(home_dir()?);
match kind {
"normal" => DeploymentKind::Normal,
"bare_alias" => DeploymentKind::BareAlias(DirAlias::new(alias)),
&_ => DeploymentKind::default(),
}
}
} else {
DeploymentKind::default()
};
node.url = item.get("url").and_then(|url| url.as_str().map(Into::into)).unwrap_or_default();
node.excluded = item.get("excluded").and_then(|rules| {
rules.as_array().map(|arr| {
arr.into_iter().map(|rule| rule.as_str().unwrap_or_default().into()).collect()
})
});
node.dependencies = item.get("dependencies").and_then(|deps| {
deps.as_array().map(|arr| {
arr.into_iter().map(|dep| dep.as_str().unwrap_or_default().into()).collect()
})
});
Ok(node)
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub enum DeploymentKind {
#[default]
Normal,
BareAlias(DirAlias),
}
impl DeploymentKind {
pub(crate) fn is_bare(&self) -> bool {
match self {
DeploymentKind::Normal => false,
DeploymentKind::BareAlias(..) => true,
}
}
}
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct DirAlias(pub(crate) PathBuf);
impl DirAlias {
pub(crate) fn new(path: impl Into<PathBuf>) -> Self {
Self(path.into())
}
pub(crate) fn is_home_dir(&self) -> bool {
let home = match home_dir() {
Ok(path) => path,
Err(_) => return false,
};
if self.0 == home {
return true;
}
false
}
pub(crate) fn to_os_string(&self) -> OsString {
OsString::from(self.0.to_string_lossy().into_owned())
}
}
impl std::fmt::Display for DirAlias {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_string_lossy().into_owned())
}
}