xapi_rs/data/
version.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3use crate::data::{DataError, Validate, ValidationError};
4use core::fmt;
5use semver::{Version, VersionReq};
6use serde::{Deserialize, Deserializer, de};
7use serde_json::Value;
8use serde_with::SerializeDisplay;
9use std::str::FromStr;
10
11/// Type for serializing/deserializing xAPI Version strings w/ relaxed
12/// parsing rules to allow missing 'patch' or even 'minor' numbers.
13#[derive(Debug, PartialEq, SerializeDisplay)]
14pub struct MyVersion(Version);
15
16impl MyVersion {
17    /// Return this 'major' number.
18    pub fn major(&self) -> u64 {
19        self.0.major
20    }
21
22    /// Return this 'minor' number.
23    pub fn minor(&self) -> u64 {
24        self.0.minor
25    }
26
27    /// Return this 'patch' number.
28    pub fn patch(&self) -> u64 {
29        self.0.patch
30    }
31
32    // Check if version is in the 1.1.x range
33    fn is_excluded(&self) -> bool {
34        self.0.major == 1 && self.0.minor == 1
35    }
36}
37
38impl FromStr for MyVersion {
39    type Err = DataError;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        // ensure we have a semver string w/ 3 parts...
43        let parts: Vec<&str> = s.trim().split('.').collect();
44        let padded = match parts.len() {
45            1 => format!("{}.0.0", parts[0]),
46            2 => format!("{}.{}.0", parts[0], parts[1]),
47            _ => s.to_string(),
48        };
49        let sv = Version::parse(&padded)?;
50        Ok(MyVersion(sv))
51    }
52}
53
54impl From<f64> for MyVersion {
55    /// IMPORTANT (rsn) 20241030 - we limit the minor version number to be < 1000.
56    fn from(float_value: f64) -> Self {
57        let major = float_value.trunc() as u64;
58        let minor = (float_value.fract() * 1000.0).round() as u64;
59        MyVersion(Version::new(major, minor, 0))
60    }
61}
62
63impl<'de> Deserialize<'de> for MyVersion {
64    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
65    where
66        D: Deserializer<'de>,
67    {
68        let value: Value = Deserialize::deserialize(deserializer)?;
69        match value {
70            Value::String(s) => MyVersion::from_str(&s).map_err(de::Error::custom),
71            Value::Number(num) => {
72                if let Some(z_float) = num.as_f64() {
73                    Ok(MyVersion::from(z_float))
74                } else {
75                    Err(de::Error::custom("Invalid number format"))
76                }
77            }
78            _ => Err(de::Error::custom("Expected string | number")),
79        }
80    }
81}
82
83impl fmt::Display for MyVersion {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(f, "{}", self.0)
86    }
87}
88
89impl Validate for MyVersion {
90    fn validate(&self) -> Vec<super::ValidationError> {
91        let mut vec = vec![];
92
93        let range = VersionReq::parse(">=1.0.0, <=2.0.0").unwrap();
94        if range.matches(&self.0) && !self.is_excluded() {
95            // saul goodman
96        } else {
97            vec.push(ValidationError::ConstraintViolation(
98                format!("Version '{self}' is invalid or not allowed").into(),
99            ))
100        }
101
102        vec
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use tracing_test::traced_test;
110
111    #[test]
112    fn test_no_patch() {
113        let v = MyVersion::from_str("1.0").unwrap();
114        assert_eq!(v.major(), 1);
115        assert_eq!(v.minor(), 0);
116        assert_eq!(v.patch(), 0);
117
118        // should also work w/ serde...
119        let v: MyVersion = serde_json::from_str("1.0").unwrap();
120        assert_eq!(v.major(), 1);
121        assert_eq!(v.minor(), 0);
122        assert_eq!(v.patch(), 0);
123    }
124
125    #[traced_test]
126    #[test]
127    fn test_invalid() {
128        assert!(!MyVersion::from_str("0.9.9").unwrap().is_valid());
129        assert!(!MyVersion::from_str("2.0.1-beta").unwrap().is_valid());
130        assert!(!MyVersion::from_str("1.1.0").unwrap().is_valid());
131    }
132
133    #[traced_test]
134    #[test]
135    fn test_valid() {
136        assert!(MyVersion::from_str("1.0").unwrap().is_valid());
137        assert!(MyVersion::from_str("1.0.3").unwrap().is_valid());
138        assert!(MyVersion::from_str("2.0.0").unwrap().is_valid());
139    }
140
141    #[derive(Debug, serde::Deserialize, serde::Serialize)]
142    struct Foo {
143        ver: Option<MyVersion>,
144    }
145
146    #[traced_test]
147    #[test]
148    fn test_serde() {
149        const F1: &str = r#"{"ver":"1.0"}"#;
150        const F2: &str = r#"{"ver":"1.0.3"}"#;
151        const F3: &str = r#"{"ver":"2.0.0"}"#;
152
153        let f: Foo = serde_json::from_str(F1).unwrap();
154        assert_eq!(f.ver, Some(MyVersion(Version::new(1, 0, 0))));
155
156        let f: Foo = serde_json::from_str(F2).unwrap();
157        assert_eq!(f.ver, Some(MyVersion(Version::new(1, 0, 3))));
158
159        let f: Foo = serde_json::from_str(F3).unwrap();
160        assert_eq!(f.ver, Some(MyVersion(Version::new(2, 0, 0))));
161    }
162}