preset_env_base/
version.rs

1//! Module for browser versions
2
3use std::{cmp, cmp::Ordering, fmt, str::FromStr};
4
5use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize};
6use tracing::warn;
7
8use crate::Versions;
9
10/// A version of a browser.
11///
12/// This is similar to semver, but this assumes a production build. (No tag like
13/// `alpha`)
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
15pub struct Version {
16    /// `a` in `a.b.c`
17    pub major: u32,
18    /// `b` in `a.b.c`
19    pub minor: u32,
20    /// `c` in `a.b.c`
21    pub patch: u32,
22}
23
24impl FromStr for Version {
25    type Err = ();
26
27    fn from_str(v: &str) -> Result<Self, Self::Err> {
28        if !v.contains('.') {
29            return Ok(Version {
30                major: v.parse().map_err(|err| {
31                    warn!("failed to parse `{}` as a version: {}", v, err);
32                })?,
33                minor: 0,
34                patch: 0,
35            });
36        }
37
38        if v.split('.').count() == 2 {
39            let mut s = v.split('.');
40            return Ok(Version {
41                major: s.next().unwrap().parse().unwrap(),
42                minor: s.next().unwrap().parse().unwrap(),
43                patch: 0,
44            });
45        }
46
47        let v = v.parse::<semver::Version>().map_err(|err| {
48            warn!("failed to parse `{}` as a version: {}", v, err);
49        })?;
50
51        Ok(Version {
52            major: v.major as _,
53            minor: v.minor as _,
54            patch: v.patch as _,
55        })
56    }
57}
58
59impl cmp::PartialOrd for Version {
60    fn partial_cmp(&self, other: &Version) -> Option<Ordering> {
61        Some(self.cmp(other))
62    }
63}
64
65impl cmp::Ord for Version {
66    fn cmp(&self, other: &Version) -> Ordering {
67        match self.major.cmp(&other.major) {
68            Ordering::Equal => {}
69            r => return r,
70        }
71
72        match self.minor.cmp(&other.minor) {
73            Ordering::Equal => {}
74            r => return r,
75        }
76
77        match self.patch.cmp(&other.patch) {
78            Ordering::Equal => {}
79            r => return r,
80        }
81
82        Ordering::Equal
83    }
84}
85
86struct SerdeVisitor;
87
88impl<'de> Visitor<'de> for SerdeVisitor {
89    type Value = Version;
90
91    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
92        formatter.write_str("a browser version")
93    }
94
95    fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
96    where
97        E: de::Error,
98    {
99        Ok(Version {
100            major: v as _,
101            minor: 0,
102            patch: 0,
103        })
104    }
105
106    fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
107    where
108        E: de::Error,
109    {
110        Ok(Version {
111            major: v as _,
112            minor: 0,
113            patch: 0,
114        })
115    }
116
117    fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
118    where
119        E: de::Error,
120    {
121        Ok(Version {
122            major: v.floor() as _,
123            minor: v.fract() as _,
124            patch: 0,
125        })
126    }
127
128    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
129    where
130        E: de::Error,
131    {
132        v.parse()
133            .map_err(|_| de::Error::invalid_type(de::Unexpected::Str(v), &self))
134    }
135
136    #[inline]
137    fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
138    where
139        E: de::Error,
140    {
141        self.visit_str(v)
142    }
143
144    fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
145    where
146        E: de::Error,
147    {
148        self.visit_str(&v)
149    }
150}
151
152impl<'de> Deserialize<'de> for Version {
153    fn deserialize<D>(deserializer: D) -> Result<Version, D::Error>
154    where
155        D: Deserializer<'de>,
156    {
157        deserializer.deserialize_any(SerdeVisitor)
158    }
159}
160
161pub fn should_enable(target: &Versions, feature: &Versions, default: bool) -> bool {
162    if target
163        .iter()
164        .zip(feature.iter())
165        .all(|((_, target_version), (_, f))| target_version.is_none() && f.is_none())
166    {
167        return default;
168    }
169
170    target.iter().zip(feature.iter()).any(
171        |((target_name, maybe_target_version), (_, maybe_feature_version))| {
172            maybe_target_version.map_or(false, |target_version| {
173                let feature_or_fallback_version =
174                    maybe_feature_version.or_else(|| match target_name {
175                        // Fall back to Chrome versions if Android browser data
176                        // is missing from the feature data. It appears the
177                        // Android browser has aligned its versioning with Chrome.
178                        "android" => feature.chrome,
179                        _ => None,
180                    });
181
182                feature_or_fallback_version.map_or(true, |v| v > target_version)
183            })
184        },
185    )
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::BrowserData;
192
193    #[test]
194    fn should_enable_android_falls_back_to_chrome() {
195        assert!(!should_enable(
196            &BrowserData {
197                android: Some("51.0.0".parse().unwrap()),
198                ..Default::default()
199            },
200            &BrowserData {
201                chrome: Some("51.0.0".parse().unwrap()),
202                ..Default::default()
203            },
204            false
205        ));
206    }
207}