Skip to main content

frm/
version.rs

1// Copyright (c) 2025-2026 Michael S. Klishin and Contributors
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::cmp::Ordering;
10use std::fmt;
11use std::str::FromStr;
12
13use serde::{Deserialize, Serialize};
14
15use crate::errors::Error;
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
18pub enum Prerelease {
19    Alpha(String),
20    Beta(String),
21    Rc(String),
22}
23
24impl Prerelease {
25    pub fn is_alpha(&self) -> bool {
26        matches!(self, Prerelease::Alpha(_))
27    }
28}
29
30impl fmt::Display for Prerelease {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            Prerelease::Alpha(s) => write!(f, "alpha.{}", s),
34            Prerelease::Beta(s) => write!(f, "beta.{}", s),
35            Prerelease::Rc(s) => write!(f, "rc.{}", s),
36        }
37    }
38}
39
40fn compare_prerelease_identifiers(a: &str, b: &str) -> Ordering {
41    match (a.parse::<u32>(), b.parse::<u32>()) {
42        (Ok(na), Ok(nb)) => na.cmp(&nb),
43        _ => a.cmp(b),
44    }
45}
46
47impl Ord for Prerelease {
48    fn cmp(&self, other: &Self) -> Ordering {
49        match (self, other) {
50            (Prerelease::Alpha(a), Prerelease::Alpha(b)) => compare_prerelease_identifiers(a, b),
51            (Prerelease::Alpha(_), _) => Ordering::Less,
52            (_, Prerelease::Alpha(_)) => Ordering::Greater,
53            (Prerelease::Beta(a), Prerelease::Beta(b)) => compare_prerelease_identifiers(a, b),
54            (Prerelease::Beta(_), Prerelease::Rc(_)) => Ordering::Less,
55            (Prerelease::Rc(_), Prerelease::Beta(_)) => Ordering::Greater,
56            (Prerelease::Rc(a), Prerelease::Rc(b)) => compare_prerelease_identifiers(a, b),
57        }
58    }
59}
60
61impl PartialOrd for Prerelease {
62    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
63        Some(self.cmp(other))
64    }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
68pub struct Version {
69    pub major: u32,
70    pub minor: u32,
71    pub patch: u32,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub prerelease: Option<Prerelease>,
74}
75
76impl Version {
77    pub fn new(major: u32, minor: u32, patch: u32) -> Self {
78        Self {
79            major,
80            minor,
81            patch,
82            prerelease: None,
83        }
84    }
85
86    pub fn with_prerelease(major: u32, minor: u32, patch: u32, prerelease: Prerelease) -> Self {
87        Self {
88            major,
89            minor,
90            patch,
91            prerelease: Some(prerelease),
92        }
93    }
94
95    pub fn dir_name(&self) -> String {
96        self.to_string()
97    }
98
99    pub fn is_distributed_via_server_packages_repository(&self) -> bool {
100        self.prerelease.as_ref().is_some_and(|p| p.is_alpha())
101    }
102
103    pub fn is_ga(&self) -> bool {
104        self.prerelease.is_none()
105    }
106
107    pub fn download_url(&self) -> String {
108        format!(
109            "https://github.com/rabbitmq/rabbitmq-server/releases/download/v{v}/rabbitmq-server-generic-unix-{v}.tar.xz",
110            v = self
111        )
112    }
113
114    pub fn download_url_with_tag(&self, tag: &str) -> String {
115        format!(
116            "https://github.com/rabbitmq/server-packages/releases/download/{tag}/rabbitmq-server-generic-unix-{v}.tar.xz",
117            tag = tag,
118            v = self
119        )
120    }
121
122    pub fn archive_name(&self) -> String {
123        format!("rabbitmq-server-generic-unix-{}.tar.xz", self)
124    }
125
126    pub fn extracted_dir_name(&self) -> String {
127        format!("rabbitmq_server-{}", self)
128    }
129}
130
131impl fmt::Display for Version {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
134        if let Some(ref pre) = self.prerelease {
135            write!(f, "-{}", pre)?;
136        }
137        Ok(())
138    }
139}
140
141impl FromStr for Version {
142    type Err = Error;
143
144    fn from_str(s: &str) -> Result<Self, Self::Err> {
145        let s = s.trim().trim_start_matches('v');
146
147        let (version_part, prerelease) = if let Some(idx) = s.find('-') {
148            let (ver, pre) = s.split_at(idx);
149            let pre = &pre[1..];
150            (ver, Some(parse_prerelease(pre, s)?))
151        } else {
152            (s, None)
153        };
154
155        let parts: Vec<&str> = version_part.split('.').collect();
156
157        if parts.len() != 3 {
158            return Err(Error::InvalidVersion(s.to_string()));
159        }
160
161        let major = parts[0]
162            .parse()
163            .map_err(|_| Error::InvalidVersion(s.to_string()))?;
164        let minor = parts[1]
165            .parse()
166            .map_err(|_| Error::InvalidVersion(s.to_string()))?;
167        let patch = parts[2]
168            .parse()
169            .map_err(|_| Error::InvalidVersion(s.to_string()))?;
170
171        Ok(Version {
172            major,
173            minor,
174            patch,
175            prerelease,
176        })
177    }
178}
179
180fn parse_prerelease(s: &str, full: &str) -> Result<Prerelease, Error> {
181    let parts: Vec<&str> = s.split('.').collect();
182    if parts.len() != 2 {
183        return Err(Error::InvalidVersion(full.to_string()));
184    }
185
186    let identifier = parts[1];
187    if identifier.is_empty() {
188        return Err(Error::InvalidVersion(full.to_string()));
189    }
190
191    match parts[0].to_lowercase().as_str() {
192        "alpha" => Ok(Prerelease::Alpha(identifier.to_string())),
193        "beta" => Ok(Prerelease::Beta(identifier.to_string())),
194        "rc" => Ok(Prerelease::Rc(identifier.to_string())),
195        _ => Err(Error::InvalidVersion(full.to_string())),
196    }
197}
198
199impl Ord for Version {
200    fn cmp(&self, other: &Self) -> Ordering {
201        let base =
202            (self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch));
203        if base != Ordering::Equal {
204            return base;
205        }
206
207        match (&self.prerelease, &other.prerelease) {
208            (None, None) => Ordering::Equal,
209            (Some(_), None) => Ordering::Less,
210            (None, Some(_)) => Ordering::Greater,
211            (Some(a), Some(b)) => a.cmp(b),
212        }
213    }
214}
215
216impl PartialOrd for Version {
217    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
218        Some(self.cmp(other))
219    }
220}