1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use thiserror::Error;
6
7pub const PROTOCOL_V1_0: ProtocolVersion = ProtocolVersion::new(1, 0);
9
10pub const CURRENT_PROTOCOL_VERSION: ProtocolVersion = PROTOCOL_V1_0;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct ProtocolVersion {
16 major: u16,
17 minor: u16,
18}
19
20impl ProtocolVersion {
21 #[must_use]
23 pub const fn new(major: u16, minor: u16) -> Self {
24 Self { major, minor }
25 }
26
27 #[must_use]
29 pub const fn major(self) -> u16 {
30 self.major
31 }
32
33 #[must_use]
35 pub const fn minor(self) -> u16 {
36 self.minor
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
42pub enum ProtocolVersionParseError {
43 #[error("protocol version must use canonical major.minor decimal text")]
45 InvalidFormat,
46 #[error("protocol version component is out of range")]
48 ComponentOutOfRange,
49}
50
51impl fmt::Display for ProtocolVersion {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(formatter, "{}.{}", self.major, self.minor)
54 }
55}
56
57impl FromStr for ProtocolVersion {
58 type Err = ProtocolVersionParseError;
59
60 fn from_str(value: &str) -> Result<Self, Self::Err> {
61 let (major, minor) = value
62 .split_once('.')
63 .ok_or(ProtocolVersionParseError::InvalidFormat)?;
64 if major.is_empty()
65 || minor.is_empty()
66 || minor.contains('.')
67 || !canonical_decimal(major)
68 || !canonical_decimal(minor)
69 {
70 return Err(ProtocolVersionParseError::InvalidFormat);
71 }
72 Ok(Self::new(
73 major
74 .parse()
75 .map_err(|_| ProtocolVersionParseError::ComponentOutOfRange)?,
76 minor
77 .parse()
78 .map_err(|_| ProtocolVersionParseError::ComponentOutOfRange)?,
79 ))
80 }
81}
82
83impl Serialize for ProtocolVersion {
84 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
85 where
86 S: Serializer,
87 {
88 serializer.collect_str(self)
89 }
90}
91
92impl<'de> Deserialize<'de> for ProtocolVersion {
93 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
94 where
95 D: Deserializer<'de>,
96 {
97 let value = String::deserialize(deserializer)?;
98 value.parse().map_err(serde::de::Error::custom)
99 }
100}
101
102fn canonical_decimal(value: &str) -> bool {
103 value.bytes().all(|byte| byte.is_ascii_digit()) && (value == "0" || !value.starts_with('0'))
104}