use std::cmp::Ordering;
use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NginxVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl NginxVersion {
pub const fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn parse(s: &str) -> Result<Self, NginxVersionParseError> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(NginxVersionParseError::InvalidFormat(s.to_string()));
}
let major = parts[0]
.parse::<u32>()
.map_err(|_| NginxVersionParseError::InvalidComponent(s.to_string()))?;
let minor = parts[1]
.parse::<u32>()
.map_err(|_| NginxVersionParseError::InvalidComponent(s.to_string()))?;
let patch = parts[2]
.parse::<u32>()
.map_err(|_| NginxVersionParseError::InvalidComponent(s.to_string()))?;
Ok(Self {
major,
minor,
patch,
})
}
}
impl Ord for NginxVersion {
fn cmp(&self, other: &Self) -> Ordering {
(self.major, self.minor, self.patch).cmp(&(other.major, other.minor, other.patch))
}
}
impl PartialOrd for NginxVersion {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl fmt::Display for NginxVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl FromStr for NginxVersion {
type Err = NginxVersionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
pub fn is_in_range(
version: &NginxVersion,
min: Option<&NginxVersion>,
max: Option<&NginxVersion>,
) -> bool {
if let (Some(min), Some(max)) = (min, max) {
debug_assert!(
min <= max,
"is_in_range called with reversed bounds: min={} > max={}",
min,
max
);
}
if let Some(min) = min
&& version < min
{
return false;
}
if let Some(max) = max
&& version > max
{
return false;
}
true
}
pub fn format_range(min: Option<&str>, max: Option<&str>) -> Option<String> {
match (min, max) {
(Some(min), Some(max)) => Some(format!("nginx >={}, <={}", min, max)),
(Some(min), None) => Some(format!("nginx >={}", min)),
(None, Some(max)) => Some(format!("nginx <={}", max)),
(None, None) => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NginxVersionParseError {
InvalidFormat(String),
InvalidComponent(String),
}
impl fmt::Display for NginxVersionParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidFormat(s) => write!(
f,
"invalid nginx version '{}': expected major.minor.patch format",
s
),
Self::InvalidComponent(s) => write!(
f,
"invalid nginx version '{}': components must be non-negative integers",
s
),
}
}
}
impl std::error::Error for NginxVersionParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_versions() {
assert_eq!(
NginxVersion::parse("1.30.0").unwrap(),
NginxVersion::new(1, 30, 0)
);
assert_eq!(
NginxVersion::parse("0.0.1").unwrap(),
NginxVersion::new(0, 0, 1)
);
assert_eq!(
NginxVersion::parse("10.20.30").unwrap(),
NginxVersion::new(10, 20, 30)
);
}
#[test]
fn parse_rejects_two_components() {
assert!(matches!(
NginxVersion::parse("1.30"),
Err(NginxVersionParseError::InvalidFormat(_))
));
}
#[test]
fn parse_rejects_four_components() {
assert!(matches!(
NginxVersion::parse("1.30.0.1"),
Err(NginxVersionParseError::InvalidFormat(_))
));
}
#[test]
fn parse_rejects_v_prefix() {
assert!(matches!(
NginxVersion::parse("v1.30.0"),
Err(NginxVersionParseError::InvalidComponent(_))
));
}
#[test]
fn parse_rejects_non_numeric_component() {
assert!(matches!(
NginxVersion::parse("1.30.x"),
Err(NginxVersionParseError::InvalidComponent(_))
));
assert!(matches!(
NginxVersion::parse("1.a.0"),
Err(NginxVersionParseError::InvalidComponent(_))
));
}
#[test]
fn parse_rejects_empty() {
assert!(NginxVersion::parse("").is_err());
}
#[test]
fn ordering() {
let v_1_30_0 = NginxVersion::new(1, 30, 0);
let v_1_30_1 = NginxVersion::new(1, 30, 1);
let v_1_31_0 = NginxVersion::new(1, 31, 0);
let v_2_0_0 = NginxVersion::new(2, 0, 0);
assert!(v_1_30_0 < v_1_30_1);
assert!(v_1_30_1 < v_1_31_0);
assert!(v_1_31_0 < v_2_0_0);
assert_eq!(v_1_30_0, NginxVersion::new(1, 30, 0));
}
#[test]
fn display() {
assert_eq!(NginxVersion::new(1, 30, 1).to_string(), "1.30.1");
}
#[test]
fn range_unbounded() {
let v = NginxVersion::new(1, 30, 0);
assert!(is_in_range(&v, None, None));
}
#[test]
fn range_min_only() {
let min = NginxVersion::new(1, 0, 0);
assert!(is_in_range(&NginxVersion::new(1, 0, 0), Some(&min), None));
assert!(is_in_range(&NginxVersion::new(2, 0, 0), Some(&min), None));
assert!(!is_in_range(&NginxVersion::new(0, 9, 0), Some(&min), None));
}
#[test]
fn range_max_only() {
let max = NginxVersion::new(1, 30, 0);
assert!(is_in_range(&NginxVersion::new(1, 30, 0), None, Some(&max)));
assert!(is_in_range(&NginxVersion::new(1, 0, 0), None, Some(&max)));
assert!(!is_in_range(&NginxVersion::new(1, 30, 1), None, Some(&max)));
assert!(!is_in_range(&NginxVersion::new(1, 31, 0), None, Some(&max)));
}
#[test]
fn range_both_bounds_inclusive() {
let min = NginxVersion::new(0, 6, 27);
let max = NginxVersion::new(1, 30, 0);
assert!(is_in_range(
&NginxVersion::new(0, 6, 27),
Some(&min),
Some(&max)
));
assert!(is_in_range(
&NginxVersion::new(1, 30, 0),
Some(&min),
Some(&max)
));
assert!(is_in_range(
&NginxVersion::new(1, 0, 0),
Some(&min),
Some(&max)
));
assert!(!is_in_range(
&NginxVersion::new(0, 6, 26),
Some(&min),
Some(&max)
));
assert!(!is_in_range(
&NginxVersion::new(1, 30, 1),
Some(&min),
Some(&max)
));
}
#[test]
fn from_str_works() {
let v: NginxVersion = "1.30.1".parse().unwrap();
assert_eq!(v, NginxVersion::new(1, 30, 1));
}
#[test]
#[should_panic(expected = "reversed bounds")]
fn range_panics_on_reversed_bounds_in_debug() {
let min = NginxVersion::new(2, 0, 0);
let max = NginxVersion::new(1, 0, 0);
let v = NginxVersion::new(1, 5, 0);
is_in_range(&v, Some(&min), Some(&max));
}
#[test]
fn range_equal_bounds_is_allowed() {
let exact = NginxVersion::new(1, 30, 0);
assert!(is_in_range(
&NginxVersion::new(1, 30, 0),
Some(&exact),
Some(&exact)
));
assert!(!is_in_range(
&NginxVersion::new(1, 30, 1),
Some(&exact),
Some(&exact)
));
}
#[test]
fn format_range_both_bounds() {
assert_eq!(
format_range(Some("0.6.27"), Some("1.30.0")),
Some("nginx >=0.6.27, <=1.30.0".to_string())
);
}
#[test]
fn format_range_min_only() {
assert_eq!(
format_range(Some("1.0.0"), None),
Some("nginx >=1.0.0".to_string())
);
}
#[test]
fn format_range_max_only() {
assert_eq!(
format_range(None, Some("1.29.6")),
Some("nginx <=1.29.6".to_string())
);
}
#[test]
fn format_range_none() {
assert_eq!(format_range(None, None), None);
}
}