use std::fmt;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid version: {0}")]
pub struct ProtocolVersionParseError(pub String);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ProtocolVersion {
pub major: u64,
pub minor: u64,
pub patch: u64,
}
impl ProtocolVersion {
pub const fn new(major: u64, minor: u64, patch: u64) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn parse(s: &str) -> Result<Self, ProtocolVersionParseError> {
let parts: Vec<&str> = s.split('.').collect();
if parts.len() != 3 {
return Err(ProtocolVersionParseError(s.to_string()));
}
let parse_one = |p: &str| {
p.parse::<u64>()
.map_err(|_| ProtocolVersionParseError(s.to_string()))
};
Ok(Self {
major: parse_one(parts[0])?,
minor: parse_one(parts[1])?,
patch: parse_one(parts[2])?,
})
}
}
impl fmt::Display for ProtocolVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl FromStr for ProtocolVersion {
type Err = ProtocolVersionParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
pub fn is_compatible(client: &ProtocolVersion, server: &ProtocolVersion) -> bool {
if client.major != server.major {
return false;
}
if client.major == 0 {
return client.minor == server.minor;
}
client.minor <= server.minor
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProtocolVersionError {
pub client_version: Option<String>,
pub server_version: Option<String>,
pub min_supported: Option<String>,
pub max_supported: Option<String>,
pub message: String,
}
impl ProtocolVersionError {
pub fn new(message: impl Into<String>) -> Self {
Self {
client_version: None,
server_version: None,
min_supported: None,
max_supported: None,
message: message.into(),
}
}
}
impl fmt::Display for ProtocolVersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)?;
let mut diag = Vec::new();
if let Some(c) = &self.client_version {
diag.push(format!("client={c}"));
}
if let Some(s) = &self.server_version {
diag.push(format!("server={s}"));
}
if let Some(m) = &self.min_supported {
diag.push(format!("min={m}"));
}
if let Some(m) = &self.max_supported {
diag.push(format!("max={m}"));
}
if !diag.is_empty() {
write!(f, " ({})", diag.join(", "))?;
}
Ok(())
}
}
impl std::error::Error for ProtocolVersionError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid() {
assert_eq!(
ProtocolVersion::parse("0.2.0").unwrap(),
ProtocolVersion::new(0, 2, 0)
);
assert_eq!(
ProtocolVersion::parse("1.5.2").unwrap(),
ProtocolVersion::new(1, 5, 2)
);
assert_eq!(
ProtocolVersion::parse("10.20.30").unwrap(),
ProtocolVersion::new(10, 20, 30)
);
assert_eq!(
ProtocolVersion::parse("0.0.0").unwrap(),
ProtocolVersion::new(0, 0, 0)
);
assert_eq!(
"0.2.1".parse::<ProtocolVersion>().unwrap(),
ProtocolVersion::new(0, 2, 1)
);
}
#[test]
fn test_parse_invalid_strict_three_segments() {
for s in [
"", "0.2", "0", "0.2.0.1", "abc", "0.x.0", "0.2.", ".2.0", "0..0", "-1.0.0", "0.2.0 ",
" 0.2.0", "v0.2.0",
] {
assert!(ProtocolVersion::parse(s).is_err(), "{s:?} 应解析失败");
}
}
#[test]
fn test_display_roundtrip() {
for s in ["0.2.0", "1.5.2", "10.20.30", "0.0.0"] {
let v = ProtocolVersion::parse(s).unwrap();
assert_eq!(v.to_string(), s);
assert_eq!(ProtocolVersion::parse(&v.to_string()).unwrap(), v);
}
}
#[test]
fn test_is_compatible_v0_strict_minor() {
let v020 = ProtocolVersion::new(0, 2, 0);
assert!(is_compatible(&v020, &ProtocolVersion::new(0, 2, 1)));
assert!(is_compatible(&ProtocolVersion::new(0, 2, 99), &v020));
assert!(!is_compatible(&ProtocolVersion::new(0, 1, 0), &v020));
assert!(!is_compatible(&ProtocolVersion::new(0, 3, 0), &v020));
assert!(!is_compatible(&v020, &ProtocolVersion::new(0, 3, 0)));
}
#[test]
fn test_is_compatible_v1_backward() {
assert!(is_compatible(
&ProtocolVersion::new(1, 0, 0),
&ProtocolVersion::new(1, 2, 0)
));
assert!(is_compatible(
&ProtocolVersion::new(1, 2, 5),
&ProtocolVersion::new(1, 2, 0)
));
assert!(!is_compatible(
&ProtocolVersion::new(1, 2, 0),
&ProtocolVersion::new(1, 0, 0)
));
}
#[test]
fn test_is_compatible_major_mismatch() {
assert!(!is_compatible(
&ProtocolVersion::new(1, 0, 0),
&ProtocolVersion::new(2, 0, 0)
));
assert!(!is_compatible(
&ProtocolVersion::new(0, 2, 0),
&ProtocolVersion::new(1, 2, 0)
));
}
#[test]
fn test_ordering() {
assert!(ProtocolVersion::new(0, 2, 0) < ProtocolVersion::new(0, 2, 1));
assert!(ProtocolVersion::new(0, 2, 1) < ProtocolVersion::new(0, 3, 0));
assert!(ProtocolVersion::new(0, 3, 0) < ProtocolVersion::new(1, 0, 0));
}
#[test]
fn test_protocol_version_constant_parses() {
let v = ProtocolVersion::parse(crate::PROTOCOL_VERSION).unwrap();
assert_eq!(v, ProtocolVersion::new(0, 2, 0));
}
}