protocol_droid/domain/acp/
version.rs1use core::fmt;
34use core::str::FromStr;
35
36use serde::{Deserialize, Deserializer, Serialize, Serializer};
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum AcpVersion {
48 V1_25_0,
52 Unknown {
57 major: u16,
61 minor: u16,
63 patch: u16,
65 },
66}
67
68const fn parts(version: AcpVersion) -> (u16, u16, u16) {
69 match version {
70 AcpVersion::V1_25_0 => (1, 25, 0),
71 AcpVersion::Unknown {
72 major,
73 minor,
74 patch,
75 } => (major, minor, patch),
76 }
77}
78
79impl PartialOrd for AcpVersion {
80 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
81 Some(self.cmp(other))
82 }
83}
84
85impl Ord for AcpVersion {
86 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
87 parts(*self).cmp(&parts(*other))
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct AcpVersionParseError {
95 input: String,
96 reason: &'static str,
97}
98
99impl fmt::Display for AcpVersionParseError {
100 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
101 write!(
102 formatter,
103 "ACP version {:?} is invalid: {}",
104 self.input, self.reason
105 )
106 }
107}
108
109impl std::error::Error for AcpVersionParseError {}
110
111impl FromStr for AcpVersion {
112 type Err = AcpVersionParseError;
113
114 fn from_str(input: &str) -> Result<Self, Self::Err> {
115 let version_part = input.strip_prefix("schema-v").unwrap_or(input);
120 let mut iter = version_part.split('.');
121 let Some(major) = iter.next() else {
122 return Err(AcpVersionParseError {
123 input: input.to_owned(),
124 reason: "missing the major version",
125 });
126 };
127 let Some(minor) = iter.next() else {
128 return Err(AcpVersionParseError {
129 input: input.to_owned(),
130 reason: "missing the minor version",
131 });
132 };
133 let Some(patch) = iter.next() else {
134 return Err(AcpVersionParseError {
135 input: input.to_owned(),
136 reason: "missing the patch version",
137 });
138 };
139 if iter.next().is_some() {
140 return Err(AcpVersionParseError {
141 input: input.to_owned(),
142 reason: "expected exactly three numeric segments",
143 });
144 }
145 let major = major.parse::<u16>().map_err(|_| AcpVersionParseError {
146 input: input.to_owned(),
147 reason: "the major version is not a u16",
148 })?;
149 let minor = minor.parse::<u16>().map_err(|_| AcpVersionParseError {
150 input: input.to_owned(),
151 reason: "the minor version is not a u16",
152 })?;
153 let patch = patch.parse::<u16>().map_err(|_| AcpVersionParseError {
154 input: input.to_owned(),
155 reason: "the patch version is not a u16",
156 })?;
157 if (1, 25, 0) == (major, minor, patch) {
158 Ok(Self::V1_25_0)
159 } else {
160 Ok(Self::Unknown {
161 major,
162 minor,
163 patch,
164 })
165 }
166 }
167}
168
169impl fmt::Display for AcpVersion {
170 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171 let (major, minor, patch) = parts(*self);
172 write!(formatter, "{major}.{minor}.{patch}")
173 }
174}
175
176impl Serialize for AcpVersion {
177 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
178 where
179 S: Serializer,
180 {
181 serializer.collect_str(self)
182 }
183}
184
185impl<'de> Deserialize<'de> for AcpVersion {
186 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
187 where
188 D: Deserializer<'de>,
189 {
190 let raw = String::deserialize(deserializer)?;
191 raw.parse().map_err(serde::de::Error::custom)
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use core::cmp::Ordering;
199
200 #[test]
204 fn the_pinned_version_parses_with_or_without_the_prefix() -> Result<(), AcpVersionParseError> {
205 let with_prefix = "schema-v1.25.0".parse::<AcpVersion>()?;
206 let without_prefix = "1.25.0".parse::<AcpVersion>()?;
207 assert_eq!(with_prefix, AcpVersion::V1_25_0);
208 assert_eq!(without_prefix, AcpVersion::V1_25_0);
209 Ok(())
210 }
211
212 #[test]
217 fn an_unknown_version_orders_like_its_known_equivalent() -> Result<(), AcpVersionParseError> {
218 let unknown: AcpVersion = "1.25.0".parse()?;
219 assert_eq!(unknown, AcpVersion::V1_25_0);
220 assert_eq!(unknown.cmp(&AcpVersion::V1_25_0), Ordering::Equal);
221 let future: AcpVersion = "1.26.0".parse()?;
222 assert!(future > AcpVersion::V1_25_0);
223 let older: AcpVersion = "0.9.0".parse()?;
224 assert!(older < AcpVersion::V1_25_0);
225 Ok(())
226 }
227
228 #[test]
234 fn a_malformed_input_carries_the_input_and_the_reason() -> Result<(), String> {
235 let cases: [(&str, &str); 5] = [
243 ("", "missing the minor version"),
244 ("1", "missing the minor version"),
245 ("1.2", "missing the patch version"),
246 ("1.2.3.4", "expected exactly three numeric segments"),
247 ("a.b.c", "the major version is not a u16"),
248 ];
249 for (input, expected_reason) in cases {
250 let Err(error) = input.parse::<AcpVersion>() else {
251 return Err(format!("{input:?} should be an error"));
252 };
253 assert_eq!(error.reason, expected_reason, "input: {input:?}");
254 let text = format!("{error}");
255 assert!(
256 text.contains(input),
257 "the text should carry the input: {text}"
258 );
259 }
260 Ok(())
261 }
262
263 #[test]
270 fn display_then_from_str_round_trips_every_known_variant() -> Result<(), AcpVersionParseError> {
271 let known: AcpVersion = AcpVersion::V1_25_0;
272 let round_tripped: AcpVersion = format!("{known}").parse()?;
273 assert_eq!(round_tripped, known);
274 let future: AcpVersion = "1.27.3".parse()?;
275 let round_tripped: AcpVersion = format!("{future}").parse()?;
276 assert_eq!(round_tripped, future);
277 Ok(())
278 }
279}