2022-02-22 12:03:40 +03:00
|
|
|
//! Module for browser versions
|
|
|
|
|
2022-01-14 01:20:45 +03:00
|
|
|
use std::{cmp, cmp::Ordering, fmt, str::FromStr};
|
2019-12-28 05:25:54 +03:00
|
|
|
|
2022-02-09 09:33:32 +03:00
|
|
|
use serde::{de, de::Visitor, Deserialize, Deserializer};
|
|
|
|
|
|
|
|
use crate::Versions;
|
|
|
|
|
2022-02-22 12:03:40 +03:00
|
|
|
/// A version of a browser.
|
|
|
|
///
|
|
|
|
/// This is similar to semver, but this assumes a production build. (No tag like
|
|
|
|
/// `alpha`)
|
2022-01-13 17:06:11 +03:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
2019-12-28 05:25:54 +03:00
|
|
|
pub struct Version {
|
|
|
|
pub major: u16,
|
|
|
|
pub minor: u16,
|
|
|
|
pub patch: u16,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Version {
|
|
|
|
type Err = ();
|
|
|
|
|
|
|
|
fn from_str(v: &str) -> Result<Self, Self::Err> {
|
2022-01-13 17:06:11 +03:00
|
|
|
if !v.contains('.') {
|
2019-12-28 05:25:54 +03:00
|
|
|
return Ok(Version {
|
|
|
|
major: v
|
|
|
|
.parse()
|
|
|
|
.unwrap_or_else(|err| panic!("failed to parse `{}` as a version: {}", v, err)),
|
|
|
|
minor: 0,
|
|
|
|
patch: 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-01-13 17:06:11 +03:00
|
|
|
if v.split('.').count() == 2 {
|
|
|
|
let mut s = v.split('.');
|
2019-12-28 05:25:54 +03:00
|
|
|
return Ok(Version {
|
|
|
|
major: s.next().unwrap().parse().unwrap(),
|
|
|
|
minor: s.next().unwrap().parse().unwrap(),
|
|
|
|
patch: 0,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
let v = v
|
|
|
|
.parse::<semver::Version>()
|
|
|
|
.unwrap_or_else(|err| panic!("failed to parse {} as semver: {}", v, err));
|
|
|
|
|
|
|
|
Ok(Version {
|
|
|
|
major: v.major as _,
|
|
|
|
minor: v.minor as _,
|
|
|
|
patch: v.patch as _,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl cmp::PartialOrd for Version {
|
|
|
|
fn partial_cmp(&self, other: &Version) -> Option<Ordering> {
|
|
|
|
Some(self.cmp(other))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl cmp::Ord for Version {
|
|
|
|
fn cmp(&self, other: &Version) -> Ordering {
|
|
|
|
match self.major.cmp(&other.major) {
|
|
|
|
Ordering::Equal => {}
|
|
|
|
r => return r,
|
|
|
|
}
|
|
|
|
|
|
|
|
match self.minor.cmp(&other.minor) {
|
|
|
|
Ordering::Equal => {}
|
|
|
|
r => return r,
|
|
|
|
}
|
|
|
|
|
|
|
|
match self.patch.cmp(&other.patch) {
|
|
|
|
Ordering::Equal => {}
|
|
|
|
r => return r,
|
|
|
|
}
|
|
|
|
|
|
|
|
Ordering::Equal
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct SerdeVisitor;
|
|
|
|
|
|
|
|
impl<'de> Visitor<'de> for SerdeVisitor {
|
|
|
|
type Value = Version;
|
|
|
|
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
formatter.write_str("a browser version")
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
Ok(Version {
|
|
|
|
major: v as _,
|
|
|
|
minor: 0,
|
|
|
|
patch: 0,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
Ok(Version {
|
|
|
|
major: v as _,
|
|
|
|
minor: 0,
|
|
|
|
patch: 0,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
Ok(Version {
|
|
|
|
major: v.floor() as _,
|
|
|
|
minor: v.fract() as _,
|
|
|
|
patch: 0,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
v.parse()
|
|
|
|
.map_err(|_| de::Error::invalid_type(de::Unexpected::Str(v), &self))
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
self.visit_str(v)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
|
|
|
where
|
|
|
|
E: de::Error,
|
|
|
|
{
|
|
|
|
self.visit_str(&v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<'de> Deserialize<'de> for Version {
|
|
|
|
fn deserialize<D>(deserializer: D) -> Result<Version, D::Error>
|
|
|
|
where
|
|
|
|
D: Deserializer<'de>,
|
|
|
|
{
|
|
|
|
deserializer.deserialize_any(SerdeVisitor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn should_enable(target: Versions, feature: Versions, default: bool) -> bool {
|
|
|
|
if target
|
|
|
|
.iter()
|
|
|
|
.zip(feature.iter())
|
|
|
|
.all(|((_, target_version), (_, f))| target_version.is_none() && f.is_none())
|
|
|
|
{
|
|
|
|
return default;
|
|
|
|
}
|
|
|
|
|
2021-04-27 06:46:04 +03:00
|
|
|
target.iter().zip(feature.iter()).any(
|
|
|
|
|((target_name, maybe_target_version), (_, maybe_feature_version))| {
|
|
|
|
maybe_target_version.map_or(false, |target_version| {
|
|
|
|
let feature_or_fallback_version =
|
|
|
|
maybe_feature_version.or_else(|| match target_name {
|
|
|
|
// Fall back to Chrome versions if Android browser data
|
|
|
|
// is missing from the feature data. It appears the
|
|
|
|
// Android browser has aligned its versioning with Chrome.
|
|
|
|
"android" => feature.chrome,
|
|
|
|
_ => None,
|
|
|
|
});
|
|
|
|
|
|
|
|
feature_or_fallback_version.map_or(true, |v| v > target_version)
|
|
|
|
})
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
2019-12-28 05:25:54 +03:00
|
|
|
|
2021-04-27 06:46:04 +03:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
use crate::BrowserData;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn should_enable_android_falls_back_to_chrome() {
|
|
|
|
assert!(!should_enable(
|
|
|
|
BrowserData {
|
|
|
|
android: Some("51.0.0".parse().unwrap()),
|
|
|
|
..Default::default()
|
|
|
|
},
|
|
|
|
BrowserData {
|
|
|
|
chrome: Some("51.0.0".parse().unwrap()),
|
|
|
|
..Default::default()
|
|
|
|
},
|
|
|
|
false
|
|
|
|
));
|
|
|
|
}
|
2019-12-28 05:25:54 +03:00
|
|
|
}
|