agpm_cli/resolver/
conflict_service.rs

1//! Conflict detection service for version and path conflicts.
2//!
3//! This module provides high-level orchestration for conflict detection,
4//! wrapping the lower-level ConflictDetector functionality.
5
6use anyhow::Result;
7use std::collections::HashMap;
8
9use crate::core::ResourceType;
10use crate::manifest::{DetailedDependency, ResourceDependency};
11use crate::version::conflict::{ConflictDetector, VersionConflict};
12
13use super::types::DependencyKey;
14
15/// Conflict detection service.
16///
17/// This service wraps the ConflictDetector and provides high-level methods
18/// for detecting version conflicts and path conflicts in dependencies.
19#[allow(dead_code)] // detector field not yet used in service-based refactoring
20pub struct ConflictService {
21    detector: ConflictDetector,
22}
23
24impl ConflictService {
25    /// Create a new conflict service.
26    pub fn new() -> Self {
27        Self {
28            detector: ConflictDetector::new(),
29        }
30    }
31
32    /// Detect version conflicts in the provided dependencies.
33    ///
34    /// # Arguments
35    ///
36    /// * `dependencies` - Map of dependencies by (type, name) key
37    ///
38    /// # Returns
39    ///
40    /// A vector of detected version conflicts
41    pub fn detect_version_conflicts(
42        &mut self,
43        dependencies: &HashMap<DependencyKey, DetailedDependency>,
44    ) -> Result<Vec<VersionConflict>> {
45        let mut conflicts = Vec::new();
46
47        // Group dependencies by (type, path, source, tool) to find version conflicts
48        let mut grouped: HashMap<(ResourceType, String, String, String), Vec<_>> = HashMap::new();
49
50        for (key, dep) in dependencies {
51            let source = dep.source.clone().unwrap_or_default();
52            let tool = dep.tool.clone().unwrap_or_default();
53
54            let group_key = (
55                key.0, // resource_type
56                dep.path.clone(),
57                source,
58                tool,
59            );
60            grouped.entry(group_key).or_default().push((key, dep));
61        }
62
63        // Check each group for version conflicts
64        for ((resource_type, path, source, _tool), deps) in grouped {
65            if deps.len() > 1 {
66                // Multiple versions of the same resource
67                let mut conflicting_requirements = Vec::new();
68
69                for (key, dep) in &deps {
70                    let requirement = dep.version.clone().unwrap_or_else(|| "latest".to_string());
71
72                    conflicting_requirements.push(
73                        crate::version::conflict::ConflictingRequirement {
74                            required_by: format!("{}/{}", key.0, key.1), // resource_type, name
75                            requirement,
76                            resolved_version: None,
77                        },
78                    );
79                }
80
81                conflicts.push(VersionConflict {
82                    resource: format!("{}/{}/{}", resource_type, path, source),
83                    conflicting_requirements,
84                });
85            }
86        }
87
88        Ok(conflicts)
89    }
90
91    /// Detect path conflicts in the provided dependencies.
92    ///
93    /// # Arguments
94    ///
95    /// * `dependencies` - Map of dependencies by (type, name) key
96    ///
97    /// # Returns
98    ///
99    /// A vector of detected path conflicts
100    pub fn detect_path_conflicts(
101        dependencies: &HashMap<DependencyKey, DetailedDependency>,
102    ) -> Vec<(String, Vec<String>)> {
103        let mut conflicts = Vec::new();
104        let mut install_paths: HashMap<String, Vec<String>> = HashMap::new();
105
106        // Group dependencies by install path
107        for (key, dep) in dependencies {
108            let install_path = format!("{}/{}", key.0, key.1); // resource_type, name
109            install_paths.entry(install_path.clone()).or_default().push(format!(
110                "{}/{} (from {})",
111                key.0,
112                key.1,
113                dep.source.as_deref().unwrap_or("local")
114            ));
115        }
116
117        // Find paths with multiple dependencies
118        for (path, deps) in install_paths {
119            if deps.len() > 1 {
120                conflicts.push((path, deps));
121            }
122        }
123
124        conflicts
125    }
126
127    /// Check if a dependency conflicts with existing dependencies.
128    ///
129    /// # Arguments
130    ///
131    /// * `dependencies` - Existing dependencies
132    /// * `new_dep` - New dependency to check
133    /// * `new_key` - Key for the new dependency
134    ///
135    /// # Returns
136    ///
137    /// True if there's a conflict, false otherwise
138    pub fn has_conflict(
139        &mut self,
140        dependencies: &HashMap<DependencyKey, DetailedDependency>,
141        new_dep: &ResourceDependency,
142        new_key: &DependencyKey,
143    ) -> bool {
144        // For ResourceDependency, we need to extract the path and source info
145        let (new_path, new_source, new_tool) = match new_dep {
146            ResourceDependency::Simple(path) => (path, None, None),
147            ResourceDependency::Detailed(details) => {
148                (&details.path, details.source.as_deref(), details.tool.as_deref())
149            }
150        };
151
152        // Check for version conflicts
153        for (key, dep) in dependencies {
154            if key.0 == new_key.0 // resource_type
155                && key.1 != new_key.1 // name
156                && dep.path == *new_path
157                && dep.source == new_source.map(String::from)
158                && dep.tool == new_tool.map(String::from)
159            {
160                return true;
161            }
162        }
163
164        false
165    }
166}
167
168impl Default for ConflictService {
169    fn default() -> Self {
170        Self::new()
171    }
172}