agpm_cli/version/constraints/resolver.rs
1//! Constraint resolver implementation for multi-dependency version resolution.
2
3use anyhow::Result;
4use semver::Version;
5use std::collections::HashMap;
6
7use super::{ConstraintSet, VersionConstraint};
8use crate::core::AgpmError;
9
10/// Manages version constraints for multiple dependencies and resolves them simultaneously.
11///
12/// `ConstraintResolver` coordinates version resolution across an entire dependency graph,
13/// ensuring that all constraints are satisfied and conflicts are detected. It maintains
14/// separate [`ConstraintSet`]s for each dependency and resolves them against available
15/// version catalogs.
16///
17/// # Multi-Dependency Resolution
18///
19/// Unlike [`ConstraintSet`] which manages constraints for a single dependency,
20/// `ConstraintResolver` handles multiple dependencies simultaneously:
21///
22/// - Each dependency gets its own constraint set
23/// - Constraints can be added incrementally
24/// - Resolution happens across the entire dependency graph
25/// - Missing dependencies are detected and reported
26///
27/// # Resolution Process
28///
29/// 1. **Collect constraints**: Gather all constraints for each dependency
30/// 2. **Validate availability**: Ensure versions exist for all dependencies
31/// 3. **Apply constraint sets**: Use each dependency's constraints to filter versions
32/// 4. **Select best matches**: Choose optimal versions for each dependency
33/// 5. **Return resolution map**: Provide final version selections
34///
35/// # Examples
36///
37/// ## Basic Multi-Dependency Resolution
38///
39/// ```rust,no_run
40/// use agpm_cli::version::constraints::ConstraintResolver;
41/// use semver::Version;
42/// use std::collections::HashMap;
43///
44/// let mut resolver = ConstraintResolver::new();
45///
46/// // Add constraints for multiple dependencies
47/// resolver.add_constraint("dep1", "^1.0.0")?;
48/// resolver.add_constraint("dep2", "~2.1.0")?;
49/// resolver.add_constraint("dep3", "main")?;
50///
51/// // Provide available versions for each dependency
52/// let mut available = HashMap::new();
53/// available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
54/// available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
55/// available.insert("dep3".to_string(), vec![Version::parse("3.0.0")?]);
56///
57/// // Resolve all dependencies
58/// let resolved = resolver.resolve(&available)?;
59/// assert_eq!(resolved.len(), 3);
60/// # Ok::<(), anyhow::Error>(())
61/// ```
62///
63/// ## Incremental Constraint Addition
64///
65/// ```rust,no_run
66/// use agpm_cli::version::constraints::ConstraintResolver;
67///
68/// let mut resolver = ConstraintResolver::new();
69///
70/// // Add multiple constraints for the same dependency
71/// resolver.add_constraint("my-dep", ">=1.0.0")?;
72/// resolver.add_constraint("my-dep", "<2.0.0")?;
73/// resolver.add_constraint("my-dep", "^1.5.0")?;
74///
75/// // All constraints will be combined into a single constraint set
76/// # Ok::<(), anyhow::Error>(())
77/// ```
78///
79/// # Error Conditions
80///
81/// The resolver reports several types of errors:
82///
83/// - **Missing dependencies**: A constraint exists but no versions are available
84/// - **Unsatisfiable constraints**: No available version meets all requirements
85/// - **Conflicting constraints**: Impossible constraint combinations
86///
87/// # Use Cases
88///
89/// This resolver is particularly useful for:
90/// - Package managers resolving dependency graphs
91/// - Build systems selecting compatible versions
92/// - Configuration management ensuring consistent environments
93/// - Update analysis determining safe upgrade paths
94pub struct ConstraintResolver {
95 constraints: HashMap<String, ConstraintSet>,
96}
97
98impl Default for ConstraintResolver {
99 fn default() -> Self {
100 Self::new()
101 }
102}
103
104impl ConstraintResolver {
105 /// Creates a new constraint resolver
106 ///
107 /// # Returns
108 ///
109 /// Returns a new `ConstraintResolver` with empty constraint and resolution maps
110 #[must_use]
111 pub fn new() -> Self {
112 Self {
113 constraints: HashMap::new(),
114 }
115 }
116
117 /// Add a version constraint for a specific dependency.
118 ///
119 /// This method parses constraint string and adds it to the constraint set
120 /// for the named dependency. If this is the first constraint for the dependency,
121 /// a new constraint set is created. Multiple constraints for the same dependency
122 /// are combined into a single set with conflict detection.
123 ///
124 /// # Arguments
125 ///
126 /// * `dependency` - The name of the dependency to constrain
127 /// * `constraint` - The constraint string to parse and add (e.g., "^1.0.0", "latest")
128 ///
129 /// # Returns
130 ///
131 /// Returns `Ok(())` if the constraint was added successfully, or `Err` if:
132 /// - The constraint string is invalid
133 /// - The constraint conflicts with existing constraints for this dependency
134 ///
135 /// # Examples
136 ///
137 /// ```rust,no_run
138 /// use agpm_cli::version::constraints::ConstraintResolver;
139 ///
140 /// let mut resolver = ConstraintResolver::new();
141 ///
142 /// // Add constraints for different dependencies
143 /// resolver.add_constraint("web-framework", "^2.0.0")?;
144 /// resolver.add_constraint("database", "~1.5.0")?;
145 /// resolver.add_constraint("auth-lib", "main")?;
146 ///
147 /// // Add multiple constraints for the same dependency
148 /// resolver.add_constraint("api-client", ">=1.0.0")?;
149 /// resolver.add_constraint("api-client", "<2.0.0")?; // Compatible range
150 ///
151 /// // This would fail - conflicting exact versions
152 /// resolver.add_constraint("my-dep", "1.0.0")?;
153 /// let result = resolver.add_constraint("my-dep", "2.0.0");
154 /// assert!(result.is_err());
155 /// # Ok::<(), anyhow::Error>(())
156 /// ```
157 ///
158 /// # Constraint Combination
159 ///
160 /// When multiple constraints are added for the same dependency, they are
161 /// combined using AND logic. The final constraint set requires that all
162 /// individual constraints be satisfied simultaneously.
163 pub fn add_constraint(&mut self, dependency: &str, constraint: &str) -> Result<()> {
164 let parsed = VersionConstraint::parse(constraint)?;
165
166 self.constraints.entry(dependency.to_string()).or_default().add(parsed)?;
167
168 Ok(())
169 }
170
171 /// Resolve all dependency constraints and return the best version for each.
172 ///
173 /// This method performs the core resolution algorithm, taking all accumulated
174 /// constraints and finding the best matching version for each dependency from
175 /// the provided catalog of available versions.
176 ///
177 /// # Resolution Algorithm
178 ///
179 /// For each dependency with constraints:
180 /// 1. **Verify availability**: Check that versions exist for the dependency
181 /// 2. **Apply constraints**: Filter versions using the dependency's constraint set
182 /// 3. **Select best match**: Choose the highest compatible version
183 /// 4. **Handle prereleases**: Apply prerelease policies appropriately
184 ///
185 /// # Arguments
186 ///
187 /// * `available_versions` - Map from dependency names to lists of available versions
188 ///
189 /// # Returns
190 ///
191 /// Returns `Ok(HashMap<String, Version>)` with the resolved version for each
192 /// dependency, or `Err` if resolution fails.
193 ///
194 /// # Error Conditions
195 ///
196 /// - **Missing dependency**: Constraint exists but no versions are available
197 /// - **No satisfying version**: Available versions don't meet constraints
198 /// - **Internal errors**: Constraint set conflicts or parsing failures
199 ///
200 /// # Examples
201 ///
202 /// ```rust,no_run
203 /// use agpm_cli::version::constraints::ConstraintResolver;
204 /// use semver::Version;
205 /// use std::collections::HashMap;
206 ///
207 /// let mut resolver = ConstraintResolver::new();
208 /// resolver.add_constraint("web-server", "^1.0.0")?;
209 /// resolver.add_constraint("database", "~2.1.0")?;
210 ///
211 /// // Provide version catalog
212 /// let mut available = HashMap::new();
213 /// available.insert(
214 /// "web-server".to_string(),
215 /// vec![
216 /// Version::parse("1.0.0")?,
217 /// Version::parse("1.2.0")?,
218 /// Version::parse("1.5.0")?, // Best match for ^1.0.0
219 /// Version::parse("2.0.0")?, // Too new
220 /// ],
221 /// );
222 /// available.insert(
223 /// "database".to_string(),
224 /// vec![
225 /// Version::parse("2.1.0")?,
226 /// Version::parse("2.1.3")?, // Best match for ~2.1.0
227 /// Version::parse("2.2.0")?, // Too new
228 /// ],
229 /// );
230 ///
231 /// // Resolve dependencies
232 /// let resolved = resolver.resolve(&available)?;
233 /// assert_eq!(resolved["web-server"], Version::parse("1.5.0")?);
234 /// assert_eq!(resolved["database"], Version::parse("2.1.3")?);
235 /// # Ok::<(), anyhow::Error>(())
236 /// ```
237 ///
238 /// ## Error Handling
239 ///
240 /// ```rust,no_run
241 /// use agpm_cli::version::constraints::ConstraintResolver;
242 /// use std::collections::HashMap;
243 ///
244 /// let mut resolver = ConstraintResolver::new();
245 /// resolver.add_constraint("missing-dep", "^1.0.0")?;
246 ///
247 /// let available = HashMap::new(); // No versions provided
248 ///
249 /// let result = resolver.resolve(&available);
250 /// assert!(result.is_err()); // Missing dependency error
251 /// # Ok::<(), anyhow::Error>(())
252 /// ```
253 ///
254 /// # Performance Considerations
255 ///
256 /// - Resolution is performed independently for each dependency
257 /// - Version filtering and sorting may be expensive for large version lists
258 /// - Consider pre-filtering available versions if catalogs are very large
259 pub fn resolve(
260 &self,
261 available_versions: &HashMap<String, Vec<Version>>,
262 ) -> Result<HashMap<String, Version>> {
263 let mut resolved = HashMap::new();
264
265 for (dep, constraint_set) in &self.constraints {
266 let versions = available_versions.get(dep).ok_or_else(|| AgpmError::Other {
267 message: format!("No versions available for dependency: {dep}"),
268 })?;
269
270 let best_match =
271 constraint_set.find_best_match(versions).ok_or_else(|| AgpmError::Other {
272 message: format!("No version satisfies constraints for dependency: {dep}"),
273 })?;
274
275 resolved.insert(dep.clone(), best_match.clone());
276 }
277
278 Ok(resolved)
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use std::collections::HashMap;
286
287 #[test]
288 fn test_constraint_resolver() {
289 let mut resolver = ConstraintResolver::new();
290
291 resolver.add_constraint("dep1", "^1.0.0").unwrap();
292 resolver.add_constraint("dep2", "~2.1.0").unwrap();
293
294 let mut available = HashMap::new();
295 available.insert(
296 "dep1".to_string(),
297 vec![
298 Version::parse("0.9.0").unwrap(),
299 Version::parse("1.0.0").unwrap(),
300 Version::parse("1.5.0").unwrap(),
301 Version::parse("2.0.0").unwrap(),
302 ],
303 );
304 available.insert(
305 "dep2".to_string(),
306 vec![
307 Version::parse("2.0.0").unwrap(),
308 Version::parse("2.1.0").unwrap(),
309 Version::parse("2.1.5").unwrap(),
310 Version::parse("2.2.0").unwrap(),
311 ],
312 );
313
314 let resolved = resolver.resolve(&available).unwrap();
315 assert_eq!(resolved.get("dep1"), Some(&Version::parse("1.5.0").unwrap()));
316 assert_eq!(resolved.get("dep2"), Some(&Version::parse("2.1.5").unwrap()));
317 }
318
319 #[test]
320 fn test_constraint_resolver_missing_dependency() {
321 let mut resolver = ConstraintResolver::new();
322 resolver.add_constraint("dep1", "^1.0.0").unwrap();
323
324 let available = HashMap::new(); // No versions available
325
326 let result = resolver.resolve(&available);
327 assert!(result.is_err());
328 }
329
330 #[test]
331 fn test_constraint_resolver_no_satisfying_version() {
332 let mut resolver = ConstraintResolver::new();
333 resolver.add_constraint("dep1", "^2.0.0").unwrap();
334
335 let mut available = HashMap::new();
336 available.insert(
337 "dep1".to_string(),
338 vec![Version::parse("1.0.0").unwrap()], // Only 1.x available, but we need 2.x
339 );
340
341 let result = resolver.resolve(&available);
342 assert!(result.is_err());
343 }
344
345 #[test]
346 fn test_constraint_resolver_add_constraint_error() {
347 let mut resolver = ConstraintResolver::new();
348
349 // Add a valid constraint first
350 resolver.add_constraint("dep1", "1.0.0").unwrap();
351
352 // Add conflicting constraint
353 let result = resolver.add_constraint("dep1", "2.0.0");
354 assert!(result.is_err());
355 }
356}