ontocore_plugin/
lifecycle.rs1use crate::manifest::DiscoveredPlugin;
4use std::collections::{HashMap, HashSet, VecDeque};
5use thiserror::Error;
6
7#[derive(Debug, Error, PartialEq, Eq)]
8pub enum LifecycleError {
9 #[error("plugin dependency cycle involving: {0}")]
10 Cycle(String),
11 #[error("plugin {0} depends on missing plugin {1}")]
12 MissingDependency(String, String),
13}
14
15pub fn activation_order(plugins: &[DiscoveredPlugin]) -> Result<Vec<String>, LifecycleError> {
19 let ids: HashSet<String> = plugins.iter().map(|p| p.plugin_id().to_string()).collect();
20 let mut deps: HashMap<String, Vec<String>> = HashMap::new();
21 let mut indegree: HashMap<String, usize> = HashMap::new();
22
23 for p in plugins {
24 let id = p.plugin_id().to_string();
25 indegree.entry(id.clone()).or_insert(0);
26 let mut unique_deps = HashSet::new();
27 for dep in &p.manifest.depends_on {
28 if !ids.contains(dep) {
29 return Err(LifecycleError::MissingDependency(id, dep.clone()));
30 }
31 if unique_deps.insert(dep.clone()) {
32 deps.entry(dep.clone()).or_default().push(id.clone());
33 *indegree.entry(id.clone()).or_insert(0) += 1;
34 }
35 }
36 }
37
38 let mut queue: VecDeque<String> =
40 indegree.iter().filter(|(_, d)| **d == 0).map(|(id, _)| id.clone()).collect();
41 let mut q_vec: Vec<_> = queue.drain(..).collect();
42 q_vec.sort();
43 queue.extend(q_vec);
44
45 let mut order = Vec::new();
46 while let Some(id) = queue.pop_front() {
47 order.push(id.clone());
48 if let Some(dependents) = deps.get(&id) {
49 let mut next = Vec::new();
50 for dep in dependents {
51 if let Some(d) = indegree.get_mut(dep) {
52 *d = d.saturating_sub(1);
53 if *d == 0 {
54 next.push(dep.clone());
55 }
56 }
57 }
58 next.sort();
59 queue.extend(next);
60 }
61 }
62
63 if order.len() != plugins.len() {
64 let leftover: Vec<_> =
65 indegree.into_iter().filter(|(_, d)| *d > 0).map(|(id, _)| id).collect();
66 return Err(LifecycleError::Cycle(leftover.join(", ")));
67 }
68 Ok(order)
69}
70
71pub fn dependents_of(plugins: &[DiscoveredPlugin], plugin_id: &str) -> Vec<String> {
73 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
74 for p in plugins {
75 let id = p.plugin_id().to_string();
76 for dep in &p.manifest.depends_on {
77 graph.entry(dep.clone()).or_default().push(id.clone());
78 }
79 }
80 let mut out = Vec::new();
81 let mut stack = vec![plugin_id.to_string()];
82 let mut seen = HashSet::new();
83 while let Some(cur) = stack.pop() {
84 if let Some(deps) = graph.get(&cur) {
85 for d in deps {
86 if seen.insert(d.clone()) {
87 out.push(d.clone());
88 stack.push(d.clone());
89 }
90 }
91 }
92 }
93 out.sort();
94 out
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use crate::manifest::parse_manifest;
101 use std::path::PathBuf;
102
103 fn plugin(toml: &str) -> DiscoveredPlugin {
104 DiscoveredPlugin {
105 manifest: parse_manifest(toml).expect("manifest"),
106 manifest_path: PathBuf::from("/tmp/.ontocore/plugins/x.toml"),
107 }
108 }
109
110 #[test]
111 fn orders_dependencies() {
112 let a = plugin(
113 r#"
114[plugin]
115name = "a"
116version = "0.1.0"
117kind = "validator"
118id = "a"
119permissions = ["workspace.read"]
120"#,
121 );
122 let b = plugin(
123 r#"
124[plugin]
125name = "b"
126version = "0.1.0"
127kind = "graph"
128id = "b"
129depends_on = ["a"]
130permissions = ["workspace.read"]
131"#,
132 );
133 let order = activation_order(&[b, a]).expect("order");
134 assert_eq!(order, vec!["a".to_string(), "b".to_string()]);
135 }
136
137 #[test]
138 fn detects_cycle() {
139 let a = plugin(
140 r#"
141[plugin]
142name = "a"
143version = "0.1.0"
144kind = "validator"
145id = "a"
146depends_on = ["b"]
147permissions = ["workspace.read"]
148"#,
149 );
150 let b = plugin(
151 r#"
152[plugin]
153name = "b"
154version = "0.1.0"
155kind = "validator"
156id = "b"
157depends_on = ["a"]
158permissions = ["workspace.read"]
159"#,
160 );
161 assert!(matches!(activation_order(&[a, b]), Err(LifecycleError::Cycle(_))));
162 }
163
164 #[test]
165 fn missing_dependency_errors() {
166 let b = plugin(
167 r#"
168[plugin]
169name = "b"
170version = "0.1.0"
171kind = "graph"
172id = "b"
173depends_on = ["missing"]
174permissions = ["workspace.read"]
175"#,
176 );
177 assert!(matches!(activation_order(&[b]), Err(LifecycleError::MissingDependency(_, _))));
178 }
179}