lightshuttle_runtime/lifecycle/
plan.rs1use std::collections::{HashMap, HashSet};
5
6use lightshuttle_manifest::Manifest;
7
8use crate::lifecycle::error::LifecycleError;
9use crate::spec::{ContainerSpec, ResolvedResource, ResourceOutputs, from_resource};
10
11#[derive(Debug, Clone)]
14pub struct PlanNode {
15 pub name: String,
17 pub spec: ContainerSpec,
19 pub outputs: ResourceOutputs,
22 pub depends_on: Vec<String>,
24}
25
26#[derive(Debug, Clone)]
28pub struct LifecyclePlan {
29 nodes: Vec<PlanNode>,
30 edges: HashMap<String, Vec<String>>,
31}
32
33impl LifecyclePlan {
34 pub fn from_manifest(manifest: &Manifest) -> Result<Self, LifecycleError> {
39 let project = manifest.project.name.as_str();
40
41 let mut resolved: HashMap<String, ResolvedResource> = HashMap::new();
43 let mut deps: HashMap<String, Vec<String>> = HashMap::new();
44 for (name, kind) in &manifest.resources {
45 let r =
46 from_resource(project, name, kind).map_err(|source| LifecycleError::SpecBuild {
47 resource: name.clone(),
48 source,
49 })?;
50 resolved.insert(name.clone(), r);
51 deps.insert(name.clone(), kind.depends_on().to_vec());
52 }
53
54 for (name, dependencies) in &deps {
56 for dependency in dependencies {
57 if !resolved.contains_key(dependency) {
58 return Err(LifecycleError::UnknownResource(format!(
59 "`{dependency}` (depended on by `{name}`)"
60 )));
61 }
62 }
63 }
64
65 let mut in_degree: HashMap<String, usize> = resolved
67 .keys()
68 .map(|name| (name.clone(), 0_usize))
69 .collect();
70 for dependencies in deps.values() {
71 for dependency in dependencies {
72 *in_degree.entry(dependency.clone()).or_insert(0) += 1;
73 }
74 }
75
76 let mut reverse: HashMap<String, Vec<String>> = HashMap::new();
78 for (name, dependencies) in &deps {
79 for dependency in dependencies {
80 reverse
81 .entry(dependency.clone())
82 .or_default()
83 .push(name.clone());
84 }
85 }
86
87 let mut in_count: HashMap<String, usize> = resolved
96 .keys()
97 .map(|name| (name.clone(), deps.get(name).map_or(0, Vec::len)))
98 .collect();
99
100 let mut ready: Vec<String> = in_count
101 .iter()
102 .filter(|(_, count)| **count == 0)
103 .map(|(name, _)| name.clone())
104 .collect();
105 ready.sort();
107
108 let mut sorted: Vec<String> = Vec::with_capacity(resolved.len());
109 while let Some(node) = ready.pop() {
110 sorted.push(node.clone());
111 if let Some(dependents) = reverse.get(&node) {
112 let mut newly_ready: Vec<String> = Vec::new();
113 for dependent in dependents {
114 let count = in_count.get_mut(dependent).expect("dependent indexed");
115 *count -= 1;
116 if *count == 0 {
117 newly_ready.push(dependent.clone());
118 }
119 }
120 newly_ready.sort();
121 ready.extend(newly_ready);
122 }
123 }
124
125 if sorted.len() != resolved.len() {
126 let unresolved: Vec<&String> = resolved
127 .keys()
128 .filter(|name| !sorted.contains(name))
129 .collect();
130 return Err(LifecycleError::Cycle(format!(
131 "{unresolved:?} involved in a cycle"
132 )));
133 }
134
135 let _ = in_degree; let _ = HashSet::<&str>::new();
137
138 let edges = deps.clone();
140
141 let nodes: Vec<PlanNode> = sorted
142 .into_iter()
143 .map(|name| {
144 let ResolvedResource { spec, outputs } =
145 resolved.remove(&name).expect("spec indexed by name");
146 let dependencies = deps.remove(&name).unwrap_or_default();
147 PlanNode {
148 name,
149 spec,
150 outputs,
151 depends_on: dependencies,
152 }
153 })
154 .collect();
155
156 Ok(Self { nodes, edges })
157 }
158
159 #[must_use]
161 pub fn nodes(&self) -> &[PlanNode] {
162 &self.nodes
163 }
164
165 #[must_use]
167 pub fn dependents_of(&self, name: &str) -> Vec<&str> {
168 let mut out: Vec<&str> = Vec::new();
169 for (resource, deps) in &self.edges {
170 if deps.iter().any(|d| d == name) {
171 out.push(resource.as_str());
172 }
173 }
174 out.sort_unstable();
175 out
176 }
177}