terraform_version/
lib.rs

1//!`terraform-version` is a short parser and match calculator for terraform version constraint syntax.
2//!
3//! It follows the [terraform semantic constraints](https://developer.hashicorp.com/terraform/language/expressions/version-constraints).
4//!
5//! *Compiler support: requires rustc 1.67+*
6//!
7//!
8//! ## Example
9//!
10//! ```rust
11//! # use terraform_version::{Version, VersionRequirement, NumericIdentifiers};
12//!     let version_req = VersionRequirement::parse("< 5.4.3, >= 1.2.3").unwrap();
13//!
14//!     let version = Version::parse("1.2.3").unwrap();
15//!     assert!(version.matches(&version_req));
16//!
17//!     let version = Version::parse("5.4.4").unwrap();
18//!     assert!(!version.matches(&version_req));
19//!
20//!
21//!     let version_req = VersionRequirement::parse("= 1.2.3-beta").unwrap();
22//!
23//!     let version = Version::parse("1.2.3-beta").unwrap();
24//!     assert!(version.matches(&version_req));
25//!
26//!     let version = Version {
27//!         numeric_identifiers: NumericIdentifiers::new(vec![1, 2, 3]),
28//!         suffix: None
29//!     };
30//!     assert!(!version.matches(&version_req));
31//!
32//! ```
33//!
34//! ## License
35//!
36//! `terraform-version` is provided under the MIT license. See [LICENSE](./LICENSE).
37
38use std::{cmp, fmt};
39
40pub use error::Error;
41use error::Result;
42
43mod error;
44
45#[derive(Clone, PartialEq, Eq, Debug)]
46pub struct Version {
47    /// Series of numbers, usually representing major, minor and patch (when semantic versioning is respected).
48    pub numeric_identifiers: NumericIdentifiers,
49    /// Equivalent of prerelease and/or build metadata in semantic versioning syntax.
50    pub suffix: Option<String>,
51}
52
53impl Version {
54    /// Try to create `Version` from given string.
55    /// If the string contains a `-` character, it is split in two, the first part will be considered as the numeric identifier and the 2nd one as suffix.
56    pub fn parse(text: &str) -> Result<Self> {
57        let vec: Vec<&str> = text.splitn(2, '-').collect();
58
59        if vec == [""] {
60            return Err(Error::NoVersion);
61        }
62
63        let numeric_identifiers = NumericIdentifiers::parse(vec.first().ok_or(Error::NoVersion)?)?;
64
65        let suffix = vec.get(1).map(|pr| pr.to_string());
66
67        Ok(Self {
68            numeric_identifiers,
69            suffix,
70        })
71    }
72
73    /// Evaluate whether `self` satisfies the given `VersionRequirement`.
74    pub fn matches(&self, vr: &VersionRequirement) -> bool {
75        vr.comparators.iter().all(|c| self.matches_comparators(c))
76    }
77
78    fn matches_comparators(&self, cmp: &Comparator) -> bool {
79        let useful_len = cmp.version.numeric_identifiers.0.len();
80        let useful_lhs = &self.numeric_identifiers.0[..useful_len];
81        let useful_rhs = cmp.version.numeric_identifiers.0.as_slice();
82
83        match cmp.operator.unwrap_or(Operator::Exact) {
84            Operator::Exact => self == &cmp.version,
85            Operator::Different => self != &cmp.version,
86            Operator::Greater => useful_lhs > useful_rhs,
87            Operator::GreaterEq => useful_lhs >= useful_rhs,
88            Operator::Less => useful_lhs < useful_rhs,
89            Operator::LessEq => useful_lhs <= useful_rhs,
90            Operator::RightMost => {
91                let prefix_len = useful_rhs.len() - 1;
92                let prefix_lhs = &useful_lhs[..prefix_len];
93                let prefix_rhs = &useful_rhs[..prefix_len];
94
95                (useful_lhs >= useful_rhs) && (prefix_lhs == prefix_rhs)
96            }
97        }
98    }
99}
100
101impl cmp::PartialOrd for Version {
102    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
103        self.numeric_identifiers
104            .partial_cmp(&other.numeric_identifiers)
105    }
106}
107
108impl cmp::Ord for Version {
109    fn cmp(&self, other: &Self) -> cmp::Ordering {
110        self.numeric_identifiers.cmp(&other.numeric_identifiers)
111    }
112}
113
114impl fmt::Display for Version {
115    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
116        let pr = match &self.suffix {
117            None => "".to_string(),
118            Some(suffix) => format!("-{}", suffix),
119        };
120        write!(f, "{}{pr}", self.numeric_identifiers)
121    }
122}
123
124/// Version requirement describing the intersection of some version comparators, such as `>= 1.2.3, != 1.3.0`.
125#[derive(Clone, PartialEq, Eq, Debug)]
126pub struct VersionRequirement {
127    pub comparators: Vec<Comparator>,
128}
129
130impl VersionRequirement {
131    /// Try to create a `VersionRequirement` from given string.
132    /// Each `Comparator` is separated  by a `,` character.
133    pub fn parse(text: &str) -> Result<Self> {
134        let vec: Vec<&str> = text.split(',').map(|s| s.trim()).collect();
135        if vec == [""] {
136            return Err(Error::NoVersionRequirement);
137        }
138
139        let comparators: Vec<Comparator> = vec
140            .iter()
141            .map(|comp| Comparator::parse(comp))
142            .collect::<Result<Vec<Comparator>>>()?;
143
144        if comparators.len() > 1
145            && comparators
146                .iter()
147                .any(|c| c.operator == Some(Operator::Exact) || c.operator.is_none())
148        {
149            return Err(Error::NotAllowedOperatorWithMultipleComparators(
150                text.to_string(),
151            ));
152        }
153
154        Ok(Self { comparators })
155    }
156
157    /// Returns true if self has a single `Comparator` without `Operator`
158    pub fn is_without_operator(&self) -> bool {
159        match &self.comparators[..] {
160            [item] => item.operator.is_none(),
161            _ => false,
162        }
163    }
164}
165
166#[derive(Clone, PartialEq, Eq, Debug)]
167pub struct Comparator {
168    /// None is considered like "Exact" Operator for matching
169    pub operator: Option<Operator>,
170    pub version: Version,
171}
172
173impl Comparator {
174    fn parse(text: &str) -> Result<Self> {
175        let Some((operator, version)) = Comparator::split_and_parse_operator(text) else {
176            return Err(Error::InvalidOperator(text.to_string()));
177        };
178        let version = Version::parse(version)?;
179
180        match operator {
181            Some(op)
182                if version.suffix.is_some()
183                    && op != Operator::Exact
184                    && op != Operator::Different =>
185            {
186                return Err(Error::NotAllowedOperatorWithSuffix(op));
187            }
188            _ => {}
189        }
190
191        Ok(Self { operator, version })
192    }
193
194    #[allow(clippy::manual_map)]
195    fn split_and_parse_operator(text: &str) -> Option<(Option<Operator>, &str)> {
196        if let Some(rest) = text.strip_prefix("<=") {
197            Some((Some(Operator::LessEq), rest.trim_start()))
198        } else if let Some(rest) = text.strip_prefix(">=") {
199            Some((Some(Operator::GreaterEq), rest.trim_start()))
200        } else if let Some(rest) = text.strip_prefix("!=") {
201            Some((Some(Operator::Different), rest.trim_start()))
202        } else if let Some(rest) = text.strip_prefix("~>") {
203            Some((Some(Operator::RightMost), rest.trim_start()))
204        } else if let Some(rest) = text.strip_prefix('<') {
205            Some((Some(Operator::Less), rest.trim_start()))
206        } else if let Some(rest) = text.strip_prefix('>') {
207            Some((Some(Operator::Greater), rest.trim_start()))
208        } else if let Some(rest) = text.strip_prefix('=') {
209            Some((Some(Operator::Exact), rest.trim_start()))
210        } else if let Some(first) = text.trim_start().chars().next() {
211            if first.is_ascii_digit() {
212                Some((None, text.trim_start()))
213            } else {
214                None
215            }
216        } else {
217            None
218        }
219    }
220}
221
222#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
223pub struct NumericIdentifiers(Vec<u32>);
224
225impl NumericIdentifiers {
226    pub fn new(vec: Vec<u32>) -> NumericIdentifiers {
227        NumericIdentifiers(vec)
228    }
229
230    pub fn parse(text: &str) -> Result<Self> {
231        let nums = text
232            .split('.')
233            .map(|ni| {
234                ni.parse::<u32>()
235                    .map_err(|err| Error::ImpossibleNumericIdentifierParsing {
236                        err,
237                        text: text.into(),
238                        ni: ni.into(),
239                    })
240            })
241            .collect::<Result<Vec<_>>>()?;
242
243        Ok(Self(nums))
244    }
245}
246
247impl fmt::Display for NumericIdentifiers {
248    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
249        self.0
250            .iter()
251            .map(|integer| integer.to_string())
252            .collect::<Vec<String>>()
253            .join(".")
254            .fmt(f)
255    }
256}
257
258#[derive(Copy, Clone, PartialEq, Eq, Debug)]
259pub enum Operator {
260    /// operator `=` Allows only one exact version number. Cannot be combined with other conditions.
261    Exact,
262    /// operator `!=` : Excludes an exact version number.
263    Different,
264    /// operator `>` : Comparisons against a specified version, allowing versions for which the comparison is true. "Greater" requests newer versions.
265    Greater,
266    /// operator `>=` : Comparisons against a specified version, allowing versions for which the comparison is true.
267    GreaterEq,
268    /// operator `<` : Comparisons against a specified version, allowing versions for which the comparison is true. "Less" requests older versions.
269    Less,
270    /// operator `<=` : Comparisons against a specified version, allowing versions for which the comparison is true.
271    LessEq,
272    /// operator `~>` : Allows only the rightmost version component to increment. For example, to allow new patch releases within a specific minor release, use the full version number: ~> 1.0.4 will allow installation of 1.0.5 and 1.0.10 but not 1.1.0.
273    RightMost,
274}
275
276impl fmt::Display for Operator {
277    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
278        fmt::Debug::fmt(self, f)
279    }
280}