mockforge_plugin_registry/
dependencies.rs1use crate::{RegistryError, Result, VersionEntry};
4use semver::{Version, VersionReq};
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet, VecDeque};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Dependency {
11 pub name: String,
13
14 pub version_req: String,
16
17 pub optional: bool,
19
20 pub features: Vec<String>,
22
23 pub source: DependencySource,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum DependencySource {
31 Registry,
32 Git { url: String, rev: Option<String> },
33 Path { path: String },
34}
35
36impl Default for DependencySource {
37 fn default() -> Self {
38 Self::Registry
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct ResolvedDependency {
45 pub name: String,
46 pub version: Version,
47 pub dependencies: Vec<Dependency>,
48}
49
50#[allow(dead_code)]
52#[derive(Debug, Clone)]
53struct DependencyNode {
54 name: String,
55 version: Version,
56 dependencies: Vec<String>, }
58
59pub struct DependencyResolver {
61 available_versions: HashMap<String, Vec<VersionEntry>>,
63}
64
65impl DependencyResolver {
66 pub fn new() -> Self {
68 Self {
69 available_versions: HashMap::new(),
70 }
71 }
72
73 pub fn add_package_versions(&mut self, name: String, versions: Vec<VersionEntry>) {
75 self.available_versions.insert(name, versions);
76 }
77
78 pub fn resolve(
80 &self,
81 root_package: &str,
82 _root_version: &Version,
83 dependencies: Vec<Dependency>,
84 ) -> Result<Vec<ResolvedDependency>> {
85 let mut resolved = Vec::new();
86 let mut visited = HashSet::new();
87 let mut queue = VecDeque::new();
88
89 queue.push_back((root_package.to_string(), dependencies));
91
92 while let Some((_parent, deps)) = queue.pop_front() {
93 for dep in deps {
94 if visited.contains(&dep.name) {
96 continue;
97 }
98
99 let version_req = VersionReq::parse(&dep.version_req).map_err(|e| {
101 RegistryError::InvalidVersion(format!(
102 "Invalid version requirement '{}': {}",
103 dep.version_req, e
104 ))
105 })?;
106
107 let compatible_version =
109 self.find_compatible_version(&dep.name, &version_req)?.ok_or_else(|| {
110 RegistryError::InvalidVersion(format!(
111 "No compatible version found for {} with requirement {}",
112 dep.name, dep.version_req
113 ))
114 })?;
115
116 let version_entry =
118 self.get_version_entry(&dep.name, &compatible_version).ok_or_else(|| {
119 RegistryError::PluginNotFound(format!(
120 "{} {}",
121 dep.name, compatible_version
122 ))
123 })?;
124
125 let transitive_deps: Vec<Dependency> = version_entry
127 .dependencies
128 .iter()
129 .map(|(name, version_req)| Dependency {
130 name: name.clone(),
131 version_req: version_req.clone(),
132 optional: false,
133 features: vec![],
134 source: DependencySource::Registry,
135 })
136 .collect();
137
138 resolved.push(ResolvedDependency {
140 name: dep.name.clone(),
141 version: compatible_version.clone(),
142 dependencies: transitive_deps.clone(),
143 });
144
145 visited.insert(dep.name.clone());
146
147 if !transitive_deps.is_empty() {
149 queue.push_back((dep.name.clone(), transitive_deps));
150 }
151 }
152 }
153
154 self.check_circular_dependencies(&resolved)?;
156
157 Ok(resolved)
158 }
159
160 fn find_compatible_version(
162 &self,
163 package: &str,
164 version_req: &VersionReq,
165 ) -> Result<Option<Version>> {
166 let versions = self
167 .available_versions
168 .get(package)
169 .ok_or_else(|| RegistryError::PluginNotFound(package.to_string()))?;
170
171 let mut compatible_versions: Vec<Version> = versions
173 .iter()
174 .filter(|v| !v.yanked)
175 .filter_map(|v| Version::parse(&v.version).ok())
176 .filter(|v| version_req.matches(v))
177 .collect();
178
179 compatible_versions.sort();
181 compatible_versions.reverse();
182
183 Ok(compatible_versions.first().cloned())
184 }
185
186 fn get_version_entry(&self, package: &str, version: &Version) -> Option<&VersionEntry> {
188 self.available_versions
189 .get(package)?
190 .iter()
191 .find(|v| Version::parse(&v.version).ok().map(|v| &v == version).unwrap_or(false))
192 }
193
194 fn check_circular_dependencies(&self, resolved: &[ResolvedDependency]) -> Result<()> {
196 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
197
198 for dep in resolved {
200 let deps: Vec<String> = dep.dependencies.iter().map(|d| d.name.clone()).collect();
201 graph.insert(dep.name.clone(), deps);
202 }
203
204 let mut visited = HashSet::new();
206 let mut rec_stack = HashSet::new();
207
208 for node in graph.keys() {
209 if Self::has_cycle_impl(&graph, node, &mut visited, &mut rec_stack) {
210 return Err(RegistryError::InvalidManifest(format!(
211 "Circular dependency detected involving package: {}",
212 node
213 )));
214 }
215 }
216
217 Ok(())
218 }
219
220 fn has_cycle_impl(
222 graph: &HashMap<String, Vec<String>>,
223 node: &str,
224 visited: &mut HashSet<String>,
225 rec_stack: &mut HashSet<String>,
226 ) -> bool {
227 if rec_stack.contains(node) {
228 return true;
229 }
230
231 if visited.contains(node) {
232 return false;
233 }
234
235 visited.insert(node.to_string());
236 rec_stack.insert(node.to_string());
237
238 if let Some(neighbors) = graph.get(node) {
239 for neighbor in neighbors {
240 if Self::has_cycle_impl(graph, neighbor, visited, rec_stack) {
241 return true;
242 }
243 }
244 }
245
246 rec_stack.remove(node);
247 false
248 }
249
250 pub fn calculate_install_order(&self, resolved: &[ResolvedDependency]) -> Result<Vec<String>> {
252 let mut graph: HashMap<String, Vec<String>> = HashMap::new();
253 let mut in_degree: HashMap<String, usize> = HashMap::new();
254
255 for dep in resolved {
257 in_degree.entry(dep.name.clone()).or_insert(0);
258
259 for child_dep in &dep.dependencies {
260 graph.entry(dep.name.clone()).or_default().push(child_dep.name.clone());
261
262 *in_degree.entry(child_dep.name.clone()).or_insert(0) += 1;
263 }
264 }
265
266 let mut queue: VecDeque<String> = in_degree
268 .iter()
269 .filter(|(_, °ree)| degree == 0)
270 .map(|(name, _)| name.clone())
271 .collect();
272
273 let mut order = Vec::new();
274
275 while let Some(node) = queue.pop_front() {
276 order.push(node.clone());
277
278 if let Some(neighbors) = graph.get(&node) {
279 for neighbor in neighbors {
280 if let Some(degree) = in_degree.get_mut(neighbor) {
281 *degree -= 1;
282 if *degree == 0 {
283 queue.push_back(neighbor.clone());
284 }
285 }
286 }
287 }
288 }
289
290 if order.len() != in_degree.len() {
292 return Err(RegistryError::InvalidManifest(
293 "Circular dependency detected during install order calculation".to_string(),
294 ));
295 }
296
297 order.reverse();
299
300 Ok(order)
301 }
302}
303
304impl Default for DependencyResolver {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct DependencyConflict {
313 pub package: String,
314 pub required_by: Vec<ConflictRequirement>,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct ConflictRequirement {
320 pub package: String,
321 pub version_req: String,
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327
328 #[test]
329 fn test_dependency_resolution() {
330 let mut resolver = DependencyResolver::new();
331
332 resolver.add_package_versions(
334 "package-a".to_string(),
335 vec![
336 VersionEntry {
337 version: "1.0.0".to_string(),
338 download_url: "https://example.com/a-1.0.0".to_string(),
339 checksum: "abc".to_string(),
340 size: 1000,
341 published_at: "2025-01-01".to_string(),
342 yanked: false,
343 min_mockforge_version: None,
344 dependencies: HashMap::new(),
345 },
346 VersionEntry {
347 version: "1.1.0".to_string(),
348 download_url: "https://example.com/a-1.1.0".to_string(),
349 checksum: "def".to_string(),
350 size: 1100,
351 published_at: "2025-01-02".to_string(),
352 yanked: false,
353 min_mockforge_version: None,
354 dependencies: HashMap::new(),
355 },
356 ],
357 );
358
359 let deps = vec![Dependency {
360 name: "package-a".to_string(),
361 version_req: "^1.0".to_string(),
362 optional: false,
363 features: vec![],
364 source: DependencySource::Registry,
365 }];
366
367 let root_version = Version::parse("1.0.0").unwrap();
368 let resolved = resolver.resolve("root", &root_version, deps);
369
370 assert!(resolved.is_ok());
371 let resolved = resolved.unwrap();
372 assert_eq!(resolved.len(), 1);
373 assert_eq!(resolved[0].name, "package-a");
374 assert_eq!(resolved[0].version, Version::parse("1.1.0").unwrap());
375 }
376
377 #[test]
378 fn test_circular_dependency_detection() {
379 let resolver = DependencyResolver::new();
380
381 let resolved = vec![
383 ResolvedDependency {
384 name: "package-a".to_string(),
385 version: Version::parse("1.0.0").unwrap(),
386 dependencies: vec![Dependency {
387 name: "package-b".to_string(),
388 version_req: "1.0".to_string(),
389 optional: false,
390 features: vec![],
391 source: DependencySource::Registry,
392 }],
393 },
394 ResolvedDependency {
395 name: "package-b".to_string(),
396 version: Version::parse("1.0.0").unwrap(),
397 dependencies: vec![Dependency {
398 name: "package-a".to_string(),
399 version_req: "1.0".to_string(),
400 optional: false,
401 features: vec![],
402 source: DependencySource::Registry,
403 }],
404 },
405 ];
406
407 let result = resolver.check_circular_dependencies(&resolved);
408 assert!(result.is_err());
409 }
410}