use std::collections::{BTreeMap, BTreeSet};
pub type Graph = BTreeMap<String, BTreeSet<String>>;
pub fn waves(graph: &Graph) -> Result<Vec<Vec<String>>, Vec<String>> {
let mut pending: BTreeMap<&str, BTreeSet<&str>> = graph
.iter()
.map(|(id, deps)| {
(
id.as_str(),
deps.iter()
.filter(|d| graph.contains_key(*d) && *d != id)
.map(String::as_str)
.collect(),
)
})
.collect();
let self_loops: Vec<String> = graph
.iter()
.filter(|(id, deps)| deps.contains(*id))
.map(|(id, _)| id.clone())
.collect();
if !self_loops.is_empty() {
return Err(self_loops);
}
let mut out = Vec::new();
while !pending.is_empty() {
let ready: Vec<&str> = pending
.iter()
.filter(|(_, deps)| deps.is_empty())
.map(|(id, _)| *id)
.collect();
if ready.is_empty() {
return Err(pending.keys().map(|s| s.to_string()).collect());
}
for id in &ready {
pending.remove(id);
}
for deps in pending.values_mut() {
for id in &ready {
deps.remove(id);
}
}
out.push(ready.into_iter().map(String::from).collect());
}
Ok(out)
}
#[allow(dead_code)]
pub fn dependents_transitive(graph: &Graph, id: &str) -> BTreeSet<String> {
let mut found: BTreeSet<String> = BTreeSet::new();
let mut frontier = vec![id.to_string()];
while let Some(cur) = frontier.pop() {
for (node, deps) in graph {
if deps.contains(&cur) && found.insert(node.clone()) {
frontier.push(node.clone());
}
}
}
found
}
#[derive(Debug, PartialEq, Eq)]
pub struct DepError {
pub code: &'static str,
pub message: String,
}
pub fn validate(
project_services: &[(String, String, Vec<String>)],
self_id: Option<&str>,
depends_on: &[String],
) -> Result<(), DepError> {
let name_of = |id: &str| {
project_services
.iter()
.find(|(i, _, _)| i == id)
.map(|(_, n, _)| n.clone())
.unwrap_or_else(|| id.to_string())
};
let mut seen = BTreeSet::new();
for dep in depends_on {
if Some(dep.as_str()) == self_id {
return Err(DepError {
code: "DependencyCycle",
message: "um serviço não pode depender de si mesmo".into(),
});
}
if !project_services.iter().any(|(id, _, _)| id == dep) {
return Err(DepError {
code: "InvalidDependency",
message: format!(
"a dependência '{dep}' não existe neste projeto (só se depende de serviços do mesmo projeto)"
),
});
}
if !seen.insert(dep) {
return Err(DepError {
code: "InvalidDependency",
message: format!("a dependência '{}' está repetida", name_of(dep)),
});
}
}
let mut graph: Graph = project_services
.iter()
.map(|(id, _, deps)| (id.clone(), deps.iter().cloned().collect()))
.collect();
let node = self_id.unwrap_or("\u{0}novo").to_string();
graph.insert(node, depends_on.iter().cloned().collect());
if let Err(cycle) = waves(&graph) {
let nomes: Vec<String> = cycle
.iter()
.filter(|id| id.as_str() != "\u{0}novo")
.map(|id| name_of(id))
.collect();
return Err(DepError {
code: "DependencyCycle",
message: format!(
"essa dependência cria um ciclo entre: {}",
if nomes.is_empty() {
"(o serviço novo)".into()
} else {
nomes.join(", ")
}
),
});
}
Ok(())
}
pub fn direct_dependents(
project_services: &[(String, String, Vec<String>)],
id: &str,
) -> Vec<(String, String)> {
project_services
.iter()
.filter(|(_, _, deps)| deps.iter().any(|d| d == id))
.map(|(i, n, _)| (i.clone(), n.clone()))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn g(edges: &[(&str, &[&str])]) -> Graph {
edges
.iter()
.map(|(k, v)| (k.to_string(), v.iter().map(|s| s.to_string()).collect()))
.collect()
}
fn svcs(edges: &[(&str, &[&str])]) -> Vec<(String, String, Vec<String>)> {
edges
.iter()
.map(|(k, v)| {
(
k.to_string(),
format!("nome-{k}"),
v.iter().map(|s| s.to_string()).collect(),
)
})
.collect()
}
#[test]
fn grafo_vazio_nao_tem_ondas() {
assert_eq!(waves(&Graph::new()).unwrap(), Vec::<Vec<String>>::new());
}
#[test]
fn independentes_ficam_todos_na_primeira_onda() {
let w = waves(&g(&[("a", &[]), ("b", &[]), ("c", &[])])).unwrap();
assert_eq!(w, vec![vec!["a", "b", "c"]]);
}
#[test]
fn banco_antes_da_api_antes_do_front() {
let w = waves(&g(&[("front", &["api"]), ("api", &["db"]), ("db", &[])])).unwrap();
assert_eq!(w, vec![vec!["db"], vec!["api"], vec!["front"]]);
}
#[test]
fn diamante_agrupa_em_ondas() {
let w = waves(&g(&[
("db", &[]),
("cache", &[]),
("api", &["db", "cache"]),
("worker", &["db"]),
("front", &["api"]),
]))
.unwrap();
assert_eq!(
w,
vec![vec!["cache", "db"], vec!["api", "worker"], vec!["front"]]
);
}
#[test]
fn dependencia_fora_do_grafo_e_ignorada() {
let w = waves(&g(&[("api", &["parado"])])).unwrap();
assert_eq!(w, vec![vec!["api"]]);
}
#[test]
fn ciclo_direto_indireto_e_auto_dependencia() {
assert!(waves(&g(&[("a", &["b"]), ("b", &["a"])])).is_err());
let e = waves(&g(&[
("a", &["b"]),
("b", &["c"]),
("c", &["a"]),
("ok", &[]),
]))
.unwrap_err();
assert_eq!(e, vec!["a", "b", "c"], "só os do ciclo, não o inocente");
assert!(waves(&g(&[("a", &["a"])])).is_err());
}
#[test]
fn dependentes_transitivos() {
let gr = g(&[
("db", &[]),
("api", &["db"]),
("front", &["api"]),
("outro", &[]),
]);
let d = dependents_transitive(&gr, "db");
assert_eq!(d.into_iter().collect::<Vec<_>>(), vec!["api", "front"]);
}
#[test]
fn validate_aceita_dependencia_valida() {
let s = svcs(&[("db", &[]), ("api", &[])]);
assert!(validate(&s, Some("api"), &["db".into()]).is_ok());
assert!(validate(&s, None, &["db".into(), "api".into()]).is_ok());
}
#[test]
fn validate_recusa_inexistente_repetida_e_a_si_mesmo() {
let s = svcs(&[("db", &[]), ("api", &[])]);
assert_eq!(
validate(&s, Some("api"), &["fantasma".into()])
.unwrap_err()
.code,
"InvalidDependency"
);
assert_eq!(
validate(&s, Some("api"), &["db".into(), "db".into()])
.unwrap_err()
.code,
"InvalidDependency"
);
assert_eq!(
validate(&s, Some("api"), &["api".into()]).unwrap_err().code,
"DependencyCycle"
);
}
#[test]
fn validate_recusa_ciclo_criado_pela_edicao() {
let s = svcs(&[("db", &["api"]), ("api", &[])]);
let e = validate(&s, Some("api"), &["db".into()]).unwrap_err();
assert_eq!(e.code, "DependencyCycle");
assert!(
e.message.contains("nome-db") && e.message.contains("nome-api"),
"{}",
e.message
);
}
#[test]
fn serviço_novo_nao_cria_ciclo_sozinho() {
let s = svcs(&[("db", &[])]);
assert!(validate(&s, None, &["db".into()]).is_ok());
}
#[test]
fn dependentes_diretos_para_a_guarda_de_exclusao() {
let s = svcs(&[("db", &[]), ("api", &["db"]), ("job", &["db"]), ("x", &[])]);
let d = direct_dependents(&s, "db");
assert_eq!(d.len(), 2);
assert!(direct_dependents(&s, "x").is_empty());
}
}