use std::collections::{HashMap, HashSet};
use lightshuttle_manifest::Manifest;
use crate::lifecycle::error::LifecycleError;
use crate::spec::{ContainerSpec, ResolvedResource, ResourceOutputs, from_resource};
#[derive(Debug, Clone)]
pub struct PlanNode {
pub name: String,
pub spec: ContainerSpec,
pub outputs: ResourceOutputs,
pub depends_on: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct LifecyclePlan {
nodes: Vec<PlanNode>,
edges: HashMap<String, Vec<String>>,
}
impl LifecyclePlan {
pub fn from_manifest(manifest: &Manifest) -> Result<Self, LifecycleError> {
let project = manifest.project.name.as_str();
let mut resolved: HashMap<String, ResolvedResource> = HashMap::new();
let mut deps: HashMap<String, Vec<String>> = HashMap::new();
for (name, kind) in &manifest.resources {
let r =
from_resource(project, name, kind).map_err(|source| LifecycleError::SpecBuild {
resource: name.clone(),
source,
})?;
resolved.insert(name.clone(), r);
deps.insert(name.clone(), kind.depends_on().to_vec());
}
for (name, dependencies) in &deps {
for dependency in dependencies {
if !resolved.contains_key(dependency) {
return Err(LifecycleError::UnknownResource(format!(
"`{dependency}` (depended on by `{name}`)"
)));
}
}
}
let mut in_degree: HashMap<String, usize> = resolved
.keys()
.map(|name| (name.clone(), 0_usize))
.collect();
for dependencies in deps.values() {
for dependency in dependencies {
*in_degree.entry(dependency.clone()).or_insert(0) += 1;
}
}
let mut reverse: HashMap<String, Vec<String>> = HashMap::new();
for (name, dependencies) in &deps {
for dependency in dependencies {
reverse
.entry(dependency.clone())
.or_default()
.push(name.clone());
}
}
let mut in_count: HashMap<String, usize> = resolved
.keys()
.map(|name| (name.clone(), deps.get(name).map_or(0, Vec::len)))
.collect();
let mut ready: Vec<String> = in_count
.iter()
.filter(|(_, count)| **count == 0)
.map(|(name, _)| name.clone())
.collect();
ready.sort();
let mut sorted: Vec<String> = Vec::with_capacity(resolved.len());
while let Some(node) = ready.pop() {
sorted.push(node.clone());
if let Some(dependents) = reverse.get(&node) {
let mut newly_ready: Vec<String> = Vec::new();
for dependent in dependents {
let count = in_count.get_mut(dependent).expect("dependent indexed");
*count -= 1;
if *count == 0 {
newly_ready.push(dependent.clone());
}
}
newly_ready.sort();
ready.extend(newly_ready);
}
}
if sorted.len() != resolved.len() {
let unresolved: Vec<&String> = resolved
.keys()
.filter(|name| !sorted.contains(name))
.collect();
return Err(LifecycleError::Cycle(format!(
"{unresolved:?} involved in a cycle"
)));
}
let _ = in_degree; let _ = HashSet::<&str>::new();
let edges = deps.clone();
let nodes: Vec<PlanNode> = sorted
.into_iter()
.map(|name| {
let ResolvedResource { spec, outputs } =
resolved.remove(&name).expect("spec indexed by name");
let dependencies = deps.remove(&name).unwrap_or_default();
PlanNode {
name,
spec,
outputs,
depends_on: dependencies,
}
})
.collect();
Ok(Self { nodes, edges })
}
#[must_use]
pub fn nodes(&self) -> &[PlanNode] {
&self.nodes
}
#[must_use]
pub fn dependents_of(&self, name: &str) -> Vec<&str> {
let mut out: Vec<&str> = Vec::new();
for (resource, deps) in &self.edges {
if deps.iter().any(|d| d == name) {
out.push(resource.as_str());
}
}
out.sort_unstable();
out
}
}