Skip to main content

vynil_core/
semver.rs

1//! Semver parsing and bumping.
2//!
3//! `Semver` wraps `semver::Version` and preserves an optional leading `v`.
4//! Rhai helpers (`semver_from`, `inc_major`/`inc_minor`/… + comparison ops) are registered by
5//! `semver_rhai_register`.
6
7use crate::{Error, Result};
8#[cfg(feature = "rhai")] use crate::{RhaiRes, rhai_err};
9#[cfg(feature = "rhai")] use rhai::Engine;
10use semver::{Prerelease, Version};
11
12/// Semver wrapper that remembers whether the original string had a leading `v`.
13///
14/// Implements `Display` so `to_string()` round-trips the `v` prefix. Ordering is delegated to
15/// the inner `semver::Version`.
16#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
17pub struct Semver {
18    pub version: Version,
19    pub use_v: bool,
20}
21
22impl Semver {
23    pub fn parse(str: &str) -> Result<Self> {
24        let use_v = str.starts_with("v");
25        let version = if use_v {
26            let mut chars = str.chars();
27            chars.next();
28            Version::parse(chars.as_str()).map_err(Error::Semver)?
29        } else {
30            Version::parse(str).map_err(Error::Semver)?
31        };
32        Ok(Self { version, use_v })
33    }
34
35    pub fn opt_parse(str: &str) -> Option<Self> {
36        Self::parse(str).ok()
37    }
38
39    #[cfg(feature = "rhai")]
40    pub fn rhai_parse(str: &str) -> RhaiRes<Self> {
41        Self::parse(str).map_err(rhai_err)
42    }
43
44    pub fn inc_major(&mut self) {
45        self.version.major += 1;
46        self.version.minor = 0;
47        self.version.patch = 0;
48        self.version.pre = Prerelease::EMPTY;
49    }
50
51    pub fn inc_minor(&mut self) {
52        self.version.minor += 1;
53        self.version.patch = 0;
54        self.version.pre = Prerelease::EMPTY;
55    }
56
57    pub fn inc_patch(&mut self) {
58        if self.version.pre.is_empty() {
59            self.version.patch += 1;
60        } else {
61            self.version.pre = Prerelease::EMPTY;
62        }
63    }
64
65    pub fn inc_beta(&mut self) -> Result<()> {
66        if self.version.pre.is_empty() || !self.version.pre.starts_with("beta.") {
67            self.version.patch += 1;
68            self.version.pre = Prerelease::new("beta.1").map_err(Error::Semver)?;
69        } else {
70            let str = self.version.pre.strip_prefix("beta.").unwrap().to_string();
71            let beta = str.parse::<u32>().unwrap() + 1;
72            self.version.pre = Prerelease::new(&format!("beta.{beta}")).map_err(Error::Semver)?;
73        }
74        Ok(())
75    }
76
77    #[cfg(feature = "rhai")]
78    pub fn rhai_inc_beta(&mut self) -> RhaiRes<()> {
79        self.inc_beta().map_err(rhai_err)
80    }
81
82    pub fn inc_alpha(&mut self) -> Result<()> {
83        if self.version.pre.is_empty() || !self.version.pre.starts_with("alpha.") {
84            self.version.patch += 1;
85            self.version.pre = Prerelease::new("alpha.1").map_err(Error::Semver)?;
86        } else {
87            let str = self.version.pre.strip_prefix("alpha.").unwrap().to_string();
88            let alpha = str.parse::<u32>().unwrap() + 1;
89            self.version.pre = Prerelease::new(&format!("alpha.{alpha}")).map_err(Error::Semver)?;
90        }
91        Ok(())
92    }
93
94    #[cfg(feature = "rhai")]
95    pub fn rhai_inc_alpha(&mut self) -> RhaiRes<()> {
96        self.inc_alpha().map_err(rhai_err)
97    }
98}
99
100impl std::fmt::Display for Semver {
101    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
102        if self.use_v {
103            write!(formatter, "v{}", self.version)
104        } else {
105            self.version.fmt(formatter)
106        }
107    }
108}
109
110#[cfg(feature = "rhai")]
111pub fn semver_rhai_register(engine: &mut Engine) {
112    engine
113        .register_type_with_name::<Semver>("Semver")
114        .register_fn("semver_from", Semver::rhai_parse)
115        .register_fn("inc_major", Semver::inc_major)
116        .register_fn("inc_minor", Semver::inc_minor)
117        .register_fn("inc_patch", Semver::inc_patch)
118        .register_fn("inc_beta", Semver::rhai_inc_beta)
119        .register_fn("inc_alpha", Semver::rhai_inc_alpha)
120        .register_fn("==", |a: Semver, b: Semver| a == b)
121        .register_fn("!=", |a: Semver, b: Semver| a != b)
122        .register_fn("<", |a: Semver, b: Semver| a < b)
123        .register_fn(">", |a: Semver, b: Semver| a > b)
124        .register_fn("<=", |a: Semver, b: Semver| a <= b)
125        .register_fn(">=", |a: Semver, b: Semver| a >= b)
126        .register_fn("to_string", |s: &mut Semver| s.to_string());
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_parse_without_v_prefix() {
135        let sv = Semver::parse("1.2.3").unwrap();
136        assert_eq!(sv.version.major, 1);
137        assert_eq!(sv.version.minor, 2);
138        assert_eq!(sv.version.patch, 3);
139        assert!(!sv.use_v);
140    }
141
142    #[test]
143    fn test_parse_with_v_prefix() {
144        let sv = Semver::parse("v1.2.3").unwrap();
145        assert_eq!(sv.version.major, 1);
146        assert_eq!(sv.version.minor, 2);
147        assert_eq!(sv.version.patch, 3);
148        assert!(sv.use_v);
149    }
150
151    #[test]
152    fn test_to_string_preserves_v_prefix() {
153        let mut sv = Semver::parse("v1.2.3").unwrap();
154        assert_eq!(Semver::to_string(&mut sv), "v1.2.3");
155    }
156
157    #[test]
158    fn test_to_string_without_v_prefix() {
159        let mut sv = Semver::parse("1.2.3").unwrap();
160        assert_eq!(Semver::to_string(&mut sv), "1.2.3");
161    }
162
163    #[test]
164    fn test_comparison_lt() {
165        let v1 = Semver::parse("1.2.3").unwrap();
166        let v2 = Semver::parse("1.2.4").unwrap();
167        assert!(v1 < v2);
168        assert!(v2 > v1);
169    }
170
171    #[test]
172    fn test_comparison_eq() {
173        let v1 = Semver::parse("1.2.3").unwrap();
174        let v2 = Semver::parse("1.2.3").unwrap();
175        assert!(v1 == v2);
176        assert!(v1 <= v2);
177        assert!(v1 >= v2);
178    }
179
180    #[test]
181    fn test_comparison_major_beats_minor() {
182        let v1 = Semver::parse("2.0.0").unwrap();
183        let v2 = Semver::parse("1.99.99").unwrap();
184        assert!(v1 > v2);
185    }
186
187    #[test]
188    fn test_comparison_v_prefix_transparent() {
189        let v1 = Semver::parse("v1.2.3").unwrap();
190        let v2 = Semver::parse("2.0.0").unwrap();
191        assert!(v1 < v2);
192    }
193
194    #[test]
195    fn test_inc_major_resets_minor_and_patch() {
196        let mut sv = Semver::parse("1.2.3").unwrap();
197        sv.inc_major();
198        assert_eq!(sv.version.major, 2);
199        assert_eq!(sv.version.minor, 0);
200        assert_eq!(sv.version.patch, 0);
201        assert!(sv.version.pre.is_empty());
202    }
203
204    #[test]
205    fn test_inc_minor_resets_patch() {
206        let mut sv = Semver::parse("1.2.3").unwrap();
207        sv.inc_minor();
208        assert_eq!(sv.version.minor, 3);
209        assert_eq!(sv.version.patch, 0);
210        assert!(sv.version.pre.is_empty());
211    }
212
213    #[test]
214    fn test_inc_patch_stable() {
215        let mut sv = Semver::parse("1.2.3").unwrap();
216        sv.inc_patch();
217        assert_eq!(sv.version.patch, 4);
218    }
219
220    #[test]
221    fn test_inc_patch_clears_prerelease_without_bumping_patch() {
222        let mut sv = Semver::parse("1.2.3-beta.1").unwrap();
223        sv.inc_patch();
224        assert_eq!(sv.version.patch, 3);
225        assert!(sv.version.pre.is_empty());
226    }
227
228    #[test]
229    fn test_inc_beta_from_stable_bumps_patch() {
230        let mut sv = Semver::parse("1.2.3").unwrap();
231        sv.inc_beta().unwrap();
232        assert_eq!(sv.version.patch, 4);
233        assert_eq!(sv.version.pre.as_str(), "beta.1");
234    }
235
236    #[test]
237    fn test_inc_beta_from_existing_beta_increments_counter() {
238        let mut sv = Semver::parse("1.2.4-beta.1").unwrap();
239        sv.inc_beta().unwrap();
240        assert_eq!(sv.version.patch, 4);
241        assert_eq!(sv.version.pre.as_str(), "beta.2");
242    }
243
244    #[test]
245    fn test_inc_alpha_from_stable_bumps_patch() {
246        let mut sv = Semver::parse("1.2.3").unwrap();
247        sv.inc_alpha().unwrap();
248        assert_eq!(sv.version.patch, 4);
249        assert_eq!(sv.version.pre.as_str(), "alpha.1");
250    }
251
252    #[test]
253    fn test_inc_alpha_from_existing_alpha_increments_counter() {
254        let mut sv = Semver::parse("1.2.4-alpha.2").unwrap();
255        sv.inc_alpha().unwrap();
256        assert_eq!(sv.version.pre.as_str(), "alpha.3");
257    }
258
259    #[test]
260    fn test_parse_invalid_returns_error() {
261        assert!(Semver::parse("not-semver").is_err());
262        assert!(Semver::parse("1.2").is_err());
263        assert!(Semver::parse("").is_err());
264    }
265
266    #[test]
267    fn test_opt_parse_invalid_returns_none() {
268        assert!(Semver::opt_parse("not-semver").is_none());
269    }
270
271    #[test]
272    fn test_opt_parse_valid_returns_some() {
273        assert!(Semver::opt_parse("1.0.0").is_some());
274    }
275
276    #[test]
277    fn test_prerelease_is_less_than_stable() {
278        let pre = Semver::parse("1.2.3-alpha.1").unwrap();
279        let stable = Semver::parse("1.2.3").unwrap();
280        assert!(pre < stable);
281    }
282}