From 6b94eae45cc87491ed767b11fb5fcbbefb8a9b68 Mon Sep 17 00:00:00 2001 From: Leo McArdle Date: Tue, 16 Sep 2025 13:52:57 +0100 Subject: [PATCH] chore(web-features): update schema to match v3 upstream --- crates/rari-data/src/baseline.rs | 214 ++++++++++++++----------------- 1 file changed, 97 insertions(+), 117 deletions(-) diff --git a/crates/rari-data/src/baseline.rs b/crates/rari-data/src/baseline.rs index f6b068cf..885cc28f 100644 --- a/crates/rari-data/src/baseline.rs +++ b/crates/rari-data/src/baseline.rs @@ -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; @@ -26,7 +23,7 @@ pub struct Baseline<'a> { #[derive(Deserialize, Serialize, Clone, Debug)] pub struct WebFeatures { - pub features: IndexMap, + pub features: IndexMap, pub bcd_keys: Vec, } @@ -51,11 +48,11 @@ impl WebFeatures { pub fn from_file(path: &Path) -> Result { let json_str = read_to_string(path)?; let dirty_map: DirtyWebFeatures = serde_json::from_str(&json_str)?; - let features: IndexMap = dirty_map + let features: IndexMap = dirty_map .features .into_iter() .filter_map(|(k, v)| { - serde_json::from_value::(v) + serde_json::from_value::(v) .inspect_err(|e| { tracing::error!("Error serializing baseline for {}: {}", k, &e) }) @@ -80,13 +77,17 @@ impl WebFeatures { // `http headers Content-Security-Policy` let mut bcd_keys: Vec = 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); @@ -118,57 +119,55 @@ impl WebFeatures { pub fn baseline_by_bcd_key(&self, bcd_key: &str) -> Option> { 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::>(); + 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::>(); - 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 @@ -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), + 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, +} + #[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, /** caniuse.com identifier */ - #[serde(deserialize_with = "t_or_vec", default, skip_serializing)] + #[serde(default, skip_serializing)] pub caniuse: Vec, /** 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, + 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, #[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, pub name: String, - #[serde(deserialize_with = "t_or_vec", default, skip_serializing)] + #[serde(default, skip_serializing)] pub snapshot: Vec, /** Whether developers are formally discouraged from using this feature */ #[serde(skip_serializing_if = "Option::is_none")] @@ -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, + pub baseline: BaselineHighLow, /// Date the feature achieved Baseline low status #[serde(skip_serializing_if = "Option::is_none")] pub baseline_low_date: Option, @@ -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, + pub baseline: BaselineHighLow, /// Date the feature achieved Baseline low status #[serde(skip_serializing_if = "Option::is_none")] pub baseline_low_date: Option, @@ -357,43 +374,6 @@ pub struct SupportStatusWithByKey { pub by_compat_key: Option>, } -pub fn t_or_vec<'de, D, T>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, - T: Deserialize<'de>, -{ - struct TOrVec(PhantomData); - - impl<'de, T> Visitor<'de> for TOrVec - where - T: Deserialize<'de>, - { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("string or list of strings") - } - - fn visit_str(self, s: &str) -> Result - where - E: de::Error, - { - Ok(vec![Deserialize::deserialize( - value::StrDeserializer::new(s), - )?]) - } - - fn visit_seq(self, seq: S) -> Result - where - S: SeqAccess<'de>, - { - Deserialize::deserialize(value::SeqAccessDeserializer::new(seq)) - } - } - - deserializer.deserialize_any(TOrVec::(PhantomData)) -} - #[cfg(test)] mod test { use super::*;