1use serde::{Deserialize, Serialize};
28
29use crate::wire_schema::{DescribeWire, WireSchema};
30
31#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
40pub struct FrameworkVersion {
41 major: u16,
42 minor: u16,
43 patch: u16,
44}
45
46impl FrameworkVersion {
47 pub const CURRENT_SPELLING: &'static str = env!("CARGO_PKG_VERSION");
53
54 pub const CURRENT: Self = match Self::parse(Self::CURRENT_SPELLING.as_bytes()) {
60 Some(version) => version,
61 None => panic!("the crate version is not a canonical <major>.<minor>.<patch> version"),
62 };
63
64 #[must_use]
66 pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
67 Self {
68 major,
69 minor,
70 patch,
71 }
72 }
73
74 #[must_use]
76 pub const fn major(self) -> u16 {
77 self.major
78 }
79
80 #[must_use]
82 pub const fn minor(self) -> u16 {
83 self.minor
84 }
85
86 #[must_use]
88 pub const fn patch(self) -> u16 {
89 self.patch
90 }
91
92 #[must_use]
99 pub const fn compatibility_line(self) -> CompatibilityLine {
100 if self.major == 0 {
101 CompatibilityLine::PreV1 { minor: self.minor }
102 } else {
103 CompatibilityLine::Stable { major: self.major }
104 }
105 }
106
107 #[must_use]
122 pub const fn is_compatible_with(self, other: Self) -> bool {
123 match (self.compatibility_line(), other.compatibility_line()) {
126 (
127 CompatibilityLine::PreV1 { minor },
128 CompatibilityLine::PreV1 { minor: other_minor },
129 ) => minor == other_minor,
130 (
131 CompatibilityLine::Stable { major },
132 CompatibilityLine::Stable { major: other_major },
133 ) => major == other_major,
134 _ => false,
135 }
136 }
137
138 const fn parse(bytes: &[u8]) -> Option<Self> {
145 let (major, index) = match Self::parse_segment(bytes, 0) {
146 Some(parsed) => parsed,
147 None => return None,
148 };
149 if index >= bytes.len() || bytes[index] != b'.' {
150 return None;
151 }
152 let (minor, index) = match Self::parse_segment(bytes, index + 1) {
153 Some(parsed) => parsed,
154 None => return None,
155 };
156 if index >= bytes.len() || bytes[index] != b'.' {
157 return None;
158 }
159 let (patch, index) = match Self::parse_segment(bytes, index + 1) {
160 Some(parsed) => parsed,
161 None => return None,
162 };
163 if index != bytes.len() {
164 return None;
165 }
166 Some(Self::new(major, minor, patch))
167 }
168
169 const fn parse_segment(bytes: &[u8], start: usize) -> Option<(u16, usize)> {
174 let mut index = start;
175 let mut value: u16 = 0;
176 while index < bytes.len() && bytes[index].is_ascii_digit() {
177 let digit = (bytes[index] - b'0') as u16;
178 value = match value.checked_mul(10) {
179 Some(scaled) => scaled,
180 None => return None,
181 };
182 value = match value.checked_add(digit) {
183 Some(added) => added,
184 None => return None,
185 };
186 index += 1;
187 }
188 if index == start {
189 return None;
190 }
191 if bytes[start] == b'0' && index - start > 1 {
192 return None;
193 }
194 Some((value, index))
195 }
196}
197
198#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
205pub enum CompatibilityLine {
206 PreV1 { minor: u16 },
208 Stable { major: u16 },
210}
211
212impl std::fmt::Display for CompatibilityLine {
213 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 match self {
218 Self::PreV1 { minor } => write!(formatter, "0.{minor}.x"),
219 Self::Stable { major } => write!(formatter, "{major}.x"),
220 }
221 }
222}
223
224impl std::fmt::Display for FrameworkVersion {
225 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
227 }
228}
229
230impl std::str::FromStr for FrameworkVersion {
231 type Err = FrameworkVersionError;
232
233 fn from_str(value: &str) -> Result<Self, Self::Err> {
234 Self::parse(value.as_bytes()).ok_or_else(|| FrameworkVersionError {
235 value: value.to_owned(),
236 })
237 }
238}
239
240#[derive(Clone, Debug, thiserror::Error)]
243#[error("invalid framework version '{value}'; expected <major>.<minor>.<patch>")]
244pub struct FrameworkVersionError {
245 value: String,
246}
247
248impl Serialize for FrameworkVersion {
249 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
250 serializer.collect_str(self)
251 }
252}
253
254impl<'de> Deserialize<'de> for FrameworkVersion {
255 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
256 let value = String::deserialize(deserializer)?;
257 Self::parse(value.as_bytes())
258 .ok_or_else(|| serde::de::Error::custom(FrameworkVersionError { value }))
259 }
260}
261
262impl DescribeWire for FrameworkVersion {
263 fn wire_schema() -> WireSchema {
267 WireSchema::opaque("FrameworkVersion", WireSchema::String)
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn the_wire_spelling_is_the_semver_string_and_round_trips() {
277 let version = FrameworkVersion::new(0, 57, 2);
278 assert_eq!(version.to_string(), "0.57.2");
279 assert_eq!(
280 serde_json::to_string(&version).expect("a framework version serializes"),
281 "\"0.57.2\""
282 );
283 assert_eq!(
284 serde_json::from_str::<FrameworkVersion>("\"0.57.2\"").expect("the spelling parses"),
285 version
286 );
287 assert_eq!(
288 "0.57.2"
289 .parse::<FrameworkVersion>()
290 .expect("the spelling parses"),
291 version
292 );
293 assert_eq!(
294 (version.major(), version.minor(), version.patch()),
295 (0, 57, 2)
296 );
297 }
298
299 #[test]
303 fn every_non_canonical_spelling_is_rejected() {
304 for value in [
305 "\"v0.57.2\"",
306 "\"0.57\"",
307 "\"0.57.2.1\"",
308 "\"0.57.2-rc.1\"",
309 "\"0.57.2+build.5\"",
310 "\"0.057.2\"",
311 "\"00.57.2\"",
312 "\"0.57.2 \"",
313 "\" 0.57.2\"",
314 "\"\"",
315 r#"{"major":0,"minor":57,"patch":2}"#,
316 ] {
317 assert!(
318 serde_json::from_str::<FrameworkVersion>(value).is_err(),
319 "{value} must not parse as a framework version"
320 );
321 }
322 assert!("0.57.2-rc.1".parse::<FrameworkVersion>().is_err());
323 assert!("65536.0.0".parse::<FrameworkVersion>().is_err());
324 }
325
326 #[test]
330 fn the_declared_wire_shape_is_the_shape_the_serializer_writes() {
331 let value = serde_json::to_value(FrameworkVersion::new(0, 57, 2))
332 .expect("a framework version serializes");
333 assert_eq!(FrameworkVersion::wire_schema().conforms(&value), Ok(()));
334 assert_eq!(
335 FrameworkVersion::wire_schema().canonical_json(),
336 r#"{"kind":"opaque","name":"FrameworkVersion","wire":{"kind":"string"}}"#
337 );
338 }
339
340 #[test]
344 fn current_is_the_crate_version() {
345 assert_eq!(
346 FrameworkVersion::CURRENT.to_string(),
347 env!("CARGO_PKG_VERSION")
348 );
349 assert_eq!(
350 env!("CARGO_PKG_VERSION")
351 .parse::<FrameworkVersion>()
352 .expect("the crate version is a canonical framework version"),
353 FrameworkVersion::CURRENT
354 );
355 assert_eq!(
356 FrameworkVersion::CURRENT_SPELLING,
357 FrameworkVersion::CURRENT.to_string()
358 );
359 }
360
361 #[test]
362 fn the_compatibility_line_is_the_minor_before_v1_and_the_major_after() {
363 assert_eq!(
364 FrameworkVersion::new(0, 57, 2).compatibility_line(),
365 CompatibilityLine::PreV1 { minor: 57 }
366 );
367 assert_eq!(
368 FrameworkVersion::new(0, 58, 0).compatibility_line(),
369 CompatibilityLine::PreV1 { minor: 58 }
370 );
371 assert_eq!(
372 FrameworkVersion::new(1, 4, 9).compatibility_line(),
373 CompatibilityLine::Stable { major: 1 }
374 );
375 assert_eq!(
376 FrameworkVersion::new(2, 0, 0).compatibility_line(),
377 CompatibilityLine::Stable { major: 2 }
378 );
379 }
380
381 #[test]
383 fn a_line_is_spelled_as_the_release_it_names() {
384 assert_eq!(
385 FrameworkVersion::new(0, 58, 2)
386 .compatibility_line()
387 .to_string(),
388 "0.58.x"
389 );
390 assert_eq!(
391 FrameworkVersion::new(1, 4, 9)
392 .compatibility_line()
393 .to_string(),
394 "1.x"
395 );
396 }
397
398 #[test]
402 fn versions_on_the_same_line_are_not_equal_but_are_compatible() {
403 let earlier = FrameworkVersion::new(0, 57, 0);
404 let later = FrameworkVersion::new(0, 57, 1);
405 assert_eq!(earlier.compatibility_line(), later.compatibility_line());
406 assert_ne!(earlier, later);
407 assert!(earlier.is_compatible_with(later));
408 assert!(later.is_compatible_with(earlier));
409 }
410
411 #[test]
414 fn compatibility_is_the_line_and_the_line_is_the_break() {
415 let pre_v1 = FrameworkVersion::new(0, 58, 0);
416 assert!(pre_v1.is_compatible_with(FrameworkVersion::new(0, 58, 7)));
417 assert!(!pre_v1.is_compatible_with(FrameworkVersion::new(0, 59, 0)));
418 assert!(!pre_v1.is_compatible_with(FrameworkVersion::new(0, 57, 9)));
419
420 let stable = FrameworkVersion::new(1, 4, 2);
421 assert!(stable.is_compatible_with(FrameworkVersion::new(1, 9, 0)));
422 assert!(!stable.is_compatible_with(FrameworkVersion::new(2, 0, 0)));
423 assert!(!stable.is_compatible_with(pre_v1));
424 assert!(!pre_v1.is_compatible_with(stable));
425
426 assert!(FrameworkVersion::CURRENT.is_compatible_with(FrameworkVersion::CURRENT));
427 }
428}