mockforge_plugin_registry/
dependencies.rs

1//! Plugin dependency resolution and management
2
3use crate::{RegistryError, Result, VersionEntry};
4use semver::{Version, VersionReq};
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, HashSet, VecDeque};
7
8/// Dependency specification
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Dependency {
11    /// Package name
12    pub name: String,
13
14    /// Version requirement (semver)
15    pub version_req: String,
16
17    /// Optional dependency
18    pub optional: bool,
19
20    /// Features to enable
21    pub features: Vec<String>,
22
23    /// Registry source
24    pub source: DependencySource,
25}
26
27/// Dependency source
28#[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/// Resolved dependency
43#[derive(Debug, Clone)]
44pub struct ResolvedDependency {
45    pub name: String,
46    pub version: Version,
47    pub dependencies: Vec<Dependency>,
48}
49
50/// Dependency graph node
51#[allow(dead_code)]
52#[derive(Debug, Clone)]
53struct DependencyNode {
54    name: String,
55    version: Version,
56    dependencies: Vec<String>, // List of dependent package names
57}
58
59/// Dependency resolver
60pub struct DependencyResolver {
61    /// Available package versions
62    available_versions: HashMap<String, Vec<VersionEntry>>,
63}
64
65impl DependencyResolver {
66    /// Create a new dependency resolver
67    pub fn new() -> Self {
68        Self {
69            available_versions: HashMap::new(),
70        }
71    }
72
73    /// Add available versions for a package
74    pub fn add_package_versions(&mut self, name: String, versions: Vec<VersionEntry>) {
75        self.available_versions.insert(name, versions);
76    }
77
78    /// Resolve dependencies for a package
79    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        // Start with root dependencies
90        queue.push_back((root_package.to_string(), dependencies));
91
92        while let Some((_parent, deps)) = queue.pop_front() {
93            for dep in deps {
94                // Skip if already resolved
95                if visited.contains(&dep.name) {
96                    continue;
97                }
98
99                // Parse version requirement
100                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                // Find compatible version
108                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                // Get version entry
117                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                // Parse transitive dependencies
126                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                // Add to resolved
139                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                // Queue transitive dependencies
148                if !transitive_deps.is_empty() {
149                    queue.push_back((dep.name.clone(), transitive_deps));
150                }
151            }
152        }
153
154        // Check for circular dependencies
155        self.check_circular_dependencies(&resolved)?;
156
157        Ok(resolved)
158    }
159
160    /// Find a compatible version for a package
161    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        // Filter out yanked versions and parse semver
172        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        // Sort by version (highest first)
180        compatible_versions.sort();
181        compatible_versions.reverse();
182
183        Ok(compatible_versions.first().cloned())
184    }
185
186    /// Get version entry for a specific version
187    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    /// Check for circular dependencies
195    fn check_circular_dependencies(&self, resolved: &[ResolvedDependency]) -> Result<()> {
196        let mut graph: HashMap<String, Vec<String>> = HashMap::new();
197
198        // Build adjacency list
199        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        // DFS to detect cycles
205        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    /// Check if there's a cycle starting from a node
221    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    /// Calculate installation order (topological sort)
251    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        // Build graph
256        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        // Kahn's algorithm for topological sort
267        let mut queue: VecDeque<String> = in_degree
268            .iter()
269            .filter(|(_, &degree)| 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        // Check if all nodes are in the order (no cycles)
291        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        // Reverse to get correct install order (dependencies first)
298        order.reverse();
299
300        Ok(order)
301    }
302}
303
304impl Default for DependencyResolver {
305    fn default() -> Self {
306        Self::new()
307    }
308}
309
310/// Dependency conflict
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct DependencyConflict {
313    pub package: String,
314    pub required_by: Vec<ConflictRequirement>,
315}
316
317/// Conflict requirement
318#[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        // Add package A with versions
333        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        // Create circular dependency: A -> B -> A
382        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}