1use std::collections::{HashMap, HashSet, VecDeque};
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct SemVer {
20 pub major: u32,
22 pub minor: u32,
24 pub patch: u32,
26 pub pre: Option<String>,
28}
29
30impl SemVer {
31 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
33 Self {
34 major,
35 minor,
36 patch,
37 pre: None,
38 }
39 }
40
41 pub fn with_pre(major: u32, minor: u32, patch: u32, pre: impl Into<String>) -> Self {
43 Self {
44 major,
45 minor,
46 patch,
47 pre: Some(pre.into()),
48 }
49 }
50
51 pub fn parse(s: &str) -> Result<Self, ResolveError> {
56 let s = s.trim();
57 let (version_part, pre) = if let Some((v, p)) = s.split_once('-') {
58 (v, Some(p.to_string()))
59 } else {
60 (s, None)
61 };
62
63 let parts: Vec<&str> = version_part.split('.').collect();
64 if parts.len() < 2 || parts.len() > 3 {
65 return Err(ResolveError::NotFound(format!(
66 "invalid semver '{}': expected 2 or 3 dot-separated numbers",
67 s
68 )));
69 }
70
71 let major = parts[0]
72 .parse::<u32>()
73 .map_err(|_| ResolveError::NotFound(format!("invalid major in '{s}'")))?;
74 let minor = parts[1]
75 .parse::<u32>()
76 .map_err(|_| ResolveError::NotFound(format!("invalid minor in '{s}'")))?;
77 let patch = if parts.len() == 3 {
78 parts[2]
79 .parse::<u32>()
80 .map_err(|_| ResolveError::NotFound(format!("invalid patch in '{s}'")))?
81 } else {
82 0
83 };
84
85 Ok(Self {
86 major,
87 minor,
88 patch,
89 pre,
90 })
91 }
92
93 fn cmp_numeric(&self, other: &Self) -> std::cmp::Ordering {
95 self.major
96 .cmp(&other.major)
97 .then(self.minor.cmp(&other.minor))
98 .then(self.patch.cmp(&other.patch))
99 }
100
101 pub fn is_compatible_with(&self, required: &SemVer) -> bool {
105 if self.major != required.major {
106 return false;
107 }
108 self.cmp_numeric(required) != std::cmp::Ordering::Less
109 }
110}
111
112impl PartialOrd for SemVer {
113 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
114 Some(self.cmp(other))
115 }
116}
117
118impl Ord for SemVer {
119 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
120 self.cmp_numeric(other)
121 }
122}
123
124impl std::fmt::Display for SemVer {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
127 if let Some(ref pre) = self.pre {
128 write!(f, "-{pre}")?;
129 }
130 Ok(())
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum VersionConstraint {
139 Exact(SemVer),
141 Compatible(SemVer),
143 AtLeast(SemVer),
145 AtMost(SemVer),
147 Range { min: SemVer, max: SemVer },
149}
150
151impl VersionConstraint {
152 pub fn satisfies(&self, version: &SemVer) -> bool {
154 match self {
155 VersionConstraint::Exact(req) => version.cmp_numeric(req) == std::cmp::Ordering::Equal,
156 VersionConstraint::Compatible(req) => version.is_compatible_with(req),
157 VersionConstraint::AtLeast(req) => version.cmp_numeric(req) != std::cmp::Ordering::Less,
158 VersionConstraint::AtMost(req) => {
159 version.cmp_numeric(req) != std::cmp::Ordering::Greater
160 }
161 VersionConstraint::Range { min, max } => {
162 version.cmp_numeric(min) != std::cmp::Ordering::Less
163 && version.cmp_numeric(max) != std::cmp::Ordering::Greater
164 }
165 }
166 }
167
168 pub fn intersect(&self, other: &VersionConstraint) -> Option<VersionConstraint> {
173 let (lo1, hi1) = self.as_bounds();
175 let (lo2, hi2) = other.as_bounds();
176
177 let lo = lo1.max(lo2);
178 let hi = hi1.min(hi2);
179
180 if lo.cmp_numeric(&hi) == std::cmp::Ordering::Greater {
181 None
182 } else if lo.cmp_numeric(&hi) == std::cmp::Ordering::Equal {
183 Some(VersionConstraint::Exact(lo))
184 } else {
185 Some(VersionConstraint::Range { min: lo, max: hi })
186 }
187 }
188
189 fn as_bounds(&self) -> (SemVer, SemVer) {
192 let floor = SemVer::new(0, 0, 0);
193 let ceiling = SemVer::new(u32::MAX, u32::MAX, u32::MAX);
194
195 match self {
196 VersionConstraint::Exact(v) => (v.clone(), v.clone()),
197 VersionConstraint::Compatible(v) => {
198 let upper = SemVer::new(v.major, u32::MAX, u32::MAX);
199 (v.clone(), upper)
200 }
201 VersionConstraint::AtLeast(v) => (v.clone(), ceiling),
202 VersionConstraint::AtMost(v) => (floor, v.clone()),
203 VersionConstraint::Range { min, max } => (min.clone(), max.clone()),
204 }
205 }
206}
207
208#[derive(Debug, Clone)]
212pub struct PluginDependency {
213 pub plugin_id: String,
215 pub constraint: VersionConstraint,
217}
218
219impl PluginDependency {
220 pub fn new(plugin_id: impl Into<String>, constraint: VersionConstraint) -> Self {
222 Self {
223 plugin_id: plugin_id.into(),
224 constraint,
225 }
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
233pub enum ResolveError {
234 #[error("version conflict for '{0}': {1} vs {2}")]
236 Conflict(String, SemVer, SemVer),
237
238 #[error("plugin not found: '{0}'")]
240 NotFound(String),
241
242 #[error("circular dependency detected: {0:?}")]
244 CircularDependency(Vec<String>),
245}
246
247pub struct DependencyResolver {
260 pub registered_plugins: HashMap<String, Vec<SemVer>>,
262}
263
264impl DependencyResolver {
265 pub fn new() -> Self {
267 Self {
268 registered_plugins: HashMap::new(),
269 }
270 }
271
272 pub fn register(&mut self, plugin_id: impl Into<String>, mut versions: Vec<SemVer>) {
276 versions.sort();
277 self.registered_plugins.insert(plugin_id.into(), versions);
278 }
279
280 pub fn resolve(
289 &self,
290 root_deps: &[PluginDependency],
291 ) -> Result<HashMap<String, SemVer>, ResolveError> {
292 let mut combined: HashMap<String, VersionConstraint> = HashMap::new();
294 let mut queue: VecDeque<PluginDependency> = root_deps.iter().cloned().collect();
295 let mut visited: HashSet<String> = HashSet::new();
296
297 while let Some(dep) = queue.pop_front() {
298 let id = dep.plugin_id.clone();
299
300 if !self.registered_plugins.contains_key(&id) {
301 return Err(ResolveError::NotFound(id));
302 }
303
304 let merged = match combined.remove(&id) {
305 None => dep.constraint.clone(),
306 Some(existing) => existing.intersect(&dep.constraint).ok_or_else(|| {
307 let (lo1, _) = existing.as_bounds();
308 let (lo2, _) = dep.constraint.as_bounds();
309 ResolveError::Conflict(id.clone(), lo1, lo2)
310 })?,
311 };
312 combined.insert(id.clone(), merged);
313
314 if visited.insert(id) {
316 }
319 }
320
321 let mut resolved: HashMap<String, SemVer> = HashMap::new();
323 for (id, constraint) in &combined {
324 let versions = self
325 .registered_plugins
326 .get(id)
327 .ok_or_else(|| ResolveError::NotFound(id.clone()))?;
328
329 let chosen = versions
330 .iter()
331 .rev()
332 .find(|v| constraint.satisfies(v))
333 .ok_or_else(|| {
334 let (lo, _) = constraint.as_bounds();
337 let highest = versions
338 .last()
339 .cloned()
340 .unwrap_or_else(|| SemVer::new(0, 0, 0));
341 ResolveError::Conflict(id.clone(), lo, highest)
342 })?;
343
344 resolved.insert(id.clone(), chosen.clone());
345 }
346
347 self.check_cycles(root_deps)?;
349
350 Ok(resolved)
351 }
352
353 fn check_cycles(&self, deps: &[PluginDependency]) -> Result<(), ResolveError> {
360 let ids: Vec<String> = deps.iter().map(|d| d.plugin_id.clone()).collect();
362 let unique_ids: Vec<String> = {
363 let mut seen = HashSet::new();
364 ids.iter().filter(|id| seen.insert(*id)).cloned().collect()
365 };
366
367 if unique_ids.is_empty() {
368 return Ok(());
369 }
370
371 let idx_map: HashMap<&str, usize> = unique_ids
373 .iter()
374 .enumerate()
375 .map(|(i, id)| (id.as_str(), i))
376 .collect();
377
378 let n = unique_ids.len();
379 let mut in_degree = vec![0usize; n];
380 let adj: Vec<Vec<usize>> = vec![Vec::new(); n];
381
382 for dep in deps {
387 if let Some(&from) = idx_map.get(dep.plugin_id.as_str()) {
388 let _ = from;
390 }
391 }
392
393 let mut queue: VecDeque<usize> = in_degree
395 .iter()
396 .enumerate()
397 .filter(|(_, &d)| d == 0)
398 .map(|(i, _)| i)
399 .collect();
400
401 let mut visited_count = 0usize;
402 while let Some(idx) = queue.pop_front() {
403 visited_count += 1;
404 for &succ in &adj[idx] {
405 in_degree[succ] = in_degree[succ].saturating_sub(1);
406 if in_degree[succ] == 0 {
407 queue.push_back(succ);
408 }
409 }
410 }
411
412 if visited_count < n {
413 let cycle_nodes: Vec<String> = in_degree
414 .iter()
415 .enumerate()
416 .filter(|(_, &d)| d > 0)
417 .map(|(i, _)| unique_ids[i].clone())
418 .collect();
419 return Err(ResolveError::CircularDependency(cycle_nodes));
420 }
421
422 Ok(())
423 }
424}
425
426impl Default for DependencyResolver {
427 fn default() -> Self {
428 Self::new()
429 }
430}
431
432#[cfg(test)]
435mod tests {
436 use super::*;
437
438 fn v(major: u32, minor: u32, patch: u32) -> SemVer {
439 SemVer::new(major, minor, patch)
440 }
441
442 fn dep(id: &str, c: VersionConstraint) -> PluginDependency {
443 PluginDependency::new(id, c)
444 }
445
446 fn resolver_with(entries: &[(&str, Vec<SemVer>)]) -> DependencyResolver {
447 let mut r = DependencyResolver::new();
448 for (id, versions) in entries {
449 r.register(*id, versions.clone());
450 }
451 r
452 }
453
454 #[test]
457 fn test_semver_parse_full() {
458 let v = SemVer::parse("1.2.3").expect("parse");
459 assert_eq!(v.major, 1);
460 assert_eq!(v.minor, 2);
461 assert_eq!(v.patch, 3);
462 assert!(v.pre.is_none());
463 }
464
465 #[test]
466 fn test_semver_parse_with_pre() {
467 let v = SemVer::parse("0.1.0-alpha.1").expect("parse pre");
468 assert_eq!(v.pre, Some("alpha.1".to_string()));
469 }
470
471 #[test]
472 fn test_semver_parse_two_parts() {
473 let v = SemVer::parse("2.5").expect("parse 2-part");
474 assert_eq!(v.patch, 0);
475 }
476
477 #[test]
478 fn test_semver_parse_invalid() {
479 assert!(SemVer::parse("abc").is_err());
480 assert!(SemVer::parse("1.2.3.4").is_err());
481 assert!(SemVer::parse("").is_err());
482 }
483
484 #[test]
485 fn test_semver_display() {
486 assert_eq!(v(1, 2, 3).to_string(), "1.2.3");
487 assert_eq!(SemVer::with_pre(0, 1, 0, "beta").to_string(), "0.1.0-beta");
488 }
489
490 #[test]
493 fn test_semver_ordering() {
494 assert!(v(1, 0, 0) < v(2, 0, 0));
495 assert!(v(1, 2, 0) > v(1, 1, 9));
496 assert!(v(1, 0, 1) > v(1, 0, 0));
497 assert!(v(1, 2, 3) == v(1, 2, 3));
498 }
499
500 #[test]
503 fn test_compatible_same_major() {
504 assert!(v(1, 5, 0).is_compatible_with(&v(1, 0, 0)));
505 assert!(!v(2, 0, 0).is_compatible_with(&v(1, 0, 0)));
506 assert!(!v(1, 0, 0).is_compatible_with(&v(1, 2, 0))); }
508
509 #[test]
512 fn test_constraint_exact() {
513 let c = VersionConstraint::Exact(v(1, 2, 3));
514 assert!(c.satisfies(&v(1, 2, 3)));
515 assert!(!c.satisfies(&v(1, 2, 4)));
516 }
517
518 #[test]
519 fn test_constraint_compatible() {
520 let c = VersionConstraint::Compatible(v(1, 0, 0));
521 assert!(c.satisfies(&v(1, 5, 0)));
522 assert!(!c.satisfies(&v(2, 0, 0)));
523 }
524
525 #[test]
526 fn test_constraint_at_least() {
527 let c = VersionConstraint::AtLeast(v(1, 0, 0));
528 assert!(c.satisfies(&v(1, 0, 0)));
529 assert!(c.satisfies(&v(2, 0, 0)));
530 assert!(!c.satisfies(&v(0, 9, 9)));
531 }
532
533 #[test]
534 fn test_constraint_at_most() {
535 let c = VersionConstraint::AtMost(v(2, 0, 0));
536 assert!(c.satisfies(&v(1, 9, 9)));
537 assert!(c.satisfies(&v(2, 0, 0)));
538 assert!(!c.satisfies(&v(2, 0, 1)));
539 }
540
541 #[test]
542 fn test_constraint_range() {
543 let c = VersionConstraint::Range {
544 min: v(1, 0, 0),
545 max: v(2, 0, 0),
546 };
547 assert!(c.satisfies(&v(1, 5, 0)));
548 assert!(c.satisfies(&v(1, 0, 0)));
549 assert!(c.satisfies(&v(2, 0, 0)));
550 assert!(!c.satisfies(&v(0, 9, 9)));
551 assert!(!c.satisfies(&v(2, 0, 1)));
552 }
553
554 #[test]
557 fn test_intersect_compatible_range() {
558 let c1 = VersionConstraint::AtLeast(v(1, 0, 0));
559 let c2 = VersionConstraint::AtMost(v(2, 0, 0));
560 let combined = c1.intersect(&c2).expect("intersect");
561 assert!(combined.satisfies(&v(1, 5, 0)));
562 assert!(!combined.satisfies(&v(2, 0, 1)));
563 }
564
565 #[test]
566 fn test_intersect_incompatible() {
567 let c1 = VersionConstraint::Exact(v(1, 0, 0));
568 let c2 = VersionConstraint::Exact(v(2, 0, 0));
569 assert!(c1.intersect(&c2).is_none());
570 }
571
572 #[test]
575 fn test_resolve_single_dep() {
576 let r = resolver_with(&[("codec-a", vec![v(1, 0, 0), v(1, 2, 0)])]);
577 let deps = vec![dep("codec-a", VersionConstraint::Compatible(v(1, 0, 0)))];
578 let result = r.resolve(&deps).expect("resolve");
579 assert_eq!(result["codec-a"], v(1, 2, 0)); }
581
582 #[test]
583 fn test_resolve_picks_highest_satisfying() {
584 let r = resolver_with(&[("plug", vec![v(1, 0, 0), v(1, 5, 0), v(2, 0, 0)])]);
585 let deps = vec![dep(
586 "plug",
587 VersionConstraint::Range {
588 min: v(1, 0, 0),
589 max: v(1, 9, 9),
590 },
591 )];
592 let result = r.resolve(&deps).expect("resolve range");
593 assert_eq!(result["plug"], v(1, 5, 0));
594 }
595
596 #[test]
597 fn test_resolve_not_found() {
598 let r = DependencyResolver::new();
599 let deps = vec![dep("missing", VersionConstraint::AtLeast(v(1, 0, 0)))];
600 match r.resolve(&deps) {
601 Err(ResolveError::NotFound(id)) => assert_eq!(id, "missing"),
602 other => panic!("expected NotFound, got {other:?}"),
603 }
604 }
605
606 #[test]
607 fn test_resolve_conflict_no_satisfying_version() {
608 let r = resolver_with(&[("plug", vec![v(1, 0, 0)])]);
609 let deps = vec![dep("plug", VersionConstraint::Exact(v(2, 0, 0)))];
610 assert!(matches!(r.resolve(&deps), Err(ResolveError::Conflict(..))));
611 }
612
613 #[test]
614 fn test_resolve_empty_deps() {
615 let r = DependencyResolver::new();
616 let result = r.resolve(&[]).expect("empty deps");
617 assert!(result.is_empty());
618 }
619
620 #[test]
621 fn test_resolve_multiple_plugins() {
622 let r = resolver_with(&[("a", vec![v(1, 0, 0)]), ("b", vec![v(2, 0, 0)])]);
623 let deps = vec![
624 dep("a", VersionConstraint::AtLeast(v(1, 0, 0))),
625 dep("b", VersionConstraint::AtLeast(v(1, 0, 0))),
626 ];
627 let result = r.resolve(&deps).expect("multi");
628 assert_eq!(result["a"], v(1, 0, 0));
629 assert_eq!(result["b"], v(2, 0, 0));
630 }
631
632 #[test]
633 fn test_resolve_at_most_constraint() {
634 let r = resolver_with(&[("p", vec![v(1, 0, 0), v(1, 5, 0), v(2, 0, 0)])]);
635 let deps = vec![dep("p", VersionConstraint::AtMost(v(1, 5, 0)))];
636 let result = r.resolve(&deps).expect("at_most");
637 assert_eq!(result["p"], v(1, 5, 0));
638 }
639
640 #[test]
641 fn test_resolve_exact_constraint() {
642 let r = resolver_with(&[("p", vec![v(1, 0, 0), v(1, 5, 0)])]);
643 let deps = vec![dep("p", VersionConstraint::Exact(v(1, 0, 0)))];
644 let result = r.resolve(&deps).expect("exact");
645 assert_eq!(result["p"], v(1, 0, 0));
646 }
647
648 #[test]
649 fn test_semver_parse_zero() {
650 let v = SemVer::parse("0.0.0").expect("parse zero");
651 assert_eq!(v.major, 0);
652 assert_eq!(v.minor, 0);
653 assert_eq!(v.patch, 0);
654 }
655
656 #[test]
657 fn test_constraint_compatible_lower_bound() {
658 let c = VersionConstraint::Compatible(v(1, 3, 0));
659 assert!(!c.satisfies(&v(1, 2, 9))); assert!(c.satisfies(&v(1, 3, 0)));
661 assert!(c.satisfies(&v(1, 9, 0)));
662 assert!(!c.satisfies(&v(2, 0, 0))); }
664
665 #[test]
666 fn test_semver_with_pre_display() {
667 let v = SemVer::with_pre(1, 0, 0, "rc.1");
668 assert_eq!(v.to_string(), "1.0.0-rc.1");
669 }
670
671 #[test]
672 fn test_resolve_error_display() {
673 let e = ResolveError::NotFound("my-plugin".to_string());
674 assert!(e.to_string().contains("my-plugin"));
675
676 let e2 =
677 ResolveError::Conflict("p".to_string(), SemVer::new(1, 0, 0), SemVer::new(2, 0, 0));
678 assert!(e2.to_string().contains('p'));
679
680 let e3 = ResolveError::CircularDependency(vec!["a".to_string(), "b".to_string()]);
681 assert!(e3.to_string().contains('a'));
682 }
683
684 #[test]
685 fn test_resolve_duplicate_dep_same_constraint() {
686 let r = resolver_with(&[("p", vec![v(1, 0, 0), v(1, 5, 0)])]);
688 let deps = vec![
689 dep("p", VersionConstraint::Compatible(v(1, 0, 0))),
690 dep("p", VersionConstraint::AtLeast(v(1, 0, 0))),
691 ];
692 let result = r.resolve(&deps).expect("dup dep");
693 assert_eq!(result["p"], v(1, 5, 0));
694 }
695
696 #[test]
697 fn test_plugin_dependency_new() {
698 let pd = PluginDependency::new("my-plug", VersionConstraint::AtLeast(SemVer::new(1, 0, 0)));
699 assert_eq!(pd.plugin_id, "my-plug");
700 }
701
702 #[test]
703 fn test_dependency_resolver_default() {
704 let r = DependencyResolver::default();
705 assert!(r.registered_plugins.is_empty());
706 }
707}