Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 97 additions & 117 deletions crates/rari-data/src/baseline.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::fmt;
use std::marker::PhantomData;
use std::path::Path;

use indexmap::IndexMap;
use rari_utils::concat_strs;
use rari_utils::io::read_to_string;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde::de::{self, value, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use url::Url;
Expand All @@ -26,7 +23,7 @@ pub struct Baseline<'a> {

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct WebFeatures {
pub features: IndexMap<String, FeatureData>,
pub features: IndexMap<String, FeatureEnum>,
pub bcd_keys: Vec<KeyStatus>,
}

Expand All @@ -51,11 +48,11 @@ impl WebFeatures {
pub fn from_file(path: &Path) -> Result<Self, Error> {
let json_str = read_to_string(path)?;
let dirty_map: DirtyWebFeatures = serde_json::from_str(&json_str)?;
let features: IndexMap<String, FeatureData> = dirty_map
let features: IndexMap<String, FeatureEnum> = dirty_map
.features
.into_iter()
.filter_map(|(k, v)| {
serde_json::from_value::<FeatureData>(v)
serde_json::from_value::<FeatureEnum>(v)
.inspect_err(|e| {
tracing::error!("Error serializing baseline for {}: {}", k, &e)
})
Expand All @@ -80,13 +77,17 @@ impl WebFeatures {
// `http headers Content-Security-Policy`
let mut bcd_keys: Vec<KeyStatus> = features
.iter()
.flat_map(|(feature, fd)| {
fd.compat_features.iter().map(|bcd_key| KeyStatus {
bcd_key: bcd_key.clone(),
bcd_key_spaced: spaced(bcd_key),
feature: feature.clone(),
})
.filter_map(|(feature, fe)| match fe {
FeatureEnum::Feature(fd) => {
Some(fd.compat_features.iter().map(|bcd_key| KeyStatus {
bcd_key: bcd_key.clone(),
bcd_key_spaced: spaced(bcd_key),
feature: feature.clone(),
}))
}
_ => None,
})
.flat_map(|x| x.into_iter())
.collect();
bcd_keys.sort_by(|a, b| a.bcd_key_spaced.cmp(&b.bcd_key_spaced));
bcd_keys.dedup_by(|a, b| a.bcd_key_spaced == b.bcd_key_spaced);
Expand Down Expand Up @@ -118,57 +119,55 @@ impl WebFeatures {
pub fn baseline_by_bcd_key(&self, bcd_key: &str) -> Option<Baseline<'_>> {
let bcd_key_spaced = &spaced(bcd_key);
if let Some(feature) = self.feature_data_by_key(bcd_key_spaced) {
if let Some(status) = feature.status.as_ref() {
if let Some(status_for_key) = status
.by_compat_key
.as_ref()
.and_then(|by_key| by_key.get(bcd_key))
{
let sub_keys = self.sub_keys(bcd_key_spaced);
let sub_status = sub_keys
.iter()
.map(|sub_key| {
self.feature_data_by_name(&sub_key.feature)
.and_then(|feature| feature.status.as_ref())
.and_then(|status| status.by_compat_key.as_ref())
.and_then(|by_key| by_key.get(&sub_key.bcd_key))
.and_then(|status_for_key| status_for_key.baseline)
})
.collect::<Vec<_>>();
if let Some(status_for_key) = feature
.status
.by_compat_key
.as_ref()
.and_then(|by_key| by_key.get(bcd_key))
{
let sub_keys = self.sub_keys(bcd_key_spaced);
let sub_status = sub_keys
.iter()
.map(|sub_key| {
self.feature_data_by_name(&sub_key.feature)
.and_then(|feature| feature.status.by_compat_key.as_ref())
.and_then(|by_key| by_key.get(&sub_key.bcd_key))
.map(|status_for_key| status_for_key.baseline)
})
.collect::<Vec<_>>();

let asterisk = if sub_status
.iter()
.all(|baseline| baseline == &status_for_key.baseline)
{
false
} else {
match status_for_key.baseline {
Some(BaselineHighLow::False) => {
let Support {
chrome,
chrome_android,
firefox,
firefox_android,
safari,
safari_ios,
..
} = &status_for_key.support;
!(chrome == chrome_android
&& firefox == firefox_android
&& safari == safari_ios)
}
Some(BaselineHighLow::Low) => !sub_status.iter().all(|ss| {
matches!(ss, Some(BaselineHighLow::Low | BaselineHighLow::High))
}),
_ => true,
let asterisk = if sub_status
.iter()
.all(|baseline| baseline == &Some(status_for_key.baseline))
{
false
} else {
match status_for_key.baseline {
BaselineHighLow::False => {
let Support {
chrome,
chrome_android,
firefox,
firefox_android,
safari,
safari_ios,
..
} = &status_for_key.support;
!(chrome == chrome_android
&& firefox == firefox_android
&& safari == safari_ios)
}
};
return Some(Baseline {
support: status_for_key,
asterisk,
feature,
});
}
BaselineHighLow::Low => !sub_status.iter().all(|ss| {
matches!(ss, Some(BaselineHighLow::Low | BaselineHighLow::High))
}),
_ => true,
}
};
return Some(Baseline {
support: status_for_key,
asterisk,
feature,
});
}
}
None
Expand All @@ -186,42 +185,62 @@ impl WebFeatures {
}

fn feature_data_by_name(&self, feature_name: &str) -> Option<&FeatureData> {
if let Some(feature_data) = self.features.get(feature_name) {
if feature_data.discouraged.is_some() {
return None;
if let Some(feature_enum) = self.features.get(feature_name) {
match feature_enum {
FeatureEnum::Feature(feature_data) => {
if feature_data.discouraged.is_some() {
return None;
}
return Some(feature_data);
}
_ => {
return None;
}
}
return Some(feature_data);
}
None
}
}

#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum FeatureEnum {
Feature(Box<FeatureData>),
Moved(FeatureMovedData),
Split(FeatureSplitData),
}

#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
pub struct FeatureMovedData {
pub redirect_target: String,
}

#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
pub struct FeatureSplitData {
pub redirect_targets: Vec<String>,
}

#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
pub struct FeatureData {
/** Specification */
#[serde(deserialize_with = "t_or_vec", default, skip_serializing)]
#[serde(default, skip_serializing)]
pub spec: Vec<Url>,
/** caniuse.com identifier */
#[serde(deserialize_with = "t_or_vec", default, skip_serializing)]
#[serde(default, skip_serializing)]
pub caniuse: Vec<String>,
/** Whether a feature is considered a "baseline" web platform feature and when it achieved that status */
#[serde(skip_serializing_if = "Option::is_none")]
pub status: Option<SupportStatusWithByKey>,
pub status: SupportStatusWithByKey,
/** Sources of support data for this feature */
#[serde(deserialize_with = "t_or_vec", default, skip_serializing)]
#[serde(default, skip_serializing)]
pub compat_features: Vec<String>,
#[serde(skip_serializing)]
pub description: String,
pub description_html: String,
#[serde(
deserialize_with = "t_or_vec",
default,
skip_serializing_if = "Vec::is_empty"
)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(skip_serializing)]
pub group: Vec<String>,
pub name: String,
#[serde(deserialize_with = "t_or_vec", default, skip_serializing)]
#[serde(default, skip_serializing)]
pub snapshot: Vec<String>,
/** Whether developers are formally discouraged from using this feature */
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -328,8 +347,7 @@ where
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
pub struct SupportStatus {
/// Whether the feature is Baseline (low substatus), Baseline (high substatus), or not (false)
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline: Option<BaselineHighLow>,
pub baseline: BaselineHighLow,
/// Date the feature achieved Baseline low status
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_low_date: Option<String>,
Expand All @@ -343,8 +361,7 @@ pub struct SupportStatus {
#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
pub struct SupportStatusWithByKey {
/// Whether the feature is Baseline (low substatus), Baseline (high substatus), or not (false)
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline: Option<BaselineHighLow>,
pub baseline: BaselineHighLow,
/// Date the feature achieved Baseline low status
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_low_date: Option<String>,
Expand All @@ -357,43 +374,6 @@ pub struct SupportStatusWithByKey {
pub by_compat_key: Option<BTreeMap<String, SupportStatus>>,
}

pub fn t_or_vec<'de, D, T>(deserializer: D) -> Result<Vec<T>, D::Error>
where
D: Deserializer<'de>,
T: Deserialize<'de>,
{
struct TOrVec<T>(PhantomData<T>);

impl<'de, T> Visitor<'de> for TOrVec<T>
where
T: Deserialize<'de>,
{
type Value = Vec<T>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or list of strings")
}

fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(vec![Deserialize::deserialize(
value::StrDeserializer::new(s),
)?])
}

fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where
S: SeqAccess<'de>,
{
Deserialize::deserialize(value::SeqAccessDeserializer::new(seq))
}
}

deserializer.deserialize_any(TOrVec::<T>(PhantomData))
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
Loading