1#![allow(dead_code)]
3
4use std::collections::{HashMap, HashSet};
5
6use super::plugin_identifier::parse_plugin_identifier;
7use super::types::PluginId;
8
9const INLINE_MARKETPLACE: &str = "inline";
11
12pub fn qualify_dependency(dep: &str, declaring_plugin_id: &str) -> String {
14 let parsed = parse_plugin_identifier(dep);
15 if parsed.marketplace.is_some() {
16 return dep.to_string();
17 }
18 let declaring = parse_plugin_identifier(declaring_plugin_id);
19 match declaring.marketplace {
20 Some(ref m) if m == INLINE_MARKETPLACE => dep.to_string(),
21 None => dep.to_string(),
22 Some(ref mkt) => format!("{}@{}", dep, mkt),
23 }
24}
25
26pub struct DependencyLookupResult {
28 pub dependencies: Option<Vec<String>>,
29}
30
31pub enum ResolutionResult {
33 Ok {
34 closure: Vec<PluginId>,
35 },
36 Cycle {
37 chain: Vec<PluginId>,
38 },
39 NotFound {
40 missing: PluginId,
41 required_by: PluginId,
42 },
43 CrossMarketplace {
44 dependency: PluginId,
45 required_by: PluginId,
46 },
47}
48
49impl ResolutionResult {
50 pub fn is_ok(&self) -> bool {
51 matches!(self, ResolutionResult::Ok { .. })
52 }
53}
54
55pub async fn resolve_dependency_closure<F, Fut>(
57 root_id: &PluginId,
58 lookup: F,
59 already_enabled: &HashSet<PluginId>,
60 allowed_cross_marketplaces: &HashSet<String>,
61) -> ResolutionResult
62where
63 F: Fn(PluginId) -> Fut,
64 Fut: std::future::Future<Output = Option<DependencyLookupResult>>,
65{
66 let root_marketplace = parse_plugin_identifier(root_id).marketplace;
67 let mut closure: Vec<PluginId> = Vec::new();
68 let mut visited: HashSet<PluginId> = HashSet::new();
69 let mut stack: Vec<PluginId> = Vec::new();
70
71 async fn walk<F, Fut>(
72 id: PluginId,
73 required_by: PluginId,
74 root_id: &PluginId,
75 root_marketplace: Option<&str>,
76 already_enabled: &HashSet<PluginId>,
77 allowed_cross_marketplaces: &HashSet<String>,
78 visited: &mut HashSet<PluginId>,
79 stack: &mut Vec<PluginId>,
80 closure: &mut Vec<PluginId>,
81 lookup: &F,
82 ) -> Option<ResolutionResult>
83 where
84 F: Fn(PluginId) -> Fut,
85 Fut: std::future::Future<Output = Option<DependencyLookupResult>>,
86 {
87 if id != *root_id && already_enabled.contains(&id) {
89 return None;
90 }
91
92 let id_marketplace = parse_plugin_identifier(&id).marketplace;
94 if let (Some(id_mkt), Some(root_mkt)) = (id_marketplace.as_deref(), root_marketplace) {
95 if id_mkt != root_mkt && !allowed_cross_marketplaces.contains(id_mkt) {
96 return Some(ResolutionResult::CrossMarketplace {
97 dependency: id.clone(),
98 required_by,
99 });
100 }
101 }
102
103 if stack.contains(&id) {
104 return Some(ResolutionResult::Cycle {
105 chain: {
106 let mut c = stack.clone();
107 c.push(id.clone());
108 c
109 },
110 });
111 }
112
113 if visited.contains(&id) {
114 return None;
115 }
116 visited.insert(id.clone());
117
118 let entry = lookup(id.clone()).await;
119 let entry = match entry {
120 Some(e) => e,
121 None => {
122 return Some(ResolutionResult::NotFound {
123 missing: id,
124 required_by,
125 });
126 }
127 };
128
129 stack.push(id.clone());
130 for raw_dep in entry.dependencies.unwrap_or_default() {
131 let dep = qualify_dependency(&raw_dep, &id);
132 if let Some(err) = walk(
133 dep,
134 id.clone(),
135 root_id,
136 root_marketplace,
137 already_enabled,
138 allowed_cross_marketplaces,
139 visited,
140 stack,
141 closure,
142 lookup,
143 )
144 .await
145 {
146 return Some(err);
147 }
148 }
149 stack.pop();
150
151 closure.push(id);
152 None
153 }
154
155 let result = walk(
156 root_id.clone(),
157 root_id.clone(),
158 root_id,
159 root_marketplace.as_deref(),
160 already_enabled,
161 allowed_cross_marketplaces,
162 &mut visited,
163 &mut stack,
164 &mut closure,
165 &lookup,
166 )
167 .await;
168
169 match result {
170 Some(err) => err,
171 None => ResolutionResult::Ok { closure },
172 }
173}
174
175pub struct VerifyAndDemoteResult {
177 pub demoted: HashSet<String>,
178 pub errors: Vec<PluginError>,
179}
180
181pub enum PluginError {
183 DependencyUnsatisfied {
184 source: String,
185 plugin: String,
186 dependency: String,
187 reason: String, },
189 }
191
192pub struct LoadedPlugin {
193 pub source: String,
194 pub enabled: bool,
195 pub name: String,
196 pub manifest: PluginManifest,
197}
198
199pub struct PluginManifest {
200 pub dependencies: Option<Vec<String>>,
201}
202
203pub fn verify_and_demote(plugins: &[LoadedPlugin]) -> VerifyAndDemoteResult {
205 let known: HashSet<_> = plugins.iter().map(|p| p.source.clone()).collect();
206 let enabled: HashSet<_> = plugins
207 .iter()
208 .filter(|p| p.enabled)
209 .map(|p| p.source.clone())
210 .collect();
211
212 let known_by_name: HashSet<_> = plugins
213 .iter()
214 .map(|p| parse_plugin_identifier(&p.source).name.clone())
215 .collect();
216
217 let mut enabled_by_name: HashMap<String, i32> = HashMap::new();
218 for id in &enabled {
219 let n = parse_plugin_identifier(id).name;
220 *enabled_by_name.entry(n).or_insert(0) += 1;
221 }
222
223 let mut errors = Vec::new();
224 let mut current_enabled = enabled.clone();
225 let mut changed = true;
226
227 while changed {
228 changed = false;
229 for p in plugins {
230 if !current_enabled.contains(&p.source) {
231 continue;
232 }
233 for raw_dep in p.manifest.dependencies.iter().flatten() {
234 let dep = qualify_dependency(raw_dep, &p.source);
235 let is_bare = parse_plugin_identifier(&dep).marketplace.is_none();
236 let satisfied = if is_bare {
237 enabled_by_name.get(&dep).copied().unwrap_or(0) > 0
238 } else {
239 current_enabled.contains(&dep)
240 };
241
242 if !satisfied {
243 current_enabled.remove(&p.source);
244 let count = enabled_by_name.get(&p.name).copied().unwrap_or(0);
245 if count <= 1 {
246 enabled_by_name.remove(&p.name);
247 } else {
248 enabled_by_name.insert(p.name.clone(), count - 1);
249 }
250 errors.push(PluginError::DependencyUnsatisfied {
251 source: p.source.clone(),
252 plugin: p.name.clone(),
253 dependency: dep.clone(),
254 reason: if (is_bare && known_by_name.contains(&dep)) || known.contains(&dep)
255 {
256 "not-enabled".to_string()
257 } else {
258 "not-found".to_string()
259 },
260 });
261 changed = true;
262 break;
263 }
264 }
265 }
266 }
267
268 let demoted: HashSet<_> = plugins
269 .iter()
270 .filter(|p| p.enabled && !current_enabled.contains(&p.source))
271 .map(|p| p.source.clone())
272 .collect();
273
274 VerifyAndDemoteResult { demoted, errors }
275}
276
277pub fn find_reverse_dependents(plugin_id: &PluginId, plugins: &[LoadedPlugin]) -> Vec<String> {
279 let target_name = parse_plugin_identifier(plugin_id).name;
280 plugins
281 .iter()
282 .filter(|p| {
283 p.enabled
284 && p.source != *plugin_id
285 && p.manifest.dependencies.iter().flatten().any(|d| {
286 let qualified = qualify_dependency(d, &p.source);
287 if parse_plugin_identifier(&qualified).marketplace.is_some() {
288 qualified == *plugin_id
289 } else {
290 qualified == target_name
291 }
292 })
293 })
294 .map(|p| p.name.clone())
295 .collect()
296}
297
298pub fn format_dependency_count_suffix(installed_deps: &[String]) -> String {
300 if installed_deps.is_empty() {
301 return String::new();
302 }
303 let n = installed_deps.len();
304 format!(
305 " (+ {} {})",
306 n,
307 if n == 1 { "dependency" } else { "dependencies" }
308 )
309}
310
311pub fn format_reverse_dependents_suffix(rdeps: Option<&[String]>) -> String {
313 match rdeps {
314 Some(d) if !d.is_empty() => {
315 format!(" — warning: required by {}", d.join(", "))
316 }
317 _ => String::new(),
318 }
319}