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