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: p2,
1003 ..
1004 },
1005 )
1006 | (
1007 VersionConstraint::Requirement {
1008 prefix: p1,
1009 ..
1010 },
1011 VersionConstraint::Exact {
1012 prefix: p2,
1013 ..
1014 },
1015 )
1016 | (
1017 VersionConstraint::Requirement {
1018 prefix: p1,
1019 ..
1020 },
1021 VersionConstraint::Requirement {
1022 prefix: p2,
1023 ..
1024 },
1025 ) => {
1026 // Different prefixes = different namespaces, no conflict
1027 if p1 != p2 {
1028 continue;
1029 }
1030 // Same prefix - could do more sophisticated conflict detection here
1031 }
1032 _ => {
1033 // More sophisticated conflict detection could be added here
1034 }
1035 }
1036 }
1037 false
1038 }
1039}
1040
1041/// Manages version constraints for multiple dependencies and resolves them simultaneously.
1042///
1043/// `ConstraintResolver` coordinates version resolution across an entire dependency graph,
1044/// ensuring that all constraints are satisfied and conflicts are detected. It maintains
1045/// separate [`ConstraintSet`]s for each dependency and resolves them against available
1046/// version catalogs.
1047///
1048/// # Multi-Dependency Resolution
1049///
1050/// Unlike [`ConstraintSet`] which manages constraints for a single dependency, the
1051/// `ConstraintResolver` handles multiple dependencies simultaneously:
1052///
1053/// - Each dependency gets its own constraint set
1054/// - Constraints can be added incrementally
1055/// - Resolution happens across the entire dependency graph
1056/// - Missing dependencies are detected and reported
1057///
1058/// # Resolution Process
1059///
1060/// 1. **Collect constraints**: Gather all constraints for each dependency
1061/// 2. **Validate availability**: Ensure versions exist for all dependencies
1062/// 3. **Apply constraint sets**: Use each dependency's constraints to filter versions
1063/// 4. **Select best matches**: Choose optimal versions for each dependency
1064/// 5. **Return resolution map**: Provide final version selections
1065///
1066/// # Examples
1067///
1068/// ## Basic Multi-Dependency Resolution
1069///
1070/// ```rust,no_run
1071/// use agpm_cli::version::constraints::ConstraintResolver;
1072/// use semver::Version;
1073/// use std::collections::HashMap;
1074///
1075/// let mut resolver = ConstraintResolver::new();
1076///
1077/// // Add constraints for multiple dependencies
1078/// resolver.add_constraint("dep1", "^1.0.0")?;
1079/// resolver.add_constraint("dep2", "~2.1.0")?;
1080/// resolver.add_constraint("dep3", "main")?;
1081///
1082/// // Provide available versions for each dependency
1083/// let mut available = HashMap::new();
1084/// available.insert("dep1".to_string(), vec![Version::parse("1.5.0")?]);
1085/// available.insert("dep2".to_string(), vec![Version::parse("2.1.3")?]);
1086/// available.insert("dep3".to_string(), vec![Version::parse("3.0.0")?]);
1087///
1088/// // Resolve all dependencies
1089/// let resolved = resolver.resolve(&available)?;
1090/// assert_eq!(resolved.len(), 3);
1091/// # Ok::<(), anyhow::Error>(())
1092/// ```
1093///
1094/// ## Incremental Constraint Addition
1095///
1096/// ```rust,no_run
1097/// use agpm_cli::version::constraints::ConstraintResolver;
1098///
1099/// let mut resolver = ConstraintResolver::new();
1100///
1101/// // Add multiple constraints for the same dependency
1102/// resolver.add_constraint("my-dep", ">=1.0.0")?;
1103/// resolver.add_constraint("my-dep", "<2.0.0")?;
1104/// resolver.add_constraint("my-dep", "^1.5.0")?;
1105///
1106/// // All constraints will be combined into a single constraint set
1107/// # Ok::<(), anyhow::Error>(())
1108/// ```
1109///
1110/// # Error Conditions
1111///
1112/// The resolver reports several types of errors:
1113///
1114/// - **Missing dependencies**: A constraint exists but no versions are available
1115/// - **Unsatisfiable constraints**: No available version meets all requirements
1116/// - **Conflicting constraints**: Impossible constraint combinations
1117///
1118/// # Use Cases
1119///
1120/// This resolver is particularly useful for:
1121/// - Package managers resolving dependency graphs
1122/// - Build systems selecting compatible versions
1123/// - Configuration management ensuring consistent environments
1124/// - Update analysis determining safe upgrade paths
1125pub struct ConstraintResolver {
1126 constraints: HashMap<String, ConstraintSet>,
1127}
1128
1129impl Default for ConstraintResolver {
1130 fn default() -> Self {
1131 Self::new()
1132 }
1133}
1134
1135impl ConstraintResolver {
1136 /// Creates a new constraint resolver
1137 ///
1138 /// # Returns
1139 ///
1140 /// Returns a new `ConstraintResolver` with empty constraint and resolution maps
1141 #[must_use]
1142 pub fn new() -> Self {
1143 Self {
1144 constraints: HashMap::new(),
1145 }
1146 }
1147
1148 /// Add a version constraint for a specific dependency.
1149 ///
1150 /// This method parses the constraint string and adds it to the constraint set
1151 /// for the named dependency. If this is the first constraint for the dependency,
1152 /// a new constraint set is created. Multiple constraints for the same dependency
1153 /// are combined into a single set with conflict detection.
1154 ///
1155 /// # Arguments
1156 ///
1157 /// * `dependency` - The name of the dependency to constrain
1158 /// * `constraint` - The constraint string to parse and add (e.g., "^1.0.0", "latest")
1159 ///
1160 /// # Returns
1161 ///
1162 /// Returns `Ok(())` if the constraint was added successfully, or `Err` if:
1163 /// - The constraint string is invalid
1164 /// - The constraint conflicts with existing constraints for this dependency
1165 ///
1166 /// # Examples
1167 ///
1168 /// ```rust,no_run
1169 /// use agpm_cli::version::constraints::ConstraintResolver;
1170 ///
1171 /// let mut resolver = ConstraintResolver::new();
1172 ///
1173 /// // Add constraints for different dependencies
1174 /// resolver.add_constraint("web-framework", "^2.0.0")?;
1175 /// resolver.add_constraint("database", "~1.5.0")?;
1176 /// resolver.add_constraint("auth-lib", "main")?;
1177 ///
1178 /// // Add multiple constraints for the same dependency
1179 /// resolver.add_constraint("api-client", ">=1.0.0")?;
1180 /// resolver.add_constraint("api-client", "<2.0.0")?; // Compatible range
1181 ///
1182 /// // This would fail - conflicting exact versions
1183 /// resolver.add_constraint("my-dep", "1.0.0")?;
1184 /// let result = resolver.add_constraint("my-dep", "2.0.0");
1185 /// assert!(result.is_err());
1186 /// # Ok::<(), anyhow::Error>(())
1187 /// ```
1188 ///
1189 /// # Constraint Combination
1190 ///
1191 /// When multiple constraints are added for the same dependency, they are
1192 /// combined using AND logic. The final constraint set requires that all
1193 /// individual constraints be satisfied simultaneously.
1194 pub fn add_constraint(&mut self, dependency: &str, constraint: &str) -> Result<()> {
1195 let parsed = VersionConstraint::parse(constraint)?;
1196
1197 self.constraints.entry(dependency.to_string()).or_default().add(parsed)?;
1198
1199 Ok(())
1200 }
1201
1202 /// Resolve all dependency constraints and return the best version for each.
1203 ///
1204 /// This method performs the core resolution algorithm, taking all accumulated
1205 /// constraints and finding the best matching version for each dependency from
1206 /// the provided catalog of available versions.
1207 ///
1208 /// # Resolution Algorithm
1209 ///
1210 /// For each dependency with constraints:
1211 /// 1. **Verify availability**: Check that versions exist for the dependency
1212 /// 2. **Apply constraints**: Filter versions using the dependency's constraint set
1213 /// 3. **Select best match**: Choose the highest compatible version
1214 /// 4. **Handle prereleases**: Apply prerelease policies appropriately
1215 ///
1216 /// # Arguments
1217 ///
1218 /// * `available_versions` - Map from dependency names to lists of available versions
1219 ///
1220 /// # Returns
1221 ///
1222 /// Returns `Ok(HashMap<String, Version>)` with the resolved version for each
1223 /// dependency, or `Err` if resolution fails.
1224 ///
1225 /// # Error Conditions
1226 ///
1227 /// - **Missing dependency**: Constraint exists but no versions available
1228 /// - **No satisfying version**: Available versions don't meet constraints
1229 /// - **Internal errors**: Constraint set conflicts or parsing failures
1230 ///
1231 /// # Examples
1232 ///
1233 /// ```rust,no_run
1234 /// use agpm_cli::version::constraints::ConstraintResolver;
1235 /// use semver::Version;
1236 /// use std::collections::HashMap;
1237 ///
1238 /// let mut resolver = ConstraintResolver::new();
1239 /// resolver.add_constraint("web-server", "^1.0.0")?;
1240 /// resolver.add_constraint("database", "~2.1.0")?;
1241 ///
1242 /// // Provide version catalog
1243 /// let mut available = HashMap::new();
1244 /// available.insert(
1245 /// "web-server".to_string(),
1246 /// vec![
1247 /// Version::parse("1.0.0")?,
1248 /// Version::parse("1.2.0")?,
1249 /// Version::parse("1.5.0")?, // Best match for ^1.0.0
1250 /// Version::parse("2.0.0")?, // Too new
1251 /// ],
1252 /// );
1253 /// available.insert(
1254 /// "database".to_string(),
1255 /// vec![
1256 /// Version::parse("2.1.0")?,
1257 /// Version::parse("2.1.3")?, // Best match for ~2.1.0
1258 /// Version::parse("2.2.0")?, // Too new
1259 /// ],
1260 /// );
1261 ///
1262 /// // Resolve dependencies
1263 /// let resolved = resolver.resolve(&available)?;
1264 /// assert_eq!(resolved["web-server"], Version::parse("1.5.0")?);
1265 /// assert_eq!(resolved["database"], Version::parse("2.1.3")?);
1266 /// # Ok::<(), anyhow::Error>(())
1267 /// ```
1268 ///
1269 /// ## Error Handling
1270 ///
1271 /// ```rust,no_run
1272 /// use agpm_cli::version::constraints::ConstraintResolver;
1273 /// use std::collections::HashMap;
1274 ///
1275 /// let mut resolver = ConstraintResolver::new();
1276 /// resolver.add_constraint("missing-dep", "^1.0.0")?;
1277 ///
1278 /// let available = HashMap::new(); // No versions provided
1279 ///
1280 /// let result = resolver.resolve(&available);
1281 /// assert!(result.is_err()); // Missing dependency error
1282 /// # Ok::<(), anyhow::Error>(())
1283 /// ```
1284 ///
1285 /// # Performance Considerations
1286 ///
1287 /// - Resolution is performed independently for each dependency
1288 /// - Version filtering and sorting may be expensive for large version lists
1289 /// - Consider pre-filtering available versions if catalogs are very large
1290 pub fn resolve(
1291 &self,
1292 available_versions: &HashMap<String, Vec<Version>>,
1293 ) -> Result<HashMap<String, Version>> {
1294 let mut resolved = HashMap::new();
1295
1296 for (dep, constraint_set) in &self.constraints {
1297 let versions = available_versions.get(dep).ok_or_else(|| AgpmError::Other {
1298 message: format!("No versions available for dependency: {dep}"),
1299 })?;
1300
1301 let best_match =
1302 constraint_set.find_best_match(versions).ok_or_else(|| AgpmError::Other {
1303 message: format!("No version satisfies constraints for dependency: {dep}"),
1304 })?;
1305
1306 resolved.insert(dep.clone(), best_match.clone());
1307 }
1308
1309 Ok(resolved)
1310 }
1311}
1312
1313#[cfg(test)]
1314mod tests {
1315 use super::*;
1316
1317 #[test]
1318 fn test_version_constraint_parse() {
1319 // Exact version
1320 let constraint = VersionConstraint::parse("1.0.0").unwrap();
1321 assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1322
1323 // Version with v prefix
1324 let constraint = VersionConstraint::parse("v1.0.0").unwrap();
1325 assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1326
1327 // Caret requirement
1328 let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1329 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1330
1331 // Tilde requirement
1332 let constraint = VersionConstraint::parse("~1.2.0").unwrap();
1333 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1334
1335 // Range requirement
1336 let constraint = VersionConstraint::parse(">=1.0.0, <2.0.0").unwrap();
1337 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1338
1339 // Git refs (including "latest" - it's just a tag name)
1340 let constraint = VersionConstraint::parse("latest").unwrap();
1341 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1342
1343 let constraint = VersionConstraint::parse("main").unwrap();
1344 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1345 }
1346
1347 #[test]
1348 fn test_constraint_matching() {
1349 let v100 = Version::parse("1.0.0").unwrap();
1350 let v110 = Version::parse("1.1.0").unwrap();
1351 let v200 = Version::parse("2.0.0").unwrap();
1352
1353 // Exact match
1354 let exact = VersionConstraint::Exact {
1355 prefix: None,
1356 version: v100.clone(),
1357 };
1358 assert!(exact.matches(&v100));
1359 assert!(!exact.matches(&v110));
1360
1361 // Caret requirement
1362 let caret = VersionConstraint::parse("^1.0.0").unwrap();
1363 assert!(caret.matches(&v100));
1364 assert!(caret.matches(&v110));
1365 assert!(!caret.matches(&v200));
1366
1367 // Git refs don't match semantic versions
1368 let git_ref = VersionConstraint::GitRef("latest".to_string());
1369 assert!(!git_ref.matches(&v100));
1370 assert!(!git_ref.matches(&v200));
1371 }
1372
1373 #[test]
1374 fn test_constraint_set() {
1375 let mut set = ConstraintSet::new();
1376 set.add(VersionConstraint::parse(">=1.0.0").unwrap()).unwrap();
1377 set.add(VersionConstraint::parse("<2.0.0").unwrap()).unwrap();
1378
1379 let v090 = Version::parse("0.9.0").unwrap();
1380 let v100 = Version::parse("1.0.0").unwrap();
1381 let v150 = Version::parse("1.5.0").unwrap();
1382 let v200 = Version::parse("2.0.0").unwrap();
1383
1384 assert!(!set.satisfies(&v090));
1385 assert!(set.satisfies(&v100));
1386 assert!(set.satisfies(&v150));
1387 assert!(!set.satisfies(&v200));
1388 }
1389
1390 #[test]
1391 fn test_find_best_match() {
1392 let mut set = ConstraintSet::new();
1393 set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1394
1395 let versions = vec![
1396 Version::parse("0.9.0").unwrap(),
1397 Version::parse("1.0.0").unwrap(),
1398 Version::parse("1.2.0").unwrap(),
1399 Version::parse("1.5.0").unwrap(),
1400 Version::parse("2.0.0").unwrap(),
1401 ];
1402
1403 let best = set.find_best_match(&versions).unwrap();
1404 assert_eq!(best, &Version::parse("1.5.0").unwrap());
1405 }
1406
1407 #[test]
1408 fn test_constraint_conflicts() {
1409 let mut set = ConstraintSet::new();
1410
1411 // Add first exact version
1412 set.add(VersionConstraint::Exact {
1413 prefix: None,
1414 version: Version::parse("1.0.0").unwrap(),
1415 })
1416 .unwrap();
1417
1418 // Try to add conflicting exact version
1419 let result = set.add(VersionConstraint::Exact {
1420 prefix: None,
1421 version: Version::parse("2.0.0").unwrap(),
1422 });
1423 assert!(result.is_err());
1424
1425 // Adding the same version should be ok
1426 let result = set.add(VersionConstraint::Exact {
1427 prefix: None,
1428 version: Version::parse("1.0.0").unwrap(),
1429 });
1430 assert!(result.is_ok());
1431 }
1432
1433 #[test]
1434 fn test_constraint_resolver() {
1435 let mut resolver = ConstraintResolver::new();
1436
1437 resolver.add_constraint("dep1", "^1.0.0").unwrap();
1438 resolver.add_constraint("dep2", "~2.1.0").unwrap();
1439
1440 let mut available = HashMap::new();
1441 available.insert(
1442 "dep1".to_string(),
1443 vec![
1444 Version::parse("0.9.0").unwrap(),
1445 Version::parse("1.0.0").unwrap(),
1446 Version::parse("1.5.0").unwrap(),
1447 Version::parse("2.0.0").unwrap(),
1448 ],
1449 );
1450 available.insert(
1451 "dep2".to_string(),
1452 vec![
1453 Version::parse("2.0.0").unwrap(),
1454 Version::parse("2.1.0").unwrap(),
1455 Version::parse("2.1.5").unwrap(),
1456 Version::parse("2.2.0").unwrap(),
1457 ],
1458 );
1459
1460 let resolved = resolver.resolve(&available).unwrap();
1461 assert_eq!(resolved.get("dep1"), Some(&Version::parse("1.5.0").unwrap()));
1462 assert_eq!(resolved.get("dep2"), Some(&Version::parse("2.1.5").unwrap()));
1463 }
1464
1465 #[test]
1466 fn test_allows_prerelease() {
1467 assert!(VersionConstraint::GitRef("main".to_string()).allows_prerelease());
1468 assert!(VersionConstraint::GitRef("latest".to_string()).allows_prerelease()); // Git ref
1469 assert!(
1470 !VersionConstraint::Exact {
1471 prefix: None,
1472 version: Version::parse("1.0.0").unwrap()
1473 }
1474 .allows_prerelease()
1475 );
1476 }
1477
1478 #[test]
1479 fn test_version_constraint_parse_edge_cases() {
1480 // Test latest-prerelease (just a tag name)
1481 let constraint = VersionConstraint::parse("latest-prerelease").unwrap();
1482 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1483
1484 // Test asterisk wildcard
1485 let constraint = VersionConstraint::parse("*").unwrap();
1486 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1487
1488 // Test range operators
1489 let constraint = VersionConstraint::parse(">=1.0.0").unwrap();
1490 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1491
1492 let constraint = VersionConstraint::parse("<2.0.0").unwrap();
1493 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1494
1495 let constraint = VersionConstraint::parse("=1.0.0").unwrap();
1496 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1497
1498 // Test git branch names
1499 let constraint = VersionConstraint::parse("feature/new-feature").unwrap();
1500 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1501
1502 // Test commit hash
1503 let constraint = VersionConstraint::parse("abc123def456").unwrap();
1504 assert!(matches!(constraint, VersionConstraint::GitRef(_)));
1505 }
1506
1507 #[test]
1508 fn test_version_constraint_display() {
1509 let exact = VersionConstraint::Exact {
1510 prefix: None,
1511 version: Version::parse("1.0.0").unwrap(),
1512 };
1513 assert_eq!(format!("{exact}"), "1.0.0");
1514
1515 let req = VersionConstraint::parse("^1.0.0").unwrap();
1516 assert_eq!(format!("{req}"), "^1.0.0");
1517
1518 let git_ref = VersionConstraint::GitRef("main".to_string());
1519 assert_eq!(format!("{git_ref}"), "main");
1520
1521 let latest = VersionConstraint::GitRef("latest".to_string());
1522 assert_eq!(format!("{latest}"), "latest");
1523 }
1524
1525 #[test]
1526 fn test_version_constraint_matches_ref() {
1527 let git_ref = VersionConstraint::GitRef("main".to_string());
1528 assert!(git_ref.matches_ref("main"));
1529 assert!(!git_ref.matches_ref("develop"));
1530
1531 // Other constraint types should return false for ref matching
1532 let exact = VersionConstraint::Exact {
1533 prefix: None,
1534 version: Version::parse("1.0.0").unwrap(),
1535 };
1536 assert!(!exact.matches_ref("v1.0.0"));
1537
1538 let latest = VersionConstraint::GitRef("latest".to_string());
1539 assert!(latest.matches_ref("latest"));
1540 }
1541
1542 #[test]
1543 fn test_version_constraint_to_version_req() {
1544 let exact = VersionConstraint::Exact {
1545 prefix: None,
1546 version: Version::parse("1.0.0").unwrap(),
1547 };
1548 let req = exact.to_version_req().unwrap();
1549 assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1550
1551 let caret = VersionConstraint::parse("^1.0.0").unwrap();
1552 let req = caret.to_version_req().unwrap();
1553 assert!(req.matches(&Version::parse("1.0.0").unwrap()));
1554
1555 let git_ref = VersionConstraint::GitRef("main".to_string());
1556 assert!(git_ref.to_version_req().is_none());
1557
1558 let latest = VersionConstraint::GitRef("latest".to_string());
1559 assert!(latest.to_version_req().is_none()); // Git ref - cannot convert
1560 }
1561
1562 #[test]
1563 fn test_constraint_set_with_prereleases() {
1564 let mut set = ConstraintSet::new();
1565 set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1566
1567 let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1568 let v100 = Version::parse("1.0.0").unwrap();
1569
1570 assert!(set.allows_prerelease());
1571
1572 // Git refs don't match semver versions
1573 let versions = vec![v100_pre.clone(), v100.clone()];
1574 let best = set.find_best_match(&versions);
1575 assert!(best.is_none()); // Git refs don't match semver
1576 }
1577
1578 #[test]
1579 fn test_constraint_set_no_matches() {
1580 let mut set = ConstraintSet::new();
1581 set.add(VersionConstraint::parse(">=2.0.0").unwrap()).unwrap();
1582
1583 let versions = vec![Version::parse("1.0.0").unwrap(), Version::parse("1.5.0").unwrap()];
1584
1585 let best = set.find_best_match(&versions);
1586 assert!(best.is_none());
1587 }
1588
1589 #[test]
1590 fn test_constraint_resolver_missing_dependency() {
1591 let mut resolver = ConstraintResolver::new();
1592 resolver.add_constraint("dep1", "^1.0.0").unwrap();
1593
1594 let available = HashMap::new(); // No versions available
1595
1596 let result = resolver.resolve(&available);
1597 assert!(result.is_err());
1598 }
1599
1600 #[test]
1601 fn test_constraint_resolver_no_satisfying_version() {
1602 let mut resolver = ConstraintResolver::new();
1603 resolver.add_constraint("dep1", "^2.0.0").unwrap();
1604
1605 let mut available = HashMap::new();
1606 available.insert(
1607 "dep1".to_string(),
1608 vec![Version::parse("1.0.0").unwrap()], // Only 1.x available, but we need 2.x
1609 );
1610
1611 let result = resolver.resolve(&available);
1612 assert!(result.is_err());
1613 }
1614
1615 #[test]
1616 fn test_constraint_set_git_ref_conflicts() {
1617 let mut set = ConstraintSet::new();
1618
1619 // Add first git ref
1620 set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1621
1622 // Try to add conflicting git ref
1623 let result = set.add(VersionConstraint::GitRef("develop".to_string()));
1624 assert!(result.is_err());
1625
1626 // Adding the same ref should be ok
1627 let result = set.add(VersionConstraint::GitRef("main".to_string()));
1628 assert!(result.is_ok());
1629 }
1630
1631 #[test]
1632 fn test_git_ref_constraint_with_versions() {
1633 let git_ref = VersionConstraint::GitRef("latest".to_string());
1634
1635 let v100_pre = Version::parse("1.0.0-alpha.1").unwrap();
1636 let v100 = Version::parse("1.0.0").unwrap();
1637
1638 // Git refs don't match semantic versions
1639 assert!(!git_ref.matches(&v100));
1640 assert!(!git_ref.matches(&v100_pre));
1641 }
1642
1643 #[test]
1644 fn test_git_ref_allows_prereleases() {
1645 let git_ref = VersionConstraint::GitRef("latest".to_string());
1646
1647 // Git refs allow prereleases (they reference commits)
1648 assert!(git_ref.allows_prerelease());
1649
1650 let main_ref = VersionConstraint::GitRef("main".to_string());
1651 assert!(main_ref.allows_prerelease());
1652 }
1653
1654 #[test]
1655 fn test_requirement_constraint_allows_prerelease() {
1656 let req = VersionConstraint::parse("^1.0.0").unwrap();
1657 assert!(!req.allows_prerelease());
1658
1659 let exact = VersionConstraint::Exact {
1660 prefix: None,
1661 version: Version::parse("1.0.0").unwrap(),
1662 };
1663 assert!(!exact.allows_prerelease());
1664 }
1665
1666 #[test]
1667 fn test_constraint_set_prerelease_filtering() {
1668 let mut set = ConstraintSet::new();
1669 set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1670
1671 let versions = vec![
1672 Version::parse("1.0.0-alpha.1").unwrap(),
1673 Version::parse("1.0.0").unwrap(),
1674 Version::parse("1.1.0-beta.1").unwrap(),
1675 Version::parse("1.1.0").unwrap(),
1676 ];
1677
1678 let best = set.find_best_match(&versions).unwrap();
1679 assert_eq!(best, &Version::parse("1.1.0").unwrap()); // Should pick highest stable
1680 }
1681
1682 #[test]
1683 fn test_parse_with_whitespace() {
1684 let constraint = VersionConstraint::parse(" 1.0.0 ").unwrap();
1685 assert!(matches!(constraint, VersionConstraint::Exact { .. }));
1686
1687 let constraint = VersionConstraint::parse(" latest ").unwrap();
1688 assert!(matches!(constraint, VersionConstraint::GitRef(_))); // Just a git ref
1689
1690 let constraint = VersionConstraint::parse(" ^1.0.0 ").unwrap();
1691 assert!(matches!(constraint, VersionConstraint::Requirement { .. }));
1692 }
1693
1694 #[test]
1695 fn test_constraint_resolver_add_constraint_error() {
1696 let mut resolver = ConstraintResolver::new();
1697
1698 // Add a valid constraint first
1699 resolver.add_constraint("dep1", "1.0.0").unwrap();
1700
1701 // Add conflicting constraint
1702 let result = resolver.add_constraint("dep1", "2.0.0");
1703 assert!(result.is_err());
1704 }
1705
1706 #[test]
1707 fn test_constraint_set_no_conflict_different_types() {
1708 let mut set = ConstraintSet::new();
1709
1710 // These shouldn't conflict as they are different types
1711 set.add(VersionConstraint::parse("^1.0.0").unwrap()).unwrap();
1712 set.add(VersionConstraint::GitRef("main".to_string())).unwrap();
1713
1714 // Should have 2 constraints
1715 assert_eq!(set.constraints.len(), 2);
1716 }
1717
1718 #[test]
1719 fn test_git_ref_to_version_req() {
1720 let git_ref = VersionConstraint::GitRef("latest".to_string());
1721 // Git refs cannot be converted to version requirements
1722 assert!(git_ref.to_version_req().is_none());
1723
1724 let main_ref = VersionConstraint::GitRef("main".to_string());
1725 assert!(main_ref.to_version_req().is_none());
1726 }
1727
1728 // ========== Prefix Support Tests ==========
1729
1730 #[test]
1731 fn test_prefixed_constraint_parsing() {
1732 // Prefixed exact version
1733 let constraint = VersionConstraint::parse("agents-v1.0.0").unwrap();
1734 match constraint {
1735 VersionConstraint::Exact {
1736 prefix,
1737 version,
1738 } => {
1739 assert_eq!(prefix, Some("agents".to_string()));
1740 assert_eq!(version, Version::parse("1.0.0").unwrap());
1741 }
1742 _ => panic!("Expected Exact constraint"),
1743 }
1744
1745 // Prefixed requirement
1746 let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1747 match constraint {
1748 VersionConstraint::Requirement {
1749 prefix,
1750 req,
1751 } => {
1752 assert_eq!(prefix, Some("agents".to_string()));
1753 assert!(req.matches(&Version::parse("1.5.0").unwrap()));
1754 assert!(!req.matches(&Version::parse("2.0.0").unwrap()));
1755 }
1756 _ => panic!("Expected Requirement constraint"),
1757 }
1758
1759 // Unprefixed constraint (backward compatible)
1760 let constraint = VersionConstraint::parse("^1.0.0").unwrap();
1761 match constraint {
1762 VersionConstraint::Requirement {
1763 prefix,
1764 ..
1765 } => {
1766 assert_eq!(prefix, None);
1767 }
1768 _ => panic!("Expected Requirement constraint"),
1769 }
1770 }
1771
1772 #[test]
1773 fn test_prefixed_constraint_display() {
1774 let prefixed_exact = VersionConstraint::Exact {
1775 prefix: Some("agents".to_string()),
1776 version: Version::parse("1.0.0").unwrap(),
1777 };
1778 assert_eq!(prefixed_exact.to_string(), "agents-1.0.0");
1779
1780 let unprefixed_exact = VersionConstraint::Exact {
1781 prefix: None,
1782 version: Version::parse("1.0.0").unwrap(),
1783 };
1784 assert_eq!(unprefixed_exact.to_string(), "1.0.0");
1785
1786 let prefixed_req = VersionConstraint::parse("snippets-^v2.0.0").unwrap();
1787 let display = prefixed_req.to_string();
1788 assert!(display.starts_with("snippets-"));
1789 }
1790
1791 #[test]
1792 fn test_matches_version_info() {
1793 use crate::version::VersionInfo;
1794
1795 // Prefixed constraint matching prefixed version
1796 let constraint = VersionConstraint::parse("agents-^v1.0.0").unwrap();
1797 let version_info = VersionInfo {
1798 prefix: Some("agents".to_string()),
1799 version: Version::parse("1.2.0").unwrap(),
1800 tag: "agents-v1.2.0".to_string(),
1801 prerelease: false,
1802 };
1803 assert!(constraint.matches_version_info(&version_info));
1804
1805 // Prefixed constraint NOT matching different prefix
1806 let wrong_prefix = VersionInfo {
1807 prefix: Some("snippets".to_string()),
1808 version: Version::parse("1.2.0").unwrap(),
1809 tag: "snippets-v1.2.0".to_string(),
1810 prerelease: false,
1811 };
1812 assert!(!constraint.matches_version_info(&wrong_prefix));
1813
1814 // Unprefixed constraint matching unprefixed version
1815 let unprefixed_constraint = VersionConstraint::parse("^1.0.0").unwrap();
1816 let unprefixed_version = VersionInfo {
1817 prefix: None,
1818 version: Version::parse("1.5.0").unwrap(),
1819 tag: "v1.5.0".to_string(),
1820 prerelease: false,
1821 };
1822 assert!(unprefixed_constraint.matches_version_info(&unprefixed_version));
1823
1824 // Unprefixed constraint NOT matching prefixed version
1825 assert!(!unprefixed_constraint.matches_version_info(&version_info));
1826 }
1827
1828 #[test]
1829 fn test_prefixed_constraint_conflicts() {
1830 let mut set = ConstraintSet::new();
1831
1832 // Add prefixed constraint
1833 set.add(VersionConstraint::parse("agents-^v1.0.0").unwrap()).unwrap();
1834
1835 // Different prefix should not conflict
1836 let result = set.add(VersionConstraint::parse("snippets-^v1.0.0").unwrap());
1837 assert!(result.is_ok());
1838
1839 // Same prefix but compatible constraints should not conflict
1840 let result = set.add(VersionConstraint::parse("agents-~v1.2.0").unwrap());
1841 assert!(result.is_ok());
1842
1843 // Different prefixes for Exact constraints
1844 let mut exact_set = ConstraintSet::new();
1845 exact_set.add(VersionConstraint::parse("agents-v1.0.0").unwrap()).unwrap();
1846
1847 // Different prefix, same version - should not conflict
1848 let result = exact_set.add(VersionConstraint::parse("snippets-v1.0.0").unwrap());
1849 assert!(result.is_ok());
1850 }
1851
1852 #[test]
1853 fn test_prefix_with_hyphens() {
1854 // Multiple hyphens in prefix
1855 let constraint = VersionConstraint::parse("my-cool-agent-v1.0.0").unwrap();
1856 match constraint {
1857 VersionConstraint::Exact {
1858 prefix,
1859 version,
1860 } => {
1861 assert_eq!(prefix, Some("my-cool-agent".to_string()));
1862 assert_eq!(version, Version::parse("1.0.0").unwrap());
1863 }
1864 _ => panic!("Expected Exact constraint"),
1865 }
1866
1867 // Prefix ending with 'v'
1868 let constraint = VersionConstraint::parse("tool-v-v1.0.0").unwrap();
1869 match constraint {
1870 VersionConstraint::Exact {
1871 prefix,
1872 version,
1873 } => {
1874 assert_eq!(prefix, Some("tool-v".to_string()));
1875 assert_eq!(version, Version::parse("1.0.0").unwrap());
1876 }
1877 _ => panic!("Expected Exact constraint"),
1878 }
1879 }
1880}