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 kind: String,
20 pub spec: ContainerSpec,
22 pub outputs: ResourceOutputs,
25 pub depends_on: Vec<String>,
27}
28
29#[derive(Debug, Clone)]
31pub struct LifecyclePlan {
32 nodes: Vec<PlanNode>,
33 edges: HashMap<String, Vec<String>>,
34}
35
36impl LifecyclePlan {
37 pub fn from_manifest(manifest: &Manifest) -> Result<Self, LifecycleError> {
42 let project = manifest.project.name.as_str();
43
44 let mut resolved: HashMap<String, ResolvedResource> = HashMap::new();
46 let mut deps: HashMap<String, Vec<String>> = HashMap::new();
47 let mut kinds: HashMap<String, &'static str> = HashMap::new();
48 for (name, kind) in &manifest.resources {
49 let r =
50 from_resource(project, name, kind).map_err(|source| LifecycleError::SpecBuild {
51 resource: name.clone(),
52 source,
53 })?;
54 resolved.insert(name.clone(), r);
55 deps.insert(name.clone(), kind.depends_on().to_vec());
56 kinds.insert(name.clone(), kind.kind_name());
57 }
58
59 for (name, dependencies) in &deps {
61 for dependency in dependencies {
62 if !resolved.contains_key(dependency) {
63 return Err(LifecycleError::ResourceNotFound(format!(
64 "`{dependency}` (depended on by `{name}`)"
65 )));
66 }
67 }
68 }
69
70 let mut in_degree: HashMap<String, usize> = resolved
72 .keys()
73 .map(|name| (name.clone(), 0_usize))
74 .collect();
75 for dependencies in deps.values() {
76 for dependency in dependencies {
77 *in_degree.entry(dependency.clone()).or_insert(0) += 1;
78 }
79 }
80
81 let mut reverse: HashMap<String, Vec<String>> = HashMap::new();
83 for (name, dependencies) in &deps {
84 for dependency in dependencies {
85 reverse
86 .entry(dependency.clone())
87 .or_default()
88 .push(name.clone());
89 }
90 }
91
92 let mut in_count: HashMap<String, usize> = resolved
101 .keys()
102 .map(|name| (name.clone(), deps.get(name).map_or(0, Vec::len)))
103 .collect();
104
105 let mut ready: Vec<String> = in_count
106 .iter()
107 .filter(|(_, count)| **count == 0)
108 .map(|(name, _)| name.clone())
109 .collect();
110 ready.sort();
112
113 let mut sorted: Vec<String> = Vec::with_capacity(resolved.len());
114 while let Some(node) = ready.pop() {
115 sorted.push(node.clone());
116 if let Some(dependents) = reverse.get(&node) {
117 let mut newly_ready: Vec<String> = Vec::new();
118 for dependent in dependents {
119 let count = in_count.get_mut(dependent).expect("dependent indexed");
120 *count -= 1;
121 if *count == 0 {
122 newly_ready.push(dependent.clone());
123 }
124 }
125 newly_ready.sort();
126 ready.extend(newly_ready);
127 }
128 }
129
130 if sorted.len() != resolved.len() {
131 let unresolved: Vec<&String> = resolved
132 .keys()
133 .filter(|name| !sorted.contains(name))
134 .collect();
135 return Err(LifecycleError::Cycle(format!(
136 "{unresolved:?} involved in a cycle"
137 )));
138 }
139
140 let _ = in_degree; let _ = HashSet::<&str>::new();
142
143 let edges = deps.clone();
145
146 let nodes: Vec<PlanNode> = sorted
147 .into_iter()
148 .map(|name| {
149 let ResolvedResource { spec, outputs } =
150 resolved.remove(&name).expect("spec indexed by name");
151 let dependencies = deps.remove(&name).unwrap_or_default();
152 let kind = kinds
153 .remove(&name)
154 .expect("kind indexed by name")
155 .to_owned();
156 PlanNode {
157 name,
158 kind,
159 spec,
160 outputs,
161 depends_on: dependencies,
162 }
163 })
164 .collect();
165
166 Ok(Self { nodes, edges })
167 }
168
169 #[must_use]
171 pub fn nodes(&self) -> &[PlanNode] {
172 &self.nodes
173 }
174
175 #[must_use]
177 pub fn dependents_of(&self, name: &str) -> Vec<&str> {
178 let mut out: Vec<&str> = Vec::new();
179 for (resource, deps) in &self.edges {
180 if deps.iter().any(|d| d == name) {
181 out.push(resource.as_str());
182 }
183 }
184 out.sort_unstable();
185 out
186 }
187}