agpm_cli/version/constraints.rs
1//! Version constraint parsing and resolution for AGPM dependencies.
2//!
3//! This module provides comprehensive version constraint handling for AGPM dependencies,
4//! supporting semantic versioning, Git references, and various constraint types. It enables
5//! dependency resolution with conflict detection and version matching.
6//!
7//! # Version Constraint Types
8//!
9//! AGPM supports several types of version constraints:
10//!
11//! - **Exact versions**: `"1.0.0"` - Matches exactly the specified version
12//! - **Semantic version ranges**: `"^1.0.0"`, `"~1.2.0"`, `">=1.0.0"` - Uses semver ranges
13//! - **Git references**: `"main"`, `"feature/branch"`, `"abc123"`, `"latest"` - Git branches, tags, or commits
14//!
15//! # Constraint Resolution
16//!
17//! The constraint system provides:
18//! - **Conflict detection**: Prevents incompatible constraints for the same dependency
19//! - **Version resolution**: Finds best matching versions from available options
20//! - **Prerelease handling**: Manages alpha, beta, RC versions appropriately
21//! - **Precedence rules**: Resolves multiple constraints consistently
22//!
23//! # Examples
24//!
25//! ## Basic Constraint Parsing
26//!
27//! ```rust,no_run
28//! use agpm_cli::version::constraints::VersionConstraint;
29//!
30//! // Parse different constraint types
31//! let exact = VersionConstraint::parse("1.0.0")?;
32//! let caret = VersionConstraint::parse("^1.0.0")?;
33//! let latest = VersionConstraint::parse("latest")?;
34//! let branch = VersionConstraint::parse("main")?;
35//! # Ok::<(), anyhow::Error>(())
36//! ```
37//!
38//! ## Constraint Set Management
39//!
40//! ```rust,no_run
41//! use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
42//! use semver::Version;
43//!
44//! let mut set = ConstraintSet::new();
45//! set.add(VersionConstraint::parse(">=1.0.0")?)?;
46//! set.add(VersionConstraint::parse("<2.0.0")?)?;
47//!
48//! let versions = vec![
49//! Version::parse("0.9.0")?,
50//! Version::parse("1.5.0")?,
51//! Version::parse("2.0.0")?,
52//! ];
53//!
54//! let best = set.find_best_match(&versions).unwrap();
55//! assert_eq!(best, &Version::parse("1.5.0")?);
56//! # Ok::<(), anyhow::Error>(())
57//! ```
58//!
59//! ## Dependency Resolution
60//!
61//! ```rust,no_run
62//! use agpm_cli::version::constraints::ConstraintResolver;
63//! use semver::Version;
64//! use std::collections::HashMap;
65//!
66//! let mut resolver = ConstraintResolver::new();
67//! resolver.add_constraint("dep1", "^1.0.0")?;
68//! resolver.add_constraint("dep2", "~2.1.0")?;
69//!
70//! let mut available = HashMap::new();
71//! available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
72//! available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
73//!
74//! let resolved = resolver.resolve(&available)?;
75//! # Ok::<(), anyhow::Error>(())
76//! ```
77//!
78//! # Constraint Syntax Reference
79//!
80//! | Syntax | Description | Example |
81//! |--------|-------------|----------|
82//! | `1.0.0` | Exact version | `"1.0.0"` |
83//! | `^1.0.0` | Compatible within major version | `"^1.0.0"` matches `1.x.x` |
84//! | `~1.2.0` | Compatible within minor version | `"~1.2.0"` matches `1.2.x` |
85//! | `>=1.0.0` | Greater than or equal | `">=1.0.0"` |
86//! | `<2.0.0` | Less than | `"<2.0.0"` |
87//! | `>=1.0.0, <2.0.0` | Range constraint | Multiple constraints |
88//! | `main` | Git branch reference | Branch name |
89//! | `latest` | Git tag or branch name | Just a regular ref |
90//! | `v1.0.0` | Git tag reference | Tag name |
91//! | `abc123` | Git commit reference | Commit hash (full or abbreviated) |
92//!
93//! # Version Resolution Precedence
94//!
95//! When resolving versions, AGPM follows this precedence:
96//!
97//! 1. **Exact matches** take highest priority
98//! 2. **Semantic version requirements** are resolved to highest compatible version
99//! 3. **Stable versions** are preferred over prereleases (unless explicitly allowed)
100//! 4. **Newer versions** are preferred when multiple versions satisfy constraints
101//! 5. **Git references** bypass semantic versioning and use exact ref matching
102//!
103//! # Prerelease Version Handling
104//!
105//! - **Default behavior**: Prereleases (alpha, beta, RC) are excluded from resolution
106//! - **Explicit inclusion**: Use Git references to include prereleases
107//! - **Version ranges**: Prereleases only match if explicitly specified in range
108//! - **Constraint sets**: If any constraint allows prereleases, the entire set does
109//!
110//! # Error Conditions
111//!
112//! The constraint system handles these error conditions:
113//! - **Conflicting constraints**: Same dependency with incompatible requirements
114//! - **Invalid version strings**: Malformed semantic version specifications
115//! - **Resolution failures**: No available version satisfies all constraints
116//! - **Missing dependencies**: Required dependency not found in available versions
117
118use anyhow::Result;
119use semver::{Version, VersionReq};
120use std::collections::HashMap;
121use std::fmt;
122
123use crate::core::AgpmError;
124
125/// A version constraint that defines acceptable versions for a dependency.
126///
127/// Version constraints in AGPM support multiple formats to handle different
128/// versioning strategies and Git-based dependencies. Each constraint type
129/// provides specific matching behavior for version resolution.
130///
131/// # Constraint Types
132///
133/// - [`Exact`](Self::Exact): Matches exactly one specific semantic version
134/// - [`Requirement`](Self::Requirement): Matches versions using semver ranges
135/// - [`GitRef`](Self::GitRef): Matches specific Git branches, tags, or commit hashes (including "latest")
136///
137/// # Examples
138///
139/// ```rust,no_run
140/// use agpm_cli::version::constraints::VersionConstraint;
141/// use semver::Version;
142///
143/// // Parse various constraint formats
144/// let exact = VersionConstraint::parse("1.0.0")?;
145/// let caret = VersionConstraint::parse("^1.0.0")?; // Compatible versions
146/// let tilde = VersionConstraint::parse("~1.2.0")?; // Patch-level compatible
147/// let range = VersionConstraint::parse(">=1.0.0, <2.0.0")?; // Version range
148/// let branch = VersionConstraint::parse("main")?;
149/// let latest_tag = VersionConstraint::parse("latest")?; // Just a tag name
150/// let commit = VersionConstraint::parse("abc123def")?;
151///
152/// // Test version matching
153/// let version = Version::parse("1.2.3")?;
154/// assert!(caret.matches(&version));
155/// # Ok::<(), anyhow::Error>(())
156/// ```
157///
158/// # Prerelease Handling
159///
160/// By default, most constraints exclude prerelease versions to ensure stability:
161/// - `GitRef` constraints (including "latest" tag names) may reference any commit
162///
163/// # Git Reference Matching
164///
165/// Git references are matched by name rather than semantic version:
166/// - Branch names: `"main"`, `"develop"`, `"feature/auth"`
167/// - Tag names: `"v1.0.0"`, `"release-2023-01"`
168/// - Commit hashes: `"abc123def456"` (full or abbreviated)
169///
170/// # Prefix Support (Monorepo Versioning)
171///
172/// Constraints can include optional prefixes for monorepo-style versioning:
173/// - `"agents-v1.0.0"`: Exact version with prefix
174/// - `"agents-^v1.0.0"`: Compatible version range with prefix
175/// - Prefixed constraints only match tags with the same prefix
176#[derive(Debug, Clone)]
177pub enum VersionConstraint {
178 /// Exact version match with optional prefix (e.g., "1.0.0", "agents-v1.0.0")
179 Exact {
180 prefix: Option<String>,
181 version: Version,
182 },
183
184 /// Semantic version requirement with optional prefix (e.g., "^1.0.0", "agents-^v1.0.0")
185 Requirement {
186 prefix: Option<String>,
187 req: VersionReq,
188 },
189
190 /// Git tag or branch name (including "latest" - it's just a tag name)
191 GitRef(String),
192}
193
194impl VersionConstraint {
195 /// Parse a constraint string into a [`VersionConstraint`].
196 ///
197 /// This method intelligently determines the constraint type based on the input format.
198 /// It handles various syntaxes including semantic versions, version ranges, special
199 /// keywords, and Git references.
200 ///
201 /// # Parsing Logic
202 ///
203 /// 1. **Special keywords**: `"*"` (wildcard for any version)
204 /// 2. **Exact versions**: `"1.0.0"`, `"v1.0.0"` (without range operators)
205 /// 3. **Version requirements**: `"^1.0.0"`, `"~1.2.0"`, `">=1.0.0"`, `"<2.0.0"`
206 /// 4. **Git references**: Any string that doesn't match the above patterns (including "latest")
207 ///
208 /// # Arguments
209 ///
210 /// * `constraint` - The constraint string to parse (whitespace is trimmed)
211 ///
212 /// # Returns
213 ///
214 /// Returns `Ok(VersionConstraint)` on successful parsing, or `Err` if the
215 /// semantic version parsing fails (Git references always succeed).
216 ///
217 /// # Examples
218 ///
219 /// ```rust,no_run
220 /// use agpm_cli::version::constraints::VersionConstraint;
221 ///
222 /// // Exact version matching
223 /// let exact = VersionConstraint::parse("1.0.0")?;
224 /// let exact_with_v = VersionConstraint::parse("v1.0.0")?;
225 ///
226 /// // Semantic version ranges
227 /// let caret = VersionConstraint::parse("^1.0.0")?; // 1.x.x compatible
228 /// let tilde = VersionConstraint::parse("~1.2.0")?; // 1.2.x compatible
229 /// let gte = VersionConstraint::parse(">=1.0.0")?; // Greater or equal
230 /// let range = VersionConstraint::parse(">1.0.0, <2.0.0")?; // Range
231 ///
232 /// // Special keywords
233 /// let any = VersionConstraint::parse("*")?; // Any version
234 ///
235 /// // Git references
236 /// let branch = VersionConstraint::parse("main")?; // Branch name
237 /// let tag = VersionConstraint::parse("release-v1")?; // Tag name
238 /// let latest = VersionConstraint::parse("latest")?; // Just a tag/branch name
239 /// let commit = VersionConstraint::parse("abc123def")?; // Commit hash
240 /// # Ok::<(), anyhow::Error>(())
241 /// ```
242 ///
243 /// # Error Handling
244 ///
245 /// This method only returns errors for malformed semantic version strings.
246 /// Git references and special keywords always parse successfully.
247 pub fn parse(constraint: &str) -> Result<Self> {
248 let trimmed = constraint.trim();
249
250 // Extract prefix from constraint first (e.g., "agents-^v1.0.0" → (Some("agents"), "^v1.0.0"))
251 let (prefix, version_str) = crate::version::split_prefix_and_version(trimmed);
252
253 // Check for wildcard in the version portion (supports both "*" and "agents-*")
254 if version_str == "*" {
255 // Wildcard means any version - treat as a GitRef that matches everything
256 return Ok(Self::GitRef(trimmed.to_string()));
257 }
258
259 // Try to parse as exact version (with or without 'v' prefix)
260 let cleaned_version_str = version_str.strip_prefix('v').unwrap_or(version_str);
261 if let Ok(version) = Version::parse(cleaned_version_str) {
262 // Check if it's a range operator
263 if !version_str.starts_with('^')
264 && !version_str.starts_with('~')
265 && !version_str.starts_with('>')
266 && !version_str.starts_with('<')
267 && !version_str.starts_with('=')
268 {
269 return Ok(Self::Exact {
270 prefix,
271 version,
272 });
273 }
274 }
275
276 // Try to parse as version requirement (with v-prefix normalization)
277 match crate::version::parse_version_req(version_str) {
278 Ok(req) => {
279 return Ok(Self::Requirement {
280 prefix,
281 req,
282 });
283 }
284 Err(e) => {
285 // If it looks like a semver constraint but failed to parse, return error
286 if version_str.starts_with('^')
287 || version_str.starts_with('~')
288 || version_str.starts_with('=')
289 || version_str.starts_with('>')
290 || version_str.starts_with('<')
291 {
292 return Err(anyhow::anyhow!("Invalid semver constraint '{trimmed}': {e}"));
293 }
294 // Otherwise it might be a git ref, continue
295 }
296 }
297
298 // Otherwise treat as git ref
299 Ok(Self::GitRef(trimmed.to_string()))
300 }
301
302 /// Check if a semantic version satisfies this constraint.
303 ///
304 /// This method tests whether a given semantic version matches the requirements
305 /// of this constraint. Different constraint types use different matching logic:
306 ///
307 /// - **Exact**: Version must match exactly
308 /// - **Requirement**: Version must satisfy the semver range
309 /// - **`GitRef`**: Never matches semantic versions (Git refs are matched separately)
310 ///
311 /// # Arguments
312 ///
313 /// * `version` - The semantic version to test against this constraint
314 ///
315 /// # Returns
316 ///
317 /// Returns `true` if the version satisfies the constraint, `false` otherwise.
318 ///
319 /// # Examples
320 ///
321 /// ```rust,no_run
322 /// use agpm_cli::version::constraints::VersionConstraint;
323 /// use semver::Version;
324 ///
325 /// let constraint = VersionConstraint::parse("^1.0.0")?;
326 /// let version = Version::parse("1.2.3")?;
327 ///
328 /// assert!(constraint.matches(&version)); // 1.2.3 is compatible with ^1.0.0
329 /// # Ok::<(), anyhow::Error>(())
330 /// ```
331 ///
332 /// # Note
333 ///
334 /// Git reference constraints always return `false` for this method since they
335 /// operate on Git refs rather than semantic versions. Use [`matches_ref`](Self::matches_ref)
336 /// to test Git reference matching.
337 #[must_use]
338 pub fn matches(&self, version: &Version) -> bool {
339 match self {
340 Self::Exact {
341 version: v,
342 ..
343 } => v == version,
344 Self::Requirement {
345 req,
346 ..
347 } => req.matches(version),
348 Self::GitRef(_) => false, // Git refs don't match semver versions
349 }
350 }
351
352 /// Check if a Git reference satisfies this constraint.
353 ///
354 /// This method tests whether a Git reference (branch, tag, or commit hash)
355 /// matches a Git reference constraint. Only [`GitRef`](Self::GitRef) constraints
356 /// can match Git references - all other constraint types return `false`.
357 ///
358 /// # Arguments
359 ///
360 /// * `git_ref` - The Git reference string to test (branch, tag, or commit)
361 ///
362 /// # Returns
363 ///
364 /// Returns `true` if this is a `GitRef` constraint with matching reference name,
365 /// `false` otherwise.
366 ///
367 /// # Examples
368 ///
369 /// ```rust,no_run
370 /// use agpm_cli::version::constraints::VersionConstraint;
371 ///
372 /// let branch_constraint = VersionConstraint::parse("main")?;
373 /// assert!(branch_constraint.matches_ref("main"));
374 /// assert!(!branch_constraint.matches_ref("develop"));
375 ///
376 /// let version_constraint = VersionConstraint::parse("^1.0.0")?;
377 /// assert!(!version_constraint.matches_ref("main")); // Version constraints don't match refs
378 /// # Ok::<(), anyhow::Error>(())
379 /// ```
380 ///
381 /// # Use Cases
382 ///
383 /// This method is primarily used during dependency resolution to match
384 /// dependencies that specify Git branches, tags, or commit hashes rather
385 /// than semantic versions.
386 #[must_use]
387 pub fn matches_ref(&self, git_ref: &str) -> bool {
388 match self {
389 Self::GitRef(ref_name) => ref_name == git_ref,
390 _ => false,
391 }
392 }
393
394 /// Check if a VersionInfo satisfies this constraint, including prefix matching.
395 ///
396 /// This method performs comprehensive matching that considers both the prefix
397 /// (for monorepo-style versioning) and the semantic version. It's the preferred
398 /// method for version resolution when working with potentially prefixed versions.
399 ///
400 /// # Matching Rules
401 ///
402 /// - **Prefix matching**: Constraint and version must have the same prefix (both None, or same String)
403 /// - **Version matching**: After prefix check, applies standard semver matching rules
404 /// - **Prerelease handling**: Follows same rules as [`matches`](Self::matches)
405 ///
406 /// # Arguments
407 ///
408 /// * `version_info` - The version information to test, including prefix and semver
409 ///
410 /// # Returns
411 ///
412 /// Returns `true` if both the prefix matches AND the version satisfies the constraint.
413 ///
414 /// # Examples
415 ///
416 /// ```rust,no_run
417 /// use agpm_cli::version::constraints::VersionConstraint;
418 /// use agpm_cli::version::VersionInfo;
419 /// use semver::Version;
420 ///
421 /// // Prefixed version matching
422 /// let constraint = VersionConstraint::parse("agents-^v1.0.0")?;
423 /// let version = VersionInfo {
424 /// prefix: Some("agents".to_string()),
425 /// version: Version::parse("1.2.0")?,
426 /// tag: "agents-v1.2.0".to_string(),
427 /// prerelease: false,
428 /// };
429 /// assert!(constraint.matches_version_info(&version));
430 ///
431 /// // Prefix mismatch
432 /// let wrong_prefix = VersionInfo {
433 /// prefix: Some("snippets".to_string()),
434 /// version: Version::parse("1.2.0")?,
435 /// tag: "snippets-v1.2.0".to_string(),
436 /// prerelease: false,
437 /// };
438 /// assert!(!constraint.matches_version_info(&wrong_prefix));
439 ///
440 /// // Unprefixed constraint only matches unprefixed versions
441 /// let no_prefix_constraint = VersionConstraint::parse("^1.0.0")?;
442 /// let no_prefix_version = VersionInfo {
443 /// prefix: None,
444 /// version: Version::parse("1.2.0")?,
445 /// tag: "v1.2.0".to_string(),
446 /// prerelease: false,
447 /// };
448 /// assert!(no_prefix_constraint.matches_version_info(&no_prefix_version));
449 /// assert!(!no_prefix_constraint.matches_version_info(&version)); // Has prefix
450 /// # Ok::<(), anyhow::Error>(())
451 /// ```
452 #[inline]
453 #[must_use]
454 pub fn matches_version_info(&self, version_info: &crate::version::VersionInfo) -> bool {
455 // Check prefix first
456 let constraint_prefix = match self {
457 Self::Exact {
458 prefix,
459 ..
460 }
461 | Self::Requirement {
462 prefix,
463 ..
464 } => prefix.as_ref(),
465 _ => None,
466 };
467
468 // Prefix must match (both None or both Some with same value)
469 if constraint_prefix != version_info.prefix.as_ref() {
470 return false;
471 }
472
473 // Then check version using existing logic
474 self.matches(&version_info.version)
475 }
476
477 /// Convert this constraint to a semantic version requirement if applicable.
478 ///
479 /// This method converts version-based constraints into [`VersionReq`] objects
480 /// that can be used with the semver crate for version matching. Git reference
481 /// constraints cannot be converted since they don't represent version ranges.
482 ///
483 /// # Returns
484 ///
485 /// Returns `Some(VersionReq)` for constraints that can be expressed as semantic
486 /// version requirements, or `None` for Git reference constraints.
487 ///
488 /// # Conversion Rules
489 ///
490 /// - **Exact**: Converted to `=1.0.0` requirement
491 /// - **Requirement**: Returns the inner `VersionReq` directly
492 /// - **`GitRef`**: Returns `None` (cannot be converted)
493 ///
494 /// # Examples
495 ///
496 /// ```rust,no_run
497 /// use agpm_cli::version::constraints::VersionConstraint;
498 /// use semver::Version;
499 ///
500 /// let exact = VersionConstraint::parse("1.0.0")?;
501 /// let req = exact.to_version_req().unwrap();
502 /// assert!(req.matches(&Version::parse("1.0.0")?));
503 ///
504 /// let caret = VersionConstraint::parse("^1.0.0")?;
505 /// let req = caret.to_version_req().unwrap();
506 /// assert!(req.matches(&Version::parse("1.2.0")?));
507 ///
508 /// let git_ref = VersionConstraint::parse("main")?;
509 /// assert!(git_ref.to_version_req().is_none()); // Git refs can't be converted
510 /// # Ok::<(), anyhow::Error>(())
511 /// ```
512 ///
513 /// # Use Cases
514 ///
515 /// This method is useful for integrating with existing semver-based tooling
516 /// or for performing version calculations that require `VersionReq` objects.
517 #[must_use]
518 pub fn to_version_req(&self) -> Option<VersionReq> {
519 match self {
520 Self::Exact {
521 version,
522 ..
523 } => {
524 // Create an exact version requirement
525 VersionReq::parse(&format!("={version}")).ok()
526 }
527 Self::Requirement {
528 req,
529 ..
530 } => Some(req.clone()),
531 Self::GitRef(_) => None, // Git refs cannot be converted to version requirements
532 }
533 }
534
535 /// Check if this constraint allows prerelease versions.
536 ///
537 /// Prerelease versions contain identifiers like `-alpha`, `-beta`, `-rc` that
538 /// indicate pre-release status. This method determines whether the constraint
539 /// should consider such versions during resolution.
540 ///
541 /// # Prerelease Policy
542 ///
543 /// - **`GitRef`**: Allows prereleases (Git refs may point to any commit)
544 /// - **Exact/Requirement**: Excludes prereleases unless explicitly specified
545 ///
546 /// # Returns
547 ///
548 /// Returns `true` if prerelease versions should be considered, `false` if only
549 /// stable versions should be considered.
550 ///
551 /// # Examples
552 ///
553 /// ```rust,no_run
554 /// use agpm_cli::version::constraints::VersionConstraint;
555 ///
556 /// let branch = VersionConstraint::parse("main")?;
557 /// assert!(branch.allows_prerelease()); // Git refs may be any version
558 ///
559 /// let latest = VersionConstraint::parse("latest")?;
560 /// assert!(latest.allows_prerelease()); // Git ref - just a tag name
561 ///
562 /// let exact = VersionConstraint::parse("1.0.0")?;
563 /// assert!(!exact.allows_prerelease()); // Exact stable version
564 /// # Ok::<(), anyhow::Error>(())
565 /// ```
566 ///
567 /// # Impact on Resolution
568 ///
569 /// During version resolution, if any constraint in a set allows prereleases,
570 /// the entire constraint set will consider prerelease versions as candidates.
571 #[must_use]
572 pub const fn allows_prerelease(&self) -> bool {
573 matches!(self, Self::GitRef(_))
574 }
575}
576
577impl fmt::Display for VersionConstraint {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 match self {
580 Self::Exact {
581 prefix,
582 version,
583 } => {
584 if let Some(p) = prefix {
585 write!(f, "{p}-{version}")
586 } else {
587 write!(f, "{version}")
588 }
589 }
590 Self::Requirement {
591 prefix,
592 req,
593 } => {
594 if let Some(p) = prefix {
595 write!(f, "{p}-{req}")
596 } else {
597 write!(f, "{req}")
598 }
599 }
600 Self::GitRef(ref_name) => write!(f, "{ref_name}"),
601 }
602 }
603}
604
605/// A collection of version constraints that must all be satisfied simultaneously.
606///
607/// `ConstraintSet` manages multiple [`VersionConstraint`]s for a single dependency,
608/// ensuring that all constraints are compatible and can be resolved together.
609/// It provides conflict detection, version matching, and best-match selection.
610///
611/// # Constraint Combination
612///
613/// When multiple constraints are added to a set, they create an intersection
614/// of requirements. For example:
615/// - `>=1.0.0` AND `<2.0.0` = versions in range `[1.0.0, 2.0.0)`
616/// - `^1.0.0` AND `~1.2.0` = versions compatible with both (e.g., `1.2.x`)
617///
618/// # Conflict Detection
619///
620/// The constraint set detects and prevents conflicting constraints:
621/// - Multiple exact versions: `1.0.0` AND `2.0.0` (impossible to satisfy)
622/// - Conflicting Git refs: `main` AND `develop` (can't be both branches)
623///
624/// # Resolution Strategy
625///
626/// When selecting from available versions, the set:
627/// 1. Filters versions that satisfy ALL constraints
628/// 2. Excludes prereleases unless explicitly allowed
629/// 3. Selects the highest remaining version
630///
631/// # Examples
632///
633/// ## Basic Usage
634///
635/// ```rust,no_run
636/// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
637/// use semver::Version;
638///
639/// let mut set = ConstraintSet::new();
640/// set.add(VersionConstraint::parse(">=1.0.0")?)?;
641/// set.add(VersionConstraint::parse("<2.0.0")?)?;
642///
643/// let version = Version::parse("1.5.0")?;
644/// assert!(set.satisfies(&version));
645///
646/// let version = Version::parse("2.0.0")?;
647/// assert!(!set.satisfies(&version)); // Outside range
648/// # Ok::<(), anyhow::Error>(())
649/// ```
650///
651/// ## Best Match Selection
652///
653/// ```rust,no_run
654/// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
655/// use semver::Version;
656///
657/// let mut set = ConstraintSet::new();
658/// set.add(VersionConstraint::parse("^1.0.0")?)?;
659///
660/// let versions = vec![
661/// Version::parse("0.9.0")?, // Too old
662/// Version::parse("1.0.0")?, // Matches
663/// Version::parse("1.5.0")?, // Matches, higher
664/// Version::parse("2.0.0")?, // Too new
665/// ];
666///
667/// let best = set.find_best_match(&versions).unwrap();
668/// assert_eq!(best, &Version::parse("1.5.0")?); // Highest compatible
669/// # Ok::<(), anyhow::Error>(())
670/// ```
671///
672/// ## Conflict Detection
673///
674/// ```rust,no_run
675/// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
676/// use semver::Version;
677///
678/// let mut set = ConstraintSet::new();
679/// set.add(VersionConstraint::parse("1.0.0")?)?; // Exact version
680///
681/// // This will fail - can't have two different exact versions
682/// let result = set.add(VersionConstraint::parse("2.0.0")?);
683/// assert!(result.is_err());
684/// # Ok::<(), anyhow::Error>(())
685/// ```
686#[derive(Debug, Clone)]
687pub struct ConstraintSet {
688 constraints: Vec<VersionConstraint>,
689}
690
691impl Default for ConstraintSet {
692 fn default() -> Self {
693 Self::new()
694 }
695}
696
697impl ConstraintSet {
698 /// Creates a new empty constraint set
699 ///
700 /// # Returns
701 ///
702 /// Returns a new `ConstraintSet` with no constraints
703 #[must_use]
704 pub const fn new() -> Self {
705 Self {
706 constraints: Vec::new(),
707 }
708 }
709
710 /// Add a constraint to this set with conflict detection.
711 ///
712 /// This method adds a new constraint to the set after checking for conflicts
713 /// with existing constraints. If the new constraint would create an impossible
714 /// situation (like requiring two different exact versions), an error is returned.
715 ///
716 /// # Arguments
717 ///
718 /// * `constraint` - The [`VersionConstraint`] to add to this set
719 ///
720 /// # Returns
721 ///
722 /// Returns `Ok(())` if the constraint was added successfully, or `Err` if it
723 /// conflicts with existing constraints.
724 ///
725 /// # Conflict Detection
726 ///
727 /// Current conflict detection covers:
728 /// - **Exact version conflicts**: Different exact versions for the same dependency
729 /// - **Git ref conflicts**: Different Git references for the same dependency
730 ///
731 /// Future versions may add more sophisticated conflict detection for semantic
732 /// version ranges.
733 ///
734 /// # Examples
735 ///
736 /// ```rust,no_run
737 /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
738 ///
739 /// let mut set = ConstraintSet::new();
740 ///
741 /// // These constraints are compatible
742 /// set.add(VersionConstraint::parse(">=1.0.0")?)?;
743 /// set.add(VersionConstraint::parse("<2.0.0")?)?;
744 ///
745 /// // This would conflict with exact versions
746 /// set.add(VersionConstraint::parse("1.5.0")?)?;
747 /// let result = set.add(VersionConstraint::parse("1.6.0")?);
748 /// assert!(result.is_err()); // Conflict: can't be both 1.5.0 AND 1.6.0
749 /// # Ok::<(), anyhow::Error>(())
750 /// ```
751 pub fn add(&mut self, constraint: VersionConstraint) -> Result<()> {
752 // Check for conflicting constraints
753 if self.has_conflict(&constraint) {
754 return Err(AgpmError::Other {
755 message: format!("Constraint {constraint} conflicts with existing constraints"),
756 }
757 .into());
758 }
759
760 self.constraints.push(constraint);
761 Ok(())
762 }
763
764 /// Check if a version satisfies all constraints in this set.
765 ///
766 /// This method tests whether a given version passes all the constraints
767 /// in this set. For the version to be acceptable, it must satisfy every
768 /// single constraint - this represents a logical AND operation.
769 ///
770 /// # Arguments
771 ///
772 /// * `version` - The semantic version to test against all constraints
773 ///
774 /// # Returns
775 ///
776 /// Returns `true` if the version satisfies ALL constraints, `false` if it
777 /// fails to satisfy any constraint.
778 ///
779 /// # Examples
780 ///
781 /// ```rust,no_run
782 /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
783 /// use semver::Version;
784 ///
785 /// let mut set = ConstraintSet::new();
786 /// set.add(VersionConstraint::parse(">=1.0.0")?)?; // Must be at least 1.0.0
787 /// set.add(VersionConstraint::parse("<2.0.0")?)?; // Must be less than 2.0.0
788 /// set.add(VersionConstraint::parse("^1.0.0")?)?; // Must be compatible with 1.0.0
789 ///
790 /// assert!(set.satisfies(&Version::parse("1.5.0")?)); // Satisfies all three
791 /// assert!(!set.satisfies(&Version::parse("0.9.0")?)); // Fails >=1.0.0
792 /// assert!(!set.satisfies(&Version::parse("2.0.0")?)); // Fails <2.0.0
793 /// # Ok::<(), anyhow::Error>(())
794 /// ```
795 ///
796 /// # Performance Note
797 ///
798 /// This method short-circuits on the first constraint that fails, making it
799 /// efficient even with many constraints.
800 #[must_use]
801 pub fn satisfies(&self, version: &Version) -> bool {
802 self.constraints.iter().all(|c| c.matches(version))
803 }
804
805 /// Find the best matching version from a list of available versions.
806 ///
807 /// This method filters the provided versions to find those that satisfy all
808 /// constraints, then selects the "best" match according to AGPM's resolution
809 /// strategy. The selection prioritizes newer versions while respecting prerelease
810 /// preferences.
811 ///
812 /// # Resolution Strategy
813 ///
814 /// 1. **Filter candidates**: Keep only versions that satisfy all constraints
815 /// 2. **Sort by version**: Order candidates from highest to lowest version
816 /// 3. **Apply prerelease policy**: Remove prereleases unless explicitly allowed
817 /// 4. **Select best**: Return the highest remaining version
818 ///
819 /// # Arguments
820 ///
821 /// * `versions` - Slice of available versions to choose from
822 ///
823 /// # Returns
824 ///
825 /// Returns `Some(&Version)` with the best matching version, or `None` if no
826 /// version satisfies all constraints.
827 ///
828 /// # Examples
829 ///
830 /// ```rust,no_run
831 /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
832 /// use semver::Version;
833 ///
834 /// let mut set = ConstraintSet::new();
835 /// set.add(VersionConstraint::parse("^1.0.0")?)?;
836 ///
837 /// let versions = vec![
838 /// Version::parse("0.9.0")?, // Too old
839 /// Version::parse("1.0.0")?, // Compatible
840 /// Version::parse("1.2.0")?, // Compatible, newer
841 /// Version::parse("1.5.0")?, // Compatible, newest
842 /// Version::parse("2.0.0")?, // Too new
843 /// ];
844 ///
845 /// let best = set.find_best_match(&versions).unwrap();
846 /// assert_eq!(best, &Version::parse("1.5.0")?); // Highest compatible version
847 /// # Ok::<(), anyhow::Error>(())
848 /// ```
849 ///
850 /// ## Prerelease Handling
851 ///
852 /// ```rust,no_run
853 /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
854 /// use semver::Version;
855 ///
856 /// let mut set = ConstraintSet::new();
857 /// set.add(VersionConstraint::parse("^1.0.0")?)?; // Doesn't allow prereleases
858 ///
859 /// let versions = vec![
860 /// Version::parse("1.0.0")?,
861 /// Version::parse("1.1.0-alpha.1")?, // Prerelease
862 /// Version::parse("1.1.0")?, // Stable
863 /// ];
864 ///
865 /// let best = set.find_best_match(&versions).unwrap();
866 /// assert_eq!(best, &Version::parse("1.1.0")?); // Stable version preferred
867 /// # Ok::<(), anyhow::Error>(())
868 /// ```
869 #[must_use]
870 pub fn find_best_match<'a>(&self, versions: &'a [Version]) -> Option<&'a Version> {
871 let mut candidates: Vec<&Version> = versions.iter().filter(|v| self.satisfies(v)).collect();
872
873 // Sort by version (highest first)
874 candidates.sort_by(|a, b| b.cmp(a));
875
876 // If we don't allow prereleases, filter them out
877 if !self.allows_prerelease() {
878 candidates.retain(|v| v.pre.is_empty());
879 }
880
881 candidates.first().copied()
882 }
883
884 /// Check if any constraint in this set allows prerelease versions.
885 ///
886 /// This method determines the prerelease policy for the entire constraint set.
887 /// If ANY constraint in the set allows prereleases, the entire set is considered
888 /// to allow prereleases. This ensures that explicit prerelease constraints
889 /// (like `latest-prerelease` or Git refs) are respected.
890 ///
891 /// # Returns
892 ///
893 /// Returns `true` if any constraint allows prereleases, `false` if all constraints
894 /// exclude prereleases.
895 ///
896 /// # Examples
897 ///
898 /// ```rust,no_run
899 /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
900 ///
901 /// let mut stable_set = ConstraintSet::new();
902 /// stable_set.add(VersionConstraint::parse("^1.0.0")?)?;
903 /// stable_set.add(VersionConstraint::parse("~1.2.0")?)?;
904 /// assert!(!stable_set.allows_prerelease()); // All constraints exclude prereleases
905 ///
906 /// let mut prerelease_set = ConstraintSet::new();
907 /// prerelease_set.add(VersionConstraint::parse("^1.0.0")?)?;
908 /// prerelease_set.add(VersionConstraint::parse("main")?)?; // Git ref allows prereleases
909 /// assert!(prerelease_set.allows_prerelease()); // One constraint allows prereleases
910 /// # Ok::<(), anyhow::Error>(())
911 /// ```
912 ///
913 /// # Impact on Resolution
914 ///
915 /// This setting affects [`find_best_match`](Self::find_best_match) behavior:
916 /// - If `false`: Prerelease versions are filtered out before selection
917 /// - If `true`: Prerelease versions are included in selection
918 #[must_use]
919 pub fn allows_prerelease(&self) -> bool {
920 self.constraints.iter().any(VersionConstraint::allows_prerelease)
921 }
922
923 /// Check if a new constraint would conflict with existing constraints.
924 ///
925 /// This method performs conflict detection to prevent adding incompatible
926 /// constraints to the same set. It currently detects basic conflicts but
927 /// could be enhanced with more sophisticated analysis in the future.
928 ///
929 /// # Current Conflict Detection
930 ///
931 /// - **Exact version conflicts**: Two different exact versions (`1.0.0` vs `2.0.0`)
932 /// - **Git reference conflicts**: Two different Git refs (`main` vs `develop`)
933 ///
934 /// # Arguments
935 ///
936 /// * `new_constraint` - The constraint to test for conflicts
937 ///
938 /// # Returns
939 ///
940 /// Returns `true` if the constraint conflicts with existing ones, `false` if
941 /// it's compatible.
942 ///
943 /// # Future Enhancements
944 ///
945 /// Future versions could detect more sophisticated conflicts:
946 /// - Impossible version ranges (e.g., `>2.0.0` AND `<1.0.0`)
947 /// - Contradictory semver requirements
948 /// - Mixed version and Git reference constraints
949 ///
950 /// # Examples
951 ///
952 /// ```rust,no_run,ignore
953 /// use agpm_cli::version::constraints::{ConstraintSet, VersionConstraint};
954 ///
955 /// let mut set = ConstraintSet::new();
956 /// set.add(VersionConstraint::parse("1.0.0")?)?;
957 ///
958 /// // This would conflict (different exact versions)
959 /// let conflicting = VersionConstraint::parse("2.0.0")?;
960 /// assert!(set.has_conflict(&conflicting));
961 ///
962 /// // This would not conflict (same exact version)
963 /// let compatible = VersionConstraint::parse("1.0.0")?;
964 /// assert!(!set.has_conflict(&compatible));
965 /// # Ok::<(), anyhow::Error>(())
966 /// ```
967 fn has_conflict(&self, new_constraint: &VersionConstraint) -> bool {
968 // Simple conflict detection - can be enhanced
969 for existing in &self.constraints {
970 match (existing, new_constraint) {
971 (
972 VersionConstraint::Exact {
973 prefix: p1,
974 version: v1,
975 },
976 VersionConstraint::Exact {
977 prefix: p2,
978 version: v2,
979 },
980 ) => {
981 // Different prefixes = different namespaces, no conflict
982 if p1 != p2 {
983 continue;
984 }
985 // Same prefix (or both None), conflict if different versions
986 if v1 != v2 {
987 return true;
988 }
989 }
990 (VersionConstraint::GitRef(r1), VersionConstraint::GitRef(r2)) => {
991 if r1 != r2 {
992 return true;
993 }
994 }
995 // For Requirement constraints, different prefixes = no conflict
996 (
997 VersionConstraint::Exact {
998 prefix: p1,
999 ..
1000 }
1001 | VersionConstraint::Requirement {
1002 prefix: p1,
1003 ..
1004 },
1005 VersionConstraint::Requirement {
1006 prefix: p2,
1007 ..
1008 },
1009 )
1010 | (
1011 VersionConstraint::Requirement {
1012 prefix: p1,
1013 ..
1014 },
1015 VersionConstraint::Exact {
1016 prefix: p2,
1017 ..
1018 },
1019 ) => {
1020 // Different prefixes = different namespaces, no conflict
1021 if p1 != p2 {
1022 // Continue to next pair
1023 }
1024 // Same prefix - could do more sophisticated conflict detection here
1025 }
1026 _ => {
1027 // More sophisticated conflict detection could be added here
1028 }
1029 }
1030 }
1031 false
1032 }
1033}
1034
1035/// Manages version constraints for multiple dependencies and resolves them simultaneously.
1036///
1037/// `ConstraintResolver` coordinates version resolution across an entire dependency graph,
1038/// ensuring that all constraints are satisfied and conflicts are detected. It maintains
1039/// separate [`ConstraintSet`]s for each dependency and resolves them against available
1040/// version catalogs.
1041///
1042/// # Multi-Dependency Resolution
1043///
1044/// Unlike [`ConstraintSet`] which manages constraints for a single dependency, the
1045/// `ConstraintResolver` handles multiple dependencies simultaneously:
1046///
1047/// - Each dependency gets its own constraint set
1048/// - Constraints can be added incrementally
1049/// - Resolution happens across the entire dependency graph
1050/// - Missing dependencies are detected and reported
1051///
1052/// # Resolution Process
1053///
1054/// 1. **Collect constraints**: Gather all constraints for each dependency
1055/// 2. **Validate availability**: Ensure versions exist for all dependencies
1056/// 3. **Apply constraint sets**: Use each dependency's constraints to filter versions
1057/// 4. **Select best matches**: Choose optimal versions for each dependency
1058/// 5. **Return resolution map**: Provide final version selections
1059///
1060/// # Examples
1061///
1062/// ## Basic Multi-Dependency Resolution
1063///
1064/// ```rust,no_run
1065/// use agpm_cli::version::constraints::ConstraintResolver;
1066/// use semver::Version;
1067/// use std::collections::HashMap;
1068///
1069/// let mut resolver = ConstraintResolver::new();
1070///
1071/// // Add constraints for multiple dependencies
1072/// resolver.add_constraint("dep1", "^1.0.0")?;
1073/// resolver.add_constraint("dep2", "~2.1.0")?;
1074/// resolver.add_constraint("dep3", "main")?;
1075///
1076/// // Provide available versions for each dependency
1077/// let mut available = HashMap::new();
1078/// available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
1079/// available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
1080/// available.insert("dep3".to_string(), vec![Version::parse("3.0.0")?]);
1081///
1082/// // Resolve all dependencies
1083/// let resolved = resolver.resolve(&available)?;
1084/// assert_eq!(resolved.len(), 3);
1085/// # Ok::<(), anyhow::Error>(())
1086/// ```
1087///
1088/// ## Incremental Constraint Addition
1089///
1090/// ```rust,no_run
1091/// use agpm_cli::version::constraints::ConstraintResolver;
1092///
1093/// let mut resolver = ConstraintResolver::new();
1094///
1095/// // Add multiple constraints for the same dependency
1096/// resolver.add_constraint("my-dep", ">=1.0.0")?;
1097/// resolver.add_constraint("my-dep", "<2.0.0")?;
1098/// resolver.add_constraint("my-dep", "^1.5.0")?;
1099///
1100/// // All constraints will be combined into a single constraint set
1101/// # Ok::<(), anyhow::Error>(())
1102/// ```
1103///
1104/// # Error Conditions
1105///
1106/// The resolver reports several types of errors:
1107///
1108/// - **Missing dependencies**: A constraint exists but no versions are available
1109/// - **Unsatisfiable constraints**: No available version meets all requirements
1110/// - **Conflicting constraints**: Impossible constraint combinations
1111///
1112/// # Use Cases
1113///
1114/// This resolver is particularly useful for:
1115/// - Package managers resolving dependency graphs
1116/// - Build systems selecting compatible versions
1117/// - Configuration management ensuring consistent environments
1118/// - Update analysis determining safe upgrade paths
1119pub struct ConstraintResolver {
1120 constraints: HashMap<String, ConstraintSet>,
1121}
1122
1123impl Default for ConstraintResolver {
1124 fn default() -> Self {
1125 Self::new()
1126 }
1127}
1128
1129impl ConstraintResolver {
1130 /// Creates a new constraint resolver
1131 ///
1132 /// # Returns
1133 ///
1134 /// Returns a new `ConstraintResolver` with empty constraint and resolution maps
1135 #[must_use]
1136 pub fn new() -> Self {
1137 Self {
1138 constraints: HashMap::new(),
1139 }
1140 }
1141
1142 /// Add a version constraint for a specific dependency.
1143 ///
1144 /// This method parses the constraint string and adds it to the constraint set
1145 /// for the named dependency. If this is the first constraint for the dependency,
1146 /// a new constraint set is created. Multiple constraints for the same dependency
1147 /// are combined into a single set with conflict detection.
1148 ///
1149 /// # Arguments
1150 ///
1151 /// * `dependency` - The name of the dependency to constrain
1152 /// * `constraint` - The constraint string to parse and add (e.g., "^1.0.0", "latest")
1153 ///
1154 /// # Returns
1155 ///
1156 /// Returns `Ok(())` if the constraint was added successfully, or `Err` if:
1157 /// - The constraint string is invalid
1158 /// - The constraint conflicts with existing constraints for this dependency
1159 ///
1160 /// # Examples
1161 ///
1162 /// ```rust,no_run
1163 /// use agpm_cli::version::constraints::ConstraintResolver;
1164 ///
1165 /// let mut resolver = ConstraintResolver::new();
1166 ///
1167 /// // Add constraints for different dependencies
1168 /// resolver.add_constraint("web-framework", "^2.0.0")?;
1169 /// resolver.add_constraint("database", "~1.5.0")?;
1170 /// resolver.add_constraint("auth-lib", "main")?;
1171 ///
1172 /// // Add multiple constraints for the same dependency
1173 /// resolver.add_constraint("api-client", ">=1.0.0")?;
1174 /// resolver.add_constraint("api-client", "<2.0.0")?; // Compatible range
1175 ///
1176 /// // This would fail - conflicting exact versions
1177 /// resolver.add_constraint("my-dep", "1.0.0")?;
1178 /// let result = resolver.add_constraint("my-dep", "2.0.0");
1179 /// assert!(result.is_err());
1180 /// # Ok::<(), anyhow::Error>(())
1181 /// ```
1182 ///
1183 /// # Constraint Combination
1184 ///
1185 /// When multiple constraints are added for the same dependency, they are
1186 /// combined using AND logic. The final constraint set requires that all
1187 /// individual constraints be satisfied simultaneously.
1188 pub fn add_constraint(&mut self, dependency: &str, constraint: &str) -> Result<()> {
1189 let parsed = VersionConstraint::parse(constraint)?;
1190
1191 self.constraints.entry(dependency.to_string()).or_default().add(parsed)?;
1192
1193 Ok(())
1194 }
1195
1196 /// Resolve all dependency constraints and return the best version for each.
1197 ///
1198 /// This method performs the core resolution algorithm, taking all accumulated
1199 /// constraints and finding the best matching version for each dependency from
1200 /// the provided catalog of available versions.
1201 ///
1202 /// # Resolution Algorithm
1203 ///
1204 /// For each dependency with constraints:
1205 /// 1. **Verify availability**: Check that versions exist for the dependency
1206 /// 2. **Apply constraints**: Filter versions using the dependency's constraint set
1207 /// 3. **Select best match**: Choose the highest compatible version
1208 /// 4. **Handle prereleases**: Apply prerelease policies appropriately
1209 ///
1210 /// # Arguments
1211 ///
1212 /// * `available_versions` - Map from dependency names to lists of available versions
1213 ///
1214 /// # Returns
1215 ///
1216 /// Returns `Ok(HashMap<String, Version>)` with the resolved version for each
1217 /// dependency, or `Err` if resolution fails.
1218 ///
1219 /// # Error Conditions
1220 ///
1221 /// - **Missing dependency**: Constraint exists but no versions available
1222 /// - **No satisfying version**: Available versions don't meet constraints
1223 /// - **Internal errors**: Constraint set conflicts or parsing failures
1224 ///
1225 /// # Examples
1226 ///
1227 /// ```rust,no_run
1228 /// use agpm_cli::version::constraints::ConstraintResolver;
1229 /// use semver::Version;
1230 /// use std::collections::HashMap;
1231 ///
1232 /// let mut resolver = ConstraintResolver::new();
1233 /// resolver.add_constraint("web-server", "^1.0.0")?;
1234 /// resolver.add_constraint("database", "~2.1.0")?;
1235 ///
1236 /// // Provide version catalog
1237 /// let mut available = HashMap::new();
1238 /// available.insert(
1239 /// "web-server".to_string(),
1240 /// vec![
1241 /// Version::parse("1.0.0")?,
1242 /// Version::parse("1.2.0")?,
1243 /// Version::parse("1.5.0")?, // Best match for ^1.0.0
1244 /// Version::parse("2.0.0")?, // Too new
1245 /// ],
1246 /// );
1247 /// available.insert(
1248 /// "database".to_string(),
1249 /// vec![
1250 /// Version::parse("2.1.0")?,
1251 /// Version::parse("2.1.3")?, // Best match for ~2.1.0
1252 /// Version::parse("2.2.0")?, // Too new
1253 /// ],
1254 /// );
1255 ///
1256 /// // Resolve dependencies
1257 /// let resolved = resolver.resolve(&available)?;
1258 /// assert_eq!(resolved["web-server"], Version::parse("1.5.0")?);
1259 /// assert_eq!(resolved["database"], Version::parse("2.1.3")?);
1260 /// # Ok::<(), anyhow::Error>(())
1261 /// ```
1262 ///
1263 /// ## Error Handling
1264 ///
1265 /// ```rust,no_run
1266 /// use agpm_cli::version::constraints::ConstraintResolver;
1267 /// use std::collections::HashMap;
1268 ///
1269 /// let mut resolver = ConstraintResolver::new();
1270 /// resolver.add_constraint("missing-dep", "^1.0.0")?;
1271 ///
1272 /// let available = HashMap::new(); // No versions provided
1273 ///
1274 /// let result = resolver.resolve(&available);
1275 /// assert!(result.is_err()); // Missing dependency error
1276 /// # Ok::<(), anyhow::Error>(())
1277 /// ```
1278 ///
1279 /// # Performance Considerations
1280 ///
1281 /// - Resolution is performed independently for each dependency
1282 /// - Version filtering and sorting may be expensive for large version lists
1283 /// - Consider pre-filtering available versions if catalogs are very large
1284 pub fn resolve(
1285 &self,
1286 available_versions: &HashMap<String, Vec<Version>>,
1287 ) -> Result<HashMap<String, Version>> {
1288 let mut resolved = HashMap::new();
1289
1290 for (dep, constraint_set) in &self.constraints {
1291 let versions = available_versions.get(dep).ok_or_else(|| AgpmError::Other {
1292 message: format!("No versions available for dependency: {dep}"),
1293 })?;
1294
1295 let best_match =
1296 constraint_set.find_best_match(versions).ok_or_else(|| AgpmError::Other {
1297 message: format!("No version satisfies constraints for dependency: {dep}"),
1298 })?;
1299
1300 resolved.insert(dep.clone(), best_match.clone());
1301 }
1302
1303 Ok(resolved)
1304 }
1305}
1306
1307#[cfg(test)]
1308mod tests {
1309 use super::*;
1310
1311 #[test]
1312 fn test_version_constraint_parse() {
1313 // Exact version
1314 let constraint = VersionConstraint::parse("1.0.0").unwrap();
1315 assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1316
1317 // Version with v prefix
1318 let constraint = VersionConstraint::parse("v1.0.0").unwrap();
1319 assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1320
1321 // Caret requirement
1322 let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1323 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1324
1325 // Tilde requirement
1326 let constraint = VersionConstraint::parse("~1.2.0").unwrap();
1327 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1328
1329 // Range requirement
1330 let constraint = VersionConstraint::parse(">=1.0.0, <2.0.0").unwrap();
1331 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1332
1333 // Git refs (including "latest" - it's just a tag name)
1334 let constraint = VersionConstraint::parse("latest").unwrap();
1335 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1336
1337 let constraint = VersionConstraint::parse("main").unwrap();
1338 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1339 }
1340
1341 #[test]
1342 fn test_constraint_matching() {
1343 let v100 = Version::parse("1.0.0").unwrap();
1344 let v110 = Version::parse("1.1.0").unwrap();
1345 let v200 = Version::parse("2.0.0").unwrap();
1346
1347 // Exact match
1348 let exact = VersionConstraint::Exact {
1349 prefix: None,
1350 version: v100.clone(),
1351 };
1352 assert!(exact.matches(&v100));
1353 assert!(!exact.matches(&v110));
1354
1355 // Caret requirement
1356 let caret = VersionConstraint::parse("^1.0.0").unwrap();
1357 assert!(caret.matches(&v100));
1358 assert!(caret.matches(&v110));
1359 assert!(!caret.matches(&v200));
1360
1361 // Git refs don't match semantic versions
1362 let git_ref = VersionConstraint::GitRef("latest".to_string());
1363 assert!(!git_ref.matches(&v100));
1364 assert!(!git_ref.matches(&v200));
1365 }
1366
1367 #[test]
1368 fn test_constraint_set() {
1369 let mut set = ConstraintSet::new();
1370 set.add(VersionConstraint::parse(">=1.0.0").unwrap()).unwrap();
1371 set.add(VersionConstraint::parse("<2.0.0").unwrap()).unwrap();
1372
1373 let v090 = Version::parse("0.9.0").unwrap();
1374 let v100 = Version::parse("1.0.0").unwrap();
1375 let v150 = Version::parse("1.5.0").unwrap();
1376 let v200 = Version::parse("2.0.0").unwrap();
1377
1378 assert!(!set.satisfies(&v090));
1379 assert!(set.satisfies(&v100));
1380 assert!(set.satisfies(&v150));
1381 assert!(!set.satisfies(&v200));
1382 }
1383
1384 #[test]
1385 fn test_find_best_match() {
1386 let mut set = ConstraintSet::new();
1387 set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1388
1389 let versions = vec![
1390 Version::parse("0.9.0").unwrap(),
1391 Version::parse("1.0.0").unwrap(),
1392 Version::parse("1.2.0").unwrap(),
1393 Version::parse("1.5.0").unwrap(),
1394 Version::parse("2.0.0").unwrap(),
1395 ];
1396
1397 let best = set.find_best_match(&versions).unwrap();
1398 assert_eq!(best, &Version::parse("1.5.0").unwrap());
1399 }
1400
1401 #[test]
1402 fn test_constraint_conflicts() {
1403 let mut set = ConstraintSet::new();
1404
1405 // Add first exact version
1406 set.add(VersionConstraint::Exact {
1407 prefix: None,
1408 version: Version::parse("1.0.0").unwrap(),
1409 })
1410 .unwrap();
1411
1412 // Try to add conflicting exact version
1413 let result = set.add(VersionConstraint::Exact {
1414 prefix: None,
1415 version: Version::parse("2.0.0").unwrap(),
1416 });
1417 assert!(result.is_err());
1418
1419 // Adding the same version should be ok
1420 let result = set.add(VersionConstraint::Exact {
1421 prefix: None,
1422 version: Version::parse("1.0.0").unwrap(),
1423 });
1424 assert!(result.is_ok());
1425 }
1426
1427 #[test]
1428 fn test_constraint_resolver() {
1429 let mut resolver = ConstraintResolver::new();
1430
1431 resolver.add_constraint("dep1", "^1.0.0").unwrap();
1432 resolver.add_constraint("dep2", "~2.1.0").unwrap();
1433
1434 let mut available = HashMap::new();
1435 available.insert(
1436 "dep1".to_string(),
1437 vec![
1438 Version::parse("0.9.0").unwrap(),
1439 Version::parse("1.0.0").unwrap(),
1440 Version::parse("1.5.0").unwrap(),
1441 Version::parse("2.0.0").unwrap(),
1442 ],
1443 );
1444 available.insert(
1445 "dep2".to_string(),
1446 vec![
1447 Version::parse("2.0.0").unwrap(),
1448 Version::parse("2.1.0").unwrap(),
1449 Version::parse("2.1.5").unwrap(),
1450 Version::parse("2.2.0").unwrap(),
1451 ],
1452 );
1453
1454 let resolved = resolver.resolve(&available).unwrap();
1455 assert_eq!(resolved.get("dep1"), Some(&Version::parse("1.5.0").unwrap()));
1456 assert_eq!(resolved.get("dep2"), Some(&Version::parse("2.1.5").unwrap()));
1457 }
1458
1459 #[test]
1460 fn test_allows_prerelease() {
1461 assert!(VersionConstraint::GitRef("main".to_string()).allows_prerelease());
1462 assert!(VersionConstraint::GitRef("latest".to_string()).allows_prerelease()); // Git ref
1463 assert!(
1464 !VersionConstraint::Exact {
1465 prefix: None,
1466 version: Version::parse("1.0.0").unwrap()
1467 }
1468 .allows_prerelease()
1469 );
1470 }
1471
1472 #[test]
1473 fn test_version_constraint_parse_edge_cases() {
1474 // Test latest-prerelease (just a tag name)
1475 let constraint = VersionConstraint::parse("latest-prerelease").unwrap();
1476 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1477
1478 // Test asterisk wildcard
1479 let constraint = VersionConstraint::parse("*").unwrap();
1480 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1481
1482 // Test range operators
1483 let constraint = VersionConstraint::parse(">=1.0.0").unwrap();
1484 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1485
1486 let constraint = VersionConstraint::parse("<2.0.0").unwrap();
1487 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1488
1489 let constraint = VersionConstraint::parse("=1.0.0").unwrap();
1490 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1491
1492 // Test git branch names
1493 let constraint = VersionConstraint::parse("feature/new-feature").unwrap();
1494 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1495
1496 // Test commit hash
1497 let constraint = VersionConstraint::parse("abc123def456").unwrap();
1498 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1499 }
1500
1501 #[test]
1502 fn test_version_constraint_display() {
1503 let exact = VersionConstraint::Exact {
1504 prefix: None,
1505 version: Version::parse("1.0.0").unwrap(),
1506 };
1507 assert_eq!(format!("{exact}"), "1.0.0");
1508
1509 let req = VersionConstraint::parse("^1.0.0").unwrap();
1510 assert_eq!(format!("{req}"), "^1.0.0");
1511
1512 let git_ref = VersionConstraint::GitRef("main".to_string());
1513 assert_eq!(format!("{git_ref}"), "main");
1514
1515 let latest = VersionConstraint::GitRef("latest".to_string());
1516 assert_eq!(format!("{latest}"), "latest");
1517 }
1518
1519 #[test]
1520 fn test_version_constraint_matches_ref() {
1521 let git_ref = VersionConstraint::GitRef("main".to_string());
1522 assert!(git_ref.matches_ref("main"));
1523 assert!(!git_ref.matches_ref("develop"));
1524
1525 // Other constraint types should return false for ref matching
1526 let exact = VersionConstraint::Exact {
1527 prefix: None,
1528 version: Version::parse("1.0.0").unwrap(),
1529 };
1530 assert!(!exact.matches_ref("v1.0.0"));
1531
1532 let latest = VersionConstraint::GitRef("latest".to_string());
1533 assert!(latest.matches_ref("latest"));
1534 }
1535
1536 #[test]
1537 fn test_version_constraint_to_version_req() {
1538 let exact = VersionConstraint::Exact {
1539 prefix: None,
1540 version: Version::parse("1.0.0").unwrap(),
1541 };
1542 let req = exact.to_version_req().unwrap();
1543 assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1544
1545 let caret = VersionConstraint::parse("^1.0.0").unwrap();
1546 let req = caret.to_version_req().unwrap();
1547 assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1548
1549 let git_ref = VersionConstraint::GitRef("main".to_string());
1550 assert!(git_ref.to_version_req().is_none());
1551
1552 let latest = VersionConstraint::GitRef("latest".to_string());
1553 assert!(latest.to_version_req().is_none()); // Git ref - cannot convert
1554 }
1555
1556 #[test]
1557 fn test_constraint_set_with_prereleases() {
1558 let mut set = ConstraintSet::new();
1559 set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1560
1561 let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1562 let v100 = Version::parse("1.0.0").unwrap();
1563
1564 assert!(set.allows_prerelease());
1565
1566 // Git refs don't match semver versions
1567 let versions = vec![v100_pre.clone(), v100.clone()];
1568 let best = set.find_best_match(&versions);
1569 assert!(best.is_none()); // Git refs don't match semver
1570 }
1571
1572 #[test]
1573 fn test_constraint_set_no_matches() {
1574 let mut set = ConstraintSet::new();
1575 set.add(VersionConstraint::parse(">=2.0.0").unwrap()).unwrap();
1576
1577 let versions = vec![Version::parse("1.0.0").unwrap(), Version::parse("1.5.0").unwrap()];
1578
1579 let best = set.find_best_match(&versions);
1580 assert!(best.is_none());
1581 }
1582
1583 #[test]
1584 fn test_constraint_resolver_missing_dependency() {
1585 let mut resolver = ConstraintResolver::new();
1586 resolver.add_constraint("dep1", "^1.0.0").unwrap();
1587
1588 let available = HashMap::new(); // No versions available
1589
1590 let result = resolver.resolve(&available);
1591 assert!(result.is_err());
1592 }
1593
1594 #[test]
1595 fn test_constraint_resolver_no_satisfying_version() {
1596 let mut resolver = ConstraintResolver::new();
1597 resolver.add_constraint("dep1", "^2.0.0").unwrap();
1598
1599 let mut available = HashMap::new();
1600 available.insert(
1601 "dep1".to_string(),
1602 vec![Version::parse("1.0.0").unwrap()], // Only 1.x available, but we need 2.x
1603 );
1604
1605 let result = resolver.resolve(&available);
1606 assert!(result.is_err());
1607 }
1608
1609 #[test]
1610 fn test_constraint_set_git_ref_conflicts() {
1611 let mut set = ConstraintSet::new();
1612
1613 // Add first git ref
1614 set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1615
1616 // Try to add conflicting git ref
1617 let result = set.add(VersionConstraint::GitRef("develop".to_string()));
1618 assert!(result.is_err());
1619
1620 // Adding the same ref should be ok
1621 let result = set.add(VersionConstraint::GitRef("main".to_string()));
1622 assert!(result.is_ok());
1623 }
1624
1625 #[test]
1626 fn test_git_ref_constraint_with_versions() {
1627 let git_ref = VersionConstraint::GitRef("latest".to_string());
1628
1629 let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1630 let v100 = Version::parse("1.0.0").unwrap();
1631
1632 // Git refs don't match semantic versions
1633 assert!(!git_ref.matches(&v100));
1634 assert!(!git_ref.matches(&v100_pre));
1635 }
1636
1637 #[test]
1638 fn test_git_ref_allows_prereleases() {
1639 let git_ref = VersionConstraint::GitRef("latest".to_string());
1640
1641 // Git refs allow prereleases (they reference commits)
1642 assert!(git_ref.allows_prerelease());
1643
1644 let main_ref = VersionConstraint::GitRef("main".to_string());
1645 assert!(main_ref.allows_prerelease());
1646 }
1647
1648 #[test]
1649 fn test_requirement_constraint_allows_prerelease() {
1650 let req = VersionConstraint::parse("^1.0.0").unwrap();
1651 assert!(!req.allows_prerelease());
1652
1653 let exact = VersionConstraint::Exact {
1654 prefix: None,
1655 version: Version::parse("1.0.0").unwrap(),
1656 };
1657 assert!(!exact.allows_prerelease());
1658 }
1659
1660 #[test]
1661 fn test_constraint_set_prerelease_filtering() {
1662 let mut set = ConstraintSet::new();
1663 set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1664
1665 let versions = vec![
1666 Version::parse("1.0.0-alpha.1").unwrap(),
1667 Version::parse("1.0.0").unwrap(),
1668 Version::parse("1.1.0-beta.1").unwrap(),
1669 Version::parse("1.1.0").unwrap(),
1670 ];
1671
1672 let best = set.find_best_match(&versions).unwrap();
1673 assert_eq!(best, &Version::parse("1.1.0").unwrap()); // Should pick highest stable
1674 }
1675
1676 #[test]
1677 fn test_parse_with_whitespace() {
1678 let constraint = VersionConstraint::parse(" 1.0.0 ").unwrap();
1679 assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1680
1681 let constraint = VersionConstraint::parse(" latest ").unwrap();
1682 assert!(matches!(constraint, VersionConstraint::GitRef(_))); // Just a git ref
1683
1684 let constraint = VersionConstraint::parse(" ^1.0.0 ").unwrap();
1685 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1686 }
1687
1688 #[test]
1689 fn test_constraint_resolver_add_constraint_error() {
1690 let mut resolver = ConstraintResolver::new();
1691
1692 // Add a valid constraint first
1693 resolver.add_constraint("dep1", "1.0.0").unwrap();
1694
1695 // Add conflicting constraint
1696 let result = resolver.add_constraint("dep1", "2.0.0");
1697 assert!(result.is_err());
1698 }
1699
1700 #[test]
1701 fn test_constraint_set_no_conflict_different_types() {
1702 let mut set = ConstraintSet::new();
1703
1704 // These shouldn't conflict as they are different types
1705 set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1706 set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1707
1708 // Should have 2 constraints
1709 assert_eq!(set.constraints.len(), 2);
1710 }
1711
1712 #[test]
1713 fn test_git_ref_to_version_req() {
1714 let git_ref = VersionConstraint::GitRef("latest".to_string());
1715 // Git refs cannot be converted to version requirements
1716 assert!(git_ref.to_version_req().is_none());
1717
1718 let main_ref = VersionConstraint::GitRef("main".to_string());
1719 assert!(main_ref.to_version_req().is_none());
1720 }
1721
1722 // ========== Prefix Support Tests ==========
1723
1724 #[test]
1725 fn test_prefixed_constraint_parsing() {
1726 // Prefixed exact version
1727 let constraint = VersionConstraint::parse("agents-v1.0.0").unwrap();
1728 match constraint {
1729 VersionConstraint::Exact {
1730 prefix,
1731 version,
1732 } => {
1733 assert_eq!(prefix, Some("agents".to_string()));
1734 assert_eq!(version, Version::parse("1.0.0").unwrap());
1735 }
1736 _ => panic!("Expected Exact constraint"),
1737 }
1738
1739 // Prefixed requirement
1740 let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1741 match constraint {
1742 VersionConstraint::Requirement {
1743 prefix,
1744 req,
1745 } => {
1746 assert_eq!(prefix, Some("agents".to_string()));
1747 assert!(req.matches(&Version::parse("1.5.0").unwrap()));
1748 assert!(!req.matches(&Version::parse("2.0.0").unwrap()));
1749 }
1750 _ => panic!("Expected Requirement constraint"),
1751 }
1752
1753 // Unprefixed constraint (backward compatible)
1754 let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1755 match constraint {
1756 VersionConstraint::Requirement {
1757 prefix,
1758 ..
1759 } => {
1760 assert_eq!(prefix, None);
1761 }
1762 _ => panic!("Expected Requirement constraint"),
1763 }
1764 }
1765
1766 #[test]
1767 fn test_prefixed_constraint_display() {
1768 let prefixed_exact = VersionConstraint::Exact {
1769 prefix: Some("agents".to_string()),
1770 version: Version::parse("1.0.0").unwrap(),
1771 };
1772 assert_eq!(prefixed_exact.to_string(), "agents-1.0.0");
1773
1774 let unprefixed_exact = VersionConstraint::Exact {
1775 prefix: None,
1776 version: Version::parse("1.0.0").unwrap(),
1777 };
1778 assert_eq!(unprefixed_exact.to_string(), "1.0.0");
1779
1780 let prefixed_req = VersionConstraint::parse("snippets-^v2.0.0").unwrap();
1781 let display = prefixed_req.to_string();
1782 assert!(display.starts_with("snippets-"));
1783 }
1784
1785 #[test]
1786 fn test_matches_version_info() {
1787 use crate::version::VersionInfo;
1788
1789 // Prefixed constraint matching prefixed version
1790 let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1791 let version_info = VersionInfo {
1792 prefix: Some("agents".to_string()),
1793 version: Version::parse("1.2.0").unwrap(),
1794 tag: "agents-v1.2.0".to_string(),
1795 prerelease: false,
1796 };
1797 assert!(constraint.matches_version_info(&version_info));
1798
1799 // Prefixed constraint NOT matching different prefix
1800 let wrong_prefix = VersionInfo {
1801 prefix: Some("snippets".to_string()),
1802 version: Version::parse("1.2.0").unwrap(),
1803 tag: "snippets-v1.2.0".to_string(),
1804 prerelease: false,
1805 };
1806 assert!(!constraint.matches_version_info(&wrong_prefix));
1807
1808 // Unprefixed constraint matching unprefixed version
1809 let unprefixed_constraint = VersionConstraint::parse("^1.0.0").unwrap();
1810 let unprefixed_version = VersionInfo {
1811 prefix: None,
1812 version: Version::parse("1.5.0").unwrap(),
1813 tag: "v1.5.0".to_string(),
1814 prerelease: false,
1815 };
1816 assert!(unprefixed_constraint.matches_version_info(&unprefixed_version));
1817
1818 // Unprefixed constraint NOT matching prefixed version
1819 assert!(!unprefixed_constraint.matches_version_info(&version_info));
1820 }
1821
1822 #[test]
1823 fn test_prefixed_constraint_conflicts() {
1824 let mut set = ConstraintSet::new();
1825
1826 // Add prefixed constraint
1827 set.add(VersionConstraint::parse("agents-^v1.0.0").unwrap()).unwrap();
1828
1829 // Different prefix should not conflict
1830 let result = set.add(VersionConstraint::parse("snippets-^v1.0.0").unwrap());
1831 assert!(result.is_ok());
1832
1833 // Same prefix but compatible constraints should not conflict
1834 let result = set.add(VersionConstraint::parse("agents-~v1.2.0").unwrap());
1835 assert!(result.is_ok());
1836
1837 // Different prefixes for Exact constraints
1838 let mut exact_set = ConstraintSet::new();
1839 exact_set.add(VersionConstraint::parse("agents-v1.0.0").unwrap()).unwrap();
1840
1841 // Different prefix, same version - should not conflict
1842 let result = exact_set.add(VersionConstraint::parse("snippets-v1.0.0").unwrap());
1843 assert!(result.is_ok());
1844 }
1845
1846 #[test]
1847 fn test_prefix_with_hyphens() {
1848 // Multiple hyphens in prefix
1849 let constraint = VersionConstraint::parse("my-cool-agent-v1.0.0").unwrap();
1850 match constraint {
1851 VersionConstraint::Exact {
1852 prefix,
1853 version,
1854 } => {
1855 assert_eq!(prefix, Some("my-cool-agent".to_string()));
1856 assert_eq!(version, Version::parse("1.0.0").unwrap());
1857 }
1858 _ => panic!("Expected Exact constraint"),
1859 }
1860
1861 // Prefix ending with 'v'
1862 let constraint = VersionConstraint::parse("tool-v-v1.0.0").unwrap();
1863 match constraint {
1864 VersionConstraint::Exact {
1865 prefix,
1866 version,
1867 } => {
1868 assert_eq!(prefix, Some("tool-v".to_string()));
1869 assert_eq!(version, Version::parse("1.0.0").unwrap());
1870 }
1871 _ => panic!("Expected Exact constraint"),
1872 }
1873 }
1874}