Skip to main content

pro_core/resolver/
package.rs

1//! Package representation for the resolver
2
3use serde::{Deserialize, Serialize};
4use std::cmp::Ordering;
5
6/// A Python package identifier
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub struct Package {
9    /// Normalized package name
10    pub name: String,
11}
12
13impl PartialOrd for Package {
14    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
15        Some(self.cmp(other))
16    }
17}
18
19impl Ord for Package {
20    fn cmp(&self, other: &Self) -> Ordering {
21        self.name.cmp(&other.name)
22    }
23}
24
25impl Package {
26    /// Create a new package with normalized name
27    pub fn new(name: impl Into<String>) -> Self {
28        Self {
29            name: Self::normalize(name.into()),
30        }
31    }
32
33    /// Normalize a package name according to PEP 503
34    /// - Lowercase
35    /// - Replace runs of [-_.] with single hyphen
36    fn normalize(name: String) -> String {
37        let mut result = String::with_capacity(name.len());
38        let mut prev_was_separator = false;
39
40        for c in name.chars() {
41            match c {
42                '-' | '_' | '.' => {
43                    if !prev_was_separator && !result.is_empty() {
44                        result.push('-');
45                        prev_was_separator = true;
46                    }
47                }
48                c => {
49                    result.push(c.to_ascii_lowercase());
50                    prev_was_separator = false;
51                }
52            }
53        }
54
55        // Remove trailing separator
56        if result.ends_with('-') {
57            result.pop();
58        }
59
60        result
61    }
62}
63
64impl std::fmt::Display for Package {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        write!(f, "{}", self.name)
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn test_normalize_simple() {
76        assert_eq!(Package::new("requests").name, "requests");
77    }
78
79    #[test]
80    fn test_normalize_uppercase() {
81        assert_eq!(Package::new("Requests").name, "requests");
82    }
83
84    #[test]
85    fn test_normalize_underscore() {
86        assert_eq!(Package::new("my_package").name, "my-package");
87    }
88
89    #[test]
90    fn test_normalize_dots() {
91        assert_eq!(Package::new("zope.interface").name, "zope-interface");
92    }
93
94    #[test]
95    fn test_normalize_mixed() {
96        assert_eq!(Package::new("My__Package..Name").name, "my-package-name");
97    }
98}