Skip to main content

use_go_version/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Error returned when a Go version label is invalid.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum GoVersionParseError {
10    Empty,
11    InvalidVersion,
12    MissingMinor,
13    TooManyComponents,
14}
15
16impl fmt::Display for GoVersionParseError {
17    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::Empty => formatter.write_str("Go version cannot be empty"),
20            Self::InvalidVersion => formatter.write_str("invalid Go version"),
21            Self::MissingMinor => formatter.write_str("Go patch version requires a minor version"),
22            Self::TooManyComponents => formatter.write_str("Go version has too many components"),
23        }
24    }
25}
26
27impl Error for GoVersionParseError {}
28
29/// Major component of a Go version.
30#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct GoMajorVersion(u16);
32
33impl GoMajorVersion {
34    /// Creates a major version component.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`GoVersionParseError::InvalidVersion`] when `value` is zero.
39    pub const fn new(value: u16) -> Result<Self, GoVersionParseError> {
40        if value == 0 {
41            Err(GoVersionParseError::InvalidVersion)
42        } else {
43            Ok(Self(value))
44        }
45    }
46
47    /// Returns the numeric major version.
48    #[must_use]
49    pub const fn value(self) -> u16 {
50        self.0
51    }
52}
53
54impl fmt::Display for GoMajorVersion {
55    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(formatter, "{}", self.0)
57    }
58}
59
60/// Minor component of a Go version.
61#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
62pub struct GoMinorVersion(u16);
63
64impl GoMinorVersion {
65    /// Creates a minor version component.
66    #[must_use]
67    pub const fn new(value: u16) -> Self {
68        Self(value)
69    }
70
71    /// Returns the numeric minor version.
72    #[must_use]
73    pub const fn value(self) -> u16 {
74        self.0
75    }
76}
77
78impl fmt::Display for GoMinorVersion {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        write!(formatter, "{}", self.0)
81    }
82}
83
84/// Patch component of a Go version.
85#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
86pub struct GoPatchVersion(u16);
87
88impl GoPatchVersion {
89    /// Creates a patch version component.
90    #[must_use]
91    pub const fn new(value: u16) -> Self {
92        Self(value)
93    }
94
95    /// Returns the numeric patch version.
96    #[must_use]
97    pub const fn value(self) -> u16 {
98        self.0
99    }
100}
101
102impl fmt::Display for GoPatchVersion {
103    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104        write!(formatter, "{}", self.0)
105    }
106}
107
108/// Go major release family.
109#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
110pub enum GoVersionFamily {
111    Go1,
112    Go2,
113}
114
115impl GoVersionFamily {
116    /// Returns the family label.
117    #[must_use]
118    pub const fn as_str(self) -> &'static str {
119        match self {
120            Self::Go1 => "go1",
121            Self::Go2 => "go2",
122        }
123    }
124}
125
126impl fmt::Display for GoVersionFamily {
127    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
128        formatter.write_str(self.as_str())
129    }
130}
131
132impl FromStr for GoVersionFamily {
133    type Err = GoVersionParseError;
134
135    fn from_str(input: &str) -> Result<Self, Self::Err> {
136        match normalize_go_prefix(input)?.as_str() {
137            "1" => Ok(Self::Go1),
138            "2" => Ok(Self::Go2),
139            _ => Err(GoVersionParseError::InvalidVersion),
140        }
141    }
142}
143
144/// Parsed Go version metadata.
145#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
146pub struct GoVersion {
147    major: GoMajorVersion,
148    minor: Option<GoMinorVersion>,
149    patch: Option<GoPatchVersion>,
150}
151
152impl GoVersion {
153    /// Creates Go version metadata.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`GoVersionParseError::InvalidVersion`] when `major` is zero or
158    /// [`GoVersionParseError::MissingMinor`] when `patch` is provided without `minor`.
159    pub const fn new(
160        major: u16,
161        minor: Option<u16>,
162        patch: Option<u16>,
163    ) -> Result<Self, GoVersionParseError> {
164        if minor.is_none() && patch.is_some() {
165            return Err(GoVersionParseError::MissingMinor);
166        }
167
168        let Ok(major) = GoMajorVersion::new(major) else {
169            return Err(GoVersionParseError::InvalidVersion);
170        };
171
172        Ok(Self {
173            major,
174            minor: match minor {
175                Some(value) => Some(GoMinorVersion::new(value)),
176                None => None,
177            },
178            patch: match patch {
179                Some(value) => Some(GoPatchVersion::new(value)),
180                None => None,
181            },
182        })
183    }
184
185    /// Returns the major version number.
186    #[must_use]
187    pub const fn major(self) -> u16 {
188        self.major.value()
189    }
190
191    /// Returns the optional minor version number.
192    #[must_use]
193    pub const fn minor(self) -> Option<u16> {
194        match self.minor {
195            Some(value) => Some(value.value()),
196            None => None,
197        }
198    }
199
200    /// Returns the optional patch version number.
201    #[must_use]
202    pub const fn patch(self) -> Option<u16> {
203        match self.patch {
204            Some(value) => Some(value.value()),
205            None => None,
206        }
207    }
208
209    /// Returns the version family for known Go major release lines.
210    #[must_use]
211    pub const fn family(self) -> Option<GoVersionFamily> {
212        match self.major() {
213            1 => Some(GoVersionFamily::Go1),
214            2 => Some(GoVersionFamily::Go2),
215            _ => None,
216        }
217    }
218
219    /// Returns whether this version belongs to the Go 1 release family.
220    #[must_use]
221    pub const fn is_go1(self) -> bool {
222        matches!(self.family(), Some(GoVersionFamily::Go1))
223    }
224}
225
226impl fmt::Display for GoVersion {
227    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
228        write!(formatter, "{}", self.major)?;
229        if let Some(minor) = self.minor {
230            write!(formatter, ".{minor}")?;
231        }
232        if let Some(patch) = self.patch {
233            write!(formatter, ".{patch}")?;
234        }
235        Ok(())
236    }
237}
238
239impl FromStr for GoVersion {
240    type Err = GoVersionParseError;
241
242    fn from_str(input: &str) -> Result<Self, Self::Err> {
243        parse_go_version(input)
244    }
245}
246
247impl TryFrom<&str> for GoVersion {
248    type Error = GoVersionParseError;
249
250    fn try_from(value: &str) -> Result<Self, Self::Error> {
251        Self::from_str(value)
252    }
253}
254
255/// Go toolchain version metadata.
256#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
257pub struct GoToolchainVersion(GoVersion);
258
259impl GoToolchainVersion {
260    /// Creates toolchain version metadata.
261    #[must_use]
262    pub const fn new(version: GoVersion) -> Self {
263        Self(version)
264    }
265
266    /// Returns the inner Go version.
267    #[must_use]
268    pub const fn version(self) -> GoVersion {
269        self.0
270    }
271}
272
273impl fmt::Display for GoToolchainVersion {
274    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
275        write!(formatter, "go{}", self.0)
276    }
277}
278
279impl FromStr for GoToolchainVersion {
280    type Err = GoVersionParseError;
281
282    fn from_str(input: &str) -> Result<Self, Self::Err> {
283        input.parse::<GoVersion>().map(Self)
284    }
285}
286
287impl TryFrom<&str> for GoToolchainVersion {
288    type Error = GoVersionParseError;
289
290    fn try_from(value: &str) -> Result<Self, Self::Error> {
291        Self::from_str(value)
292    }
293}
294
295/// Go compatibility version metadata.
296#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
297pub struct GoCompatibilityVersion(GoVersion);
298
299impl GoCompatibilityVersion {
300    /// Creates compatibility version metadata.
301    #[must_use]
302    pub const fn new(version: GoVersion) -> Self {
303        Self(version)
304    }
305
306    /// Returns the inner Go version.
307    #[must_use]
308    pub const fn version(self) -> GoVersion {
309        self.0
310    }
311}
312
313impl fmt::Display for GoCompatibilityVersion {
314    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
315        self.0.fmt(formatter)
316    }
317}
318
319impl FromStr for GoCompatibilityVersion {
320    type Err = GoVersionParseError;
321
322    fn from_str(input: &str) -> Result<Self, Self::Err> {
323        input.parse::<GoVersion>().map(Self)
324    }
325}
326
327impl TryFrom<&str> for GoCompatibilityVersion {
328    type Error = GoVersionParseError;
329
330    fn try_from(value: &str) -> Result<Self, Self::Error> {
331        Self::from_str(value)
332    }
333}
334
335fn parse_go_version(input: &str) -> Result<GoVersion, GoVersionParseError> {
336    let normalized = normalize_go_prefix(input)?;
337    let components = normalized.split('.').collect::<Vec<_>>();
338
339    if components.len() > 3 {
340        return Err(GoVersionParseError::TooManyComponents);
341    }
342
343    if components.iter().any(|component| component.is_empty()) {
344        return Err(GoVersionParseError::InvalidVersion);
345    }
346
347    let major = parse_component(components[0])?;
348    let minor = match components.get(1) {
349        Some(component) => Some(parse_component(component)?),
350        None => None,
351    };
352    let patch = match components.get(2) {
353        Some(component) => Some(parse_component(component)?),
354        None => None,
355    };
356
357    GoVersion::new(major, minor, patch)
358}
359
360fn normalize_go_prefix(input: &str) -> Result<String, GoVersionParseError> {
361    let trimmed = input.trim();
362    if trimmed.is_empty() {
363        return Err(GoVersionParseError::Empty);
364    }
365
366    let mut characters = trimmed.char_indices();
367    let first = characters.next();
368    let second = characters.next();
369    let without_prefix =
370        if matches!(first, Some((_, 'g' | 'G'))) && matches!(second, Some((_, 'o' | 'O'))) {
371            let Some((index, character)) = second else {
372                return Err(GoVersionParseError::InvalidVersion);
373            };
374            trimmed[index + character.len_utf8()..].trim_start()
375        } else {
376            trimmed
377        };
378
379    if without_prefix.is_empty() {
380        Err(GoVersionParseError::InvalidVersion)
381    } else {
382        Ok(without_prefix.to_string())
383    }
384}
385
386fn parse_component(component: &str) -> Result<u16, GoVersionParseError> {
387    if !component
388        .chars()
389        .all(|character| character.is_ascii_digit())
390    {
391        return Err(GoVersionParseError::InvalidVersion);
392    }
393    component
394        .parse::<u16>()
395        .map_err(|_| GoVersionParseError::InvalidVersion)
396}
397
398#[cfg(test)]
399mod tests {
400    use super::{
401        GoCompatibilityVersion, GoToolchainVersion, GoVersion, GoVersionFamily, GoVersionParseError,
402    };
403
404    #[test]
405    fn parses_go_versions() -> Result<(), GoVersionParseError> {
406        assert_eq!("1".parse::<GoVersion>()?.to_string(), "1");
407        assert_eq!("1.21".parse::<GoVersion>()?.to_string(), "1.21");
408        assert_eq!("1.21.6".parse::<GoVersion>()?.to_string(), "1.21.6");
409        assert_eq!("go1.22.0".parse::<GoVersion>()?.to_string(), "1.22.0");
410        assert_eq!("Go 1.23.1".parse::<GoVersion>()?.to_string(), "1.23.1");
411        Ok(())
412    }
413
414    #[test]
415    fn exposes_version_helpers() -> Result<(), GoVersionParseError> {
416        let version: GoVersion = "1.22.0".parse()?;
417        assert_eq!(version.major(), 1);
418        assert_eq!(version.minor(), Some(22));
419        assert_eq!(version.patch(), Some(0));
420        assert_eq!(version.family(), Some(GoVersionFamily::Go1));
421        assert!(version.is_go1());
422        Ok(())
423    }
424
425    #[test]
426    fn rejects_invalid_versions() {
427        assert_eq!("".parse::<GoVersion>(), Err(GoVersionParseError::Empty));
428        assert_eq!(
429            "0".parse::<GoVersion>(),
430            Err(GoVersionParseError::InvalidVersion)
431        );
432        assert_eq!(
433            "1.2.3.4".parse::<GoVersion>(),
434            Err(GoVersionParseError::TooManyComponents)
435        );
436        assert_eq!(
437            "1.x".parse::<GoVersion>(),
438            Err(GoVersionParseError::InvalidVersion)
439        );
440        assert_eq!(
441            "π".parse::<GoVersion>(),
442            Err(GoVersionParseError::InvalidVersion)
443        );
444    }
445
446    #[test]
447    fn models_toolchain_and_compatibility_versions() -> Result<(), GoVersionParseError> {
448        let version: GoVersion = "1.21".parse()?;
449        let toolchain = GoToolchainVersion::new(version);
450        let compatibility = GoCompatibilityVersion::new(version);
451
452        assert_eq!(toolchain.to_string(), "go1.21");
453        assert_eq!(compatibility.to_string(), "1.21");
454        assert_eq!("go2".parse::<GoVersionFamily>()?, GoVersionFamily::Go2);
455        Ok(())
456    }
457}