Skip to main content

pro_core/pep/
pep440.rs

1//! PEP 440 - Version Identification and Dependency Specification
2
3use std::cmp::Ordering;
4use std::fmt;
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9use crate::Error;
10
11/// A PEP 440 compliant version
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct Version {
14    /// Epoch (optional, defaults to 0)
15    pub epoch: u32,
16    /// Release segment (major.minor.patch...)
17    pub release: Vec<u32>,
18    /// Pre-release (alpha, beta, rc)
19    pub pre: Option<PreRelease>,
20    /// Post-release
21    pub post: Option<u32>,
22    /// Development release
23    pub dev: Option<u32>,
24    /// Local version segment
25    pub local: Option<String>,
26}
27
28/// Pre-release type
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub enum PreRelease {
31    Alpha(u32),
32    Beta(u32),
33    Rc(u32),
34}
35
36impl PreRelease {
37    /// Get the numeric value for ordering
38    fn order_key(&self) -> (u8, u32) {
39        match self {
40            PreRelease::Alpha(n) => (0, *n),
41            PreRelease::Beta(n) => (1, *n),
42            PreRelease::Rc(n) => (2, *n),
43        }
44    }
45}
46
47impl PartialOrd for PreRelease {
48    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
49        Some(self.cmp(other))
50    }
51}
52
53impl Ord for PreRelease {
54    fn cmp(&self, other: &Self) -> Ordering {
55        self.order_key().cmp(&other.order_key())
56    }
57}
58
59impl Version {
60    /// Create a new version from release numbers
61    pub fn new(release: Vec<u32>) -> Self {
62        Self {
63            epoch: 0,
64            release,
65            pre: None,
66            post: None,
67            dev: None,
68            local: None,
69        }
70    }
71
72    /// Parse a version string
73    pub fn parse(s: &str) -> Result<Self, Error> {
74        let s = s.trim();
75        if s.is_empty() {
76            return Err(Error::InvalidVersion(s.to_string()));
77        }
78
79        let mut epoch = 0u32;
80        let mut remaining = s;
81
82        // Parse epoch (N!)
83        if let Some(excl_pos) = remaining.find('!') {
84            epoch = remaining[..excl_pos]
85                .parse()
86                .map_err(|_| Error::InvalidVersion(s.to_string()))?;
87            remaining = &remaining[excl_pos + 1..];
88        }
89
90        // Split off local version (+local)
91        let (version_part, local) = if let Some(plus_pos) = remaining.find('+') {
92            let local = remaining[plus_pos + 1..].to_string();
93            (&remaining[..plus_pos], Some(local))
94        } else {
95            (remaining, None)
96        };
97
98        // Parse the version part
99        let (release, pre, post, dev) = Self::parse_version_part(version_part, s)?;
100
101        Ok(Self {
102            epoch,
103            release,
104            pre,
105            post,
106            dev,
107            local,
108        })
109    }
110
111    #[allow(clippy::type_complexity)]
112    fn parse_version_part(
113        s: &str,
114        original: &str,
115    ) -> Result<(Vec<u32>, Option<PreRelease>, Option<u32>, Option<u32>), Error> {
116        let mut pre = None;
117        let mut post = None;
118        let mut dev = None;
119
120        // Find where release ends and pre/post/dev begins
121        // The release is a sequence of numbers separated by dots
122        let mut release_end = 0;
123        let chars: Vec<char> = s.chars().collect();
124        let mut i = 0;
125
126        while i < chars.len() {
127            let c = chars[i];
128            if c.is_numeric() {
129                // Scan the number
130                while i < chars.len() && chars[i].is_numeric() {
131                    i += 1;
132                }
133                release_end = i;
134                // Check if followed by a dot and more digits
135                if i < chars.len() && chars[i] == '.' {
136                    // Look ahead to see if there's a number after the dot
137                    if i + 1 < chars.len() && chars[i + 1].is_numeric() {
138                        i += 1; // Skip the dot
139                        continue;
140                    }
141                }
142                break;
143            } else if c == '.' {
144                i += 1;
145            } else {
146                break;
147            }
148        }
149
150        let release_str = &s[..release_end];
151        let remaining = s[release_end..].trim_start_matches('.');
152
153        // Parse release segment
154        let release: Result<Vec<u32>, _> = release_str
155            .split('.')
156            .filter(|p| !p.is_empty())
157            .map(|p| p.parse::<u32>())
158            .collect();
159
160        let release = release.map_err(|_| Error::InvalidVersion(original.to_string()))?;
161
162        if release.is_empty() {
163            return Err(Error::InvalidVersion(original.to_string()));
164        }
165
166        // Parse pre-release, post-release, and dev
167        let remaining_lower = remaining.to_lowercase();
168        let mut pos = 0;
169
170        while pos < remaining_lower.len() {
171            let rest = &remaining_lower[pos..];
172
173            if rest.starts_with("dev") {
174                let num_start = 3;
175                let num_end = rest[num_start..]
176                    .find(|c: char| !c.is_numeric())
177                    .map(|i| num_start + i)
178                    .unwrap_or(rest.len());
179
180                let num: u32 = if num_end > num_start {
181                    rest[num_start..num_end].parse().unwrap_or(0)
182                } else {
183                    0
184                };
185                dev = Some(num);
186                pos += num_end;
187            } else if rest.starts_with("post") || rest.starts_with("-") || rest.starts_with("r") {
188                let prefix_len = if rest.starts_with("post") { 4 } else { 1 };
189                let num_end = rest[prefix_len..]
190                    .find(|c: char| !c.is_numeric())
191                    .map(|i| prefix_len + i)
192                    .unwrap_or(rest.len());
193
194                let num: u32 = if num_end > prefix_len {
195                    rest[prefix_len..num_end].parse().unwrap_or(0)
196                } else {
197                    0
198                };
199                post = Some(num);
200                pos += num_end;
201            } else if rest.starts_with("alpha") || rest.starts_with('a') {
202                let prefix_len = if rest.starts_with("alpha") { 5 } else { 1 };
203                let num_end = rest[prefix_len..]
204                    .find(|c: char| !c.is_numeric())
205                    .map(|i| prefix_len + i)
206                    .unwrap_or(rest.len());
207
208                let num: u32 = if num_end > prefix_len {
209                    rest[prefix_len..num_end].parse().unwrap_or(0)
210                } else {
211                    0
212                };
213                pre = Some(PreRelease::Alpha(num));
214                pos += num_end;
215            } else if rest.starts_with("beta") || rest.starts_with('b') {
216                let prefix_len = if rest.starts_with("beta") { 4 } else { 1 };
217                let num_end = rest[prefix_len..]
218                    .find(|c: char| !c.is_numeric())
219                    .map(|i| prefix_len + i)
220                    .unwrap_or(rest.len());
221
222                let num: u32 = if num_end > prefix_len {
223                    rest[prefix_len..num_end].parse().unwrap_or(0)
224                } else {
225                    0
226                };
227                pre = Some(PreRelease::Beta(num));
228                pos += num_end;
229            } else if rest.starts_with("rc") || rest.starts_with('c') || rest.starts_with("preview")
230            {
231                let prefix_len = if rest.starts_with("preview") {
232                    7
233                } else if rest.starts_with("rc") {
234                    2
235                } else {
236                    1
237                };
238                let num_end = rest[prefix_len..]
239                    .find(|c: char| !c.is_numeric())
240                    .map(|i| prefix_len + i)
241                    .unwrap_or(rest.len());
242
243                let num: u32 = if num_end > prefix_len {
244                    rest[prefix_len..num_end].parse().unwrap_or(0)
245                } else {
246                    0
247                };
248                pre = Some(PreRelease::Rc(num));
249                pos += num_end;
250            } else {
251                // Skip separator characters or unknown characters
252                pos += 1;
253            }
254        }
255
256        Ok((release, pre, post, dev))
257    }
258}
259
260impl FromStr for Version {
261    type Err = Error;
262
263    fn from_str(s: &str) -> Result<Self, Self::Err> {
264        Self::parse(s)
265    }
266}
267
268impl fmt::Display for Version {
269    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270        if self.epoch > 0 {
271            write!(f, "{}!", self.epoch)?;
272        }
273
274        let release: Vec<String> = self.release.iter().map(|n| n.to_string()).collect();
275        write!(f, "{}", release.join("."))?;
276
277        if let Some(ref pre) = self.pre {
278            match pre {
279                PreRelease::Alpha(n) => write!(f, "a{}", n)?,
280                PreRelease::Beta(n) => write!(f, "b{}", n)?,
281                PreRelease::Rc(n) => write!(f, "rc{}", n)?,
282            }
283        }
284
285        if let Some(post) = self.post {
286            write!(f, ".post{}", post)?;
287        }
288
289        if let Some(dev) = self.dev {
290            write!(f, ".dev{}", dev)?;
291        }
292
293        if let Some(ref local) = self.local {
294            write!(f, "+{}", local)?;
295        }
296
297        Ok(())
298    }
299}
300
301impl PartialOrd for Version {
302    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
303        Some(self.cmp(other))
304    }
305}
306
307impl Ord for Version {
308    fn cmp(&self, other: &Self) -> Ordering {
309        // Compare epoch first
310        match self.epoch.cmp(&other.epoch) {
311            Ordering::Equal => {}
312            ord => return ord,
313        }
314
315        // Compare release segments
316        let max_len = self.release.len().max(other.release.len());
317        for i in 0..max_len {
318            let a = self.release.get(i).unwrap_or(&0);
319            let b = other.release.get(i).unwrap_or(&0);
320            match a.cmp(b) {
321                Ordering::Equal => {}
322                ord => return ord,
323            }
324        }
325
326        // Compare pre-release
327        // No pre < pre (pre-releases come before release)
328        // But dev releases without pre are even earlier
329        match (&self.pre, &other.pre) {
330            (None, Some(_)) => {
331                // self has no pre, other has pre
332                // A release without pre is GREATER than one with pre
333                // UNLESS self has dev (then it's less)
334                if self.dev.is_some() && other.dev.is_none() {
335                    return Ordering::Less;
336                }
337                return Ordering::Greater;
338            }
339            (Some(_), None) => {
340                // self has pre, other has no pre
341                if other.dev.is_some() && self.dev.is_none() {
342                    return Ordering::Greater;
343                }
344                return Ordering::Less;
345            }
346            (Some(a), Some(b)) => match a.cmp(b) {
347                Ordering::Equal => {}
348                ord => return ord,
349            },
350            (None, None) => {}
351        }
352
353        // Compare dev (dev releases come before non-dev)
354        match (&self.dev, &other.dev) {
355            (None, Some(_)) => return Ordering::Greater,
356            (Some(_), None) => return Ordering::Less,
357            (Some(a), Some(b)) => match a.cmp(b) {
358                Ordering::Equal => {}
359                ord => return ord,
360            },
361            (None, None) => {}
362        }
363
364        // Compare post (post releases come after non-post)
365        match (&self.post, &other.post) {
366            (None, Some(_)) => return Ordering::Less,
367            (Some(_), None) => return Ordering::Greater,
368            (Some(a), Some(b)) => match a.cmp(b) {
369                Ordering::Equal => {}
370                ord => return ord,
371            },
372            (None, None) => {}
373        }
374
375        // Local versions: presence of local makes it greater, then compare lexically
376        match (&self.local, &other.local) {
377            (None, Some(_)) => Ordering::Less,
378            (Some(_), None) => Ordering::Greater,
379            (Some(a), Some(b)) => a.cmp(b),
380            (None, None) => Ordering::Equal,
381        }
382    }
383}
384
385// Implement pubgrub's Version trait
386impl pubgrub::version::Version for Version {
387    /// Returns the lowest possible version
388    fn lowest() -> Self {
389        Self {
390            epoch: 0,
391            release: vec![0],
392            pre: Some(PreRelease::Alpha(0)),
393            dev: Some(0),
394            post: None,
395            local: None,
396        }
397    }
398
399    /// Returns the next version after self
400    fn bump(&self) -> Self {
401        let mut bumped = self.clone();
402
403        // Clear local version (it doesn't affect ordering in pubgrub context)
404        bumped.local = None;
405
406        // If we have dev, bump it
407        if let Some(dev) = bumped.dev {
408            bumped.dev = Some(dev + 1);
409            return bumped;
410        }
411
412        // If we have post, bump it
413        if let Some(post) = bumped.post {
414            bumped.post = Some(post + 1);
415            return bumped;
416        }
417
418        // If we have pre, either bump or remove
419        if let Some(ref pre) = bumped.pre {
420            match pre {
421                PreRelease::Alpha(n) => bumped.pre = Some(PreRelease::Alpha(n + 1)),
422                PreRelease::Beta(n) => bumped.pre = Some(PreRelease::Beta(n + 1)),
423                PreRelease::Rc(n) => bumped.pre = Some(PreRelease::Rc(n + 1)),
424            }
425            return bumped;
426        }
427
428        // Bump the last release segment
429        if let Some(last) = bumped.release.last_mut() {
430            *last += 1;
431        } else {
432            bumped.release.push(1);
433        }
434
435        bumped
436    }
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use pubgrub::version::Version as PubgrubVersion;
443
444    #[test]
445    fn test_parse_simple() {
446        let v = Version::parse("1.2.3").unwrap();
447        assert_eq!(v.release, vec![1, 2, 3]);
448    }
449
450    #[test]
451    fn test_parse_with_epoch() {
452        let v = Version::parse("1!2.3.4").unwrap();
453        assert_eq!(v.epoch, 1);
454        assert_eq!(v.release, vec![2, 3, 4]);
455    }
456
457    #[test]
458    fn test_parse_with_pre() {
459        let v = Version::parse("1.0a1").unwrap();
460        assert_eq!(v.pre, Some(PreRelease::Alpha(1)));
461
462        let v = Version::parse("1.0b2").unwrap();
463        assert_eq!(v.pre, Some(PreRelease::Beta(2)));
464
465        let v = Version::parse("1.0rc3").unwrap();
466        assert_eq!(v.pre, Some(PreRelease::Rc(3)));
467    }
468
469    #[test]
470    fn test_parse_with_post() {
471        let v = Version::parse("1.0.post1").unwrap();
472        assert_eq!(v.post, Some(1));
473    }
474
475    #[test]
476    fn test_parse_with_dev() {
477        let v = Version::parse("1.0.dev1").unwrap();
478        assert_eq!(v.dev, Some(1));
479    }
480
481    #[test]
482    fn test_parse_with_local() {
483        let v = Version::parse("1.0+local.1").unwrap();
484        assert_eq!(v.local, Some("local.1".to_string()));
485    }
486
487    #[test]
488    fn test_parse_complex() {
489        let v = Version::parse("1!2.3.4a1.post2.dev3+local").unwrap();
490        assert_eq!(v.epoch, 1);
491        assert_eq!(v.release, vec![2, 3, 4]);
492        assert_eq!(v.pre, Some(PreRelease::Alpha(1)));
493        assert_eq!(v.post, Some(2));
494        assert_eq!(v.dev, Some(3));
495        assert_eq!(v.local, Some("local".to_string()));
496    }
497
498    #[test]
499    fn test_display() {
500        let v = Version::new(vec![1, 2, 3]);
501        assert_eq!(v.to_string(), "1.2.3");
502
503        let mut v = Version::new(vec![1, 0]);
504        v.pre = Some(PreRelease::Alpha(1));
505        assert_eq!(v.to_string(), "1.0a1");
506    }
507
508    #[test]
509    fn test_ordering_release() {
510        let v1 = Version::parse("1.0.0").unwrap();
511        let v2 = Version::parse("1.0.1").unwrap();
512        let v3 = Version::parse("1.1.0").unwrap();
513
514        assert!(v1 < v2);
515        assert!(v2 < v3);
516        assert!(v1 < v3);
517    }
518
519    #[test]
520    fn test_ordering_pre() {
521        let v1 = Version::parse("1.0a1").unwrap();
522        let v2 = Version::parse("1.0a2").unwrap();
523        let v3 = Version::parse("1.0b1").unwrap();
524        let v4 = Version::parse("1.0rc1").unwrap();
525        let v5 = Version::parse("1.0").unwrap();
526
527        assert!(v1 < v2);
528        assert!(v2 < v3);
529        assert!(v3 < v4);
530        assert!(v4 < v5); // Pre-release < release
531    }
532
533    #[test]
534    fn test_ordering_post() {
535        let v1 = Version::parse("1.0").unwrap();
536        let v2 = Version::parse("1.0.post1").unwrap();
537        let v3 = Version::parse("1.0.post2").unwrap();
538
539        assert!(v1 < v2);
540        assert!(v2 < v3);
541    }
542
543    #[test]
544    fn test_ordering_dev() {
545        let v1 = Version::parse("1.0.dev1").unwrap();
546        let v2 = Version::parse("1.0.dev2").unwrap();
547        let v3 = Version::parse("1.0").unwrap();
548
549        assert!(v1 < v2);
550        assert!(v2 < v3); // Dev < release
551    }
552
553    #[test]
554    fn test_ordering_epoch() {
555        let v1 = Version::parse("1.0").unwrap();
556        let v2 = Version::parse("1!0.1").unwrap();
557
558        assert!(v1 < v2); // Epoch 0 < Epoch 1
559    }
560
561    #[test]
562    fn test_bump() {
563        let v1 = Version::parse("1.0.0").unwrap();
564        let v2 = v1.bump();
565        assert!(v1 < v2);
566
567        let v3 = Version::parse("1.0.dev1").unwrap();
568        let v4 = v3.bump();
569        assert!(v3 < v4);
570    }
571
572    #[test]
573    fn test_lowest() {
574        let lowest = Version::lowest();
575        let v1 = Version::parse("0.0.1").unwrap();
576        assert!(lowest < v1);
577    }
578}