Skip to main content

compose_lens/validation/
version.rs

1//! Numeric implementation versions and evidence ranges.
2
3use std::error::Error;
4use std::fmt;
5use std::str::FromStr;
6
7/// An exact three-component implementation version.
8///
9/// `ComposeLens` deliberately does not infer a current version or interpret pre-release/build
10/// metadata. Callers select the exact released implementation they intend to assess.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct ImplementationVersion {
13    major: u32,
14    minor: u32,
15    patch: u32,
16}
17
18impl ImplementationVersion {
19    /// Creates an exact numeric version.
20    #[must_use]
21    pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
22        Self { major, minor, patch }
23    }
24
25    /// Returns the major component.
26    #[must_use]
27    pub const fn major(self) -> u32 {
28        self.major
29    }
30
31    /// Returns the minor component.
32    #[must_use]
33    pub const fn minor(self) -> u32 {
34        self.minor
35    }
36
37    /// Returns the patch component.
38    #[must_use]
39    pub const fn patch(self) -> u32 {
40        self.patch
41    }
42}
43
44impl fmt::Display for ImplementationVersion {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
47    }
48}
49
50impl FromStr for ImplementationVersion {
51    type Err = VersionParseError;
52
53    fn from_str(value: &str) -> Result<Self, Self::Err> {
54        let value = value.strip_prefix('v').unwrap_or(value);
55        let mut components = value.split('.');
56        let major = parse_component(components.next())?;
57        let minor = parse_component(components.next())?;
58        let patch = parse_component(components.next())?;
59        if components.next().is_some() {
60            return Err(VersionParseError);
61        }
62        Ok(Self::new(major, minor, patch))
63    }
64}
65
66fn parse_component(component: Option<&str>) -> Result<u32, VersionParseError> {
67    let component = component
68        .filter(|component| !component.is_empty())
69        .ok_or(VersionParseError)?;
70    if !component.bytes().all(|byte| byte.is_ascii_digit()) {
71        return Err(VersionParseError);
72    }
73    component.parse().map_err(|_| VersionParseError)
74}
75
76/// An exact implementation version could not be parsed.
77///
78/// The error does not retain or display the supplied value so version parsing can safely be used
79/// with untrusted command input.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct VersionParseError;
82
83impl fmt::Display for VersionParseError {
84    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
85        formatter.write_str("expected a three-component numeric implementation version")
86    }
87}
88
89impl Error for VersionParseError {}
90
91/// An inclusive implementation-version range attached to compatibility evidence.
92#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
93pub struct VersionRange {
94    minimum: Option<ImplementationVersion>,
95    maximum: Option<ImplementationVersion>,
96}
97
98impl VersionRange {
99    /// Creates an unbounded range.
100    #[must_use]
101    pub const fn unbounded() -> Self {
102        Self {
103            minimum: None,
104            maximum: None,
105        }
106    }
107
108    /// Creates a range with an inclusive minimum and no maximum.
109    #[must_use]
110    pub const fn from_minimum(minimum: ImplementationVersion) -> Self {
111        Self {
112            minimum: Some(minimum),
113            maximum: None,
114        }
115    }
116
117    /// Creates a range containing one exact version.
118    #[must_use]
119    pub const fn exact(version: ImplementationVersion) -> Self {
120        Self {
121            minimum: Some(version),
122            maximum: Some(version),
123        }
124    }
125
126    /// Creates a checked inclusive range.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`InvalidVersionRange`] when both bounds exist and the minimum is newer than the
131    /// maximum.
132    pub fn new(
133        minimum: Option<ImplementationVersion>,
134        maximum: Option<ImplementationVersion>,
135    ) -> Result<Self, InvalidVersionRange> {
136        if minimum.zip(maximum).is_some_and(|(minimum, maximum)| minimum > maximum) {
137            return Err(InvalidVersionRange);
138        }
139        Ok(Self { minimum, maximum })
140    }
141
142    /// Returns the inclusive minimum.
143    #[must_use]
144    pub const fn minimum(self) -> Option<ImplementationVersion> {
145        self.minimum
146    }
147
148    /// Returns the inclusive maximum.
149    #[must_use]
150    pub const fn maximum(self) -> Option<ImplementationVersion> {
151        self.maximum
152    }
153
154    /// Reports whether an exact version is inside the inclusive range.
155    #[must_use]
156    pub fn contains(self, version: ImplementationVersion) -> bool {
157        self.minimum.is_none_or(|minimum| version >= minimum) && self.maximum.is_none_or(|maximum| version <= maximum)
158    }
159}
160
161/// A version range has its minimum after its maximum.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub struct InvalidVersionRange;
164
165impl fmt::Display for InvalidVersionRange {
166    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167        formatter.write_str("minimum implementation version is newer than maximum")
168    }
169}
170
171impl Error for InvalidVersionRange {}