Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(baseline): compute asterisk #77

Merged
merged 2 commits into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
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
237 changes: 205 additions & 32 deletions crates/rari-data/src/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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;
use serde::de::{self, value, SeqAccess, Visitor};
Expand All @@ -13,59 +14,205 @@ use url::Url;

use crate::error::Error;

#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct Baseline<'a> {
#[serde(flatten)]
pub support: &'a SupportStatusWithByKey,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub asterisk: bool,
}

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

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct KeyStatus {
bcd_key: String,
feature: String,
}

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct DirtyWebFeatures {
pub features: IndexMap<String, Value>,
}

#[inline]
fn spaced(bcd_key: &str) -> String {
bcd_key.replace('.', " ")
}

#[inline]
fn unspaced(bcd_key: &str) -> String {
bcd_key.replace(' ', ".")
}

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 map = WebFeatures {
features: dirty_map
.features
.into_iter()
.filter_map(|(k, v)| {
serde_json::from_value::<FeatureData>(v)
.inspect_err(|e| {
tracing::error!("Error serializing baseline for {}: {}", k, &e)
})
.ok()
.map(|v| (k, v))
let features: IndexMap<String, FeatureData> = dirty_map
.features
.into_iter()
.filter_map(|(k, v)| {
serde_json::from_value::<FeatureData>(v)
.inspect_err(|e| {
tracing::error!("Error serializing baseline for {}: {}", k, &e)
})
.ok()
.map(|v| (k, v))
})
.collect();
// bcd_keys is a sorted by KeyStatus.bcd_key
// We replace "." with " " so the sorting is stable as in:
// http headers Content-Security-Policy
// http headers Content-Security-Policy base-uri
// http headers Content-Security-Policy child-src
// http headers Content-Security-Policy-Report-Only
//
// instead of:
// http.headers.Content-Security-Policy
// http.headers.Content-Security-Policy-Report-Only
// http.headers.Content-Security-Policy.base-uri
// http.headers.Content-Security-Policy.child-src
//
// This allows to simple return ranges when looking for keys prefixed with
// `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: spaced(bcd_key),
feature: feature.clone(),
})
.collect(),
};
})
.collect();
bcd_keys.sort_by(|a, b| a.bcd_key.cmp(&b.bcd_key));
bcd_keys.dedup_by(|a, b| a.bcd_key == b.bcd_key);

let map = WebFeatures { features, bcd_keys };
Ok(map)
}

pub fn feature_status(&self, bcd_key: &str) -> Option<&SupportStatusWithByKey> {
self.features.values().find_map(|feature_data| {
if let Some(ref status) = feature_data.status {
if feature_data
.compat_features
pub fn sub_keys(&self, bcd_key: &str) -> &[KeyStatus] {
let suffix = concat_strs!(bcd_key, " ");
if let Ok(start) = self
.bcd_keys
.binary_search_by_key(&bcd_key, |ks| &ks.bcd_key)
{
if start < self.bcd_keys.len() {
if let Some(end) = self.bcd_keys[start + 1..]
.iter()
.any(|key| key == bcd_key)
.position(|ks| !ks.bcd_key.starts_with(&suffix))
{
if feature_data.discouraged.is_some() {
return None;
return &self.bcd_keys[start + 1..start + 1 + end];
}
}
}
&[]
}

// Compute status according to:
// https://github.com/mdn/yari/issues/11546#issuecomment-2531611136
pub fn feature_status(&self, bcd_key: &str) -> Option<Baseline> {
let bcd_key_spaced = &spaced(bcd_key);
if let Some(status) = self.feature_status_internal(bcd_key_spaced) {
let sub_keys = self.sub_keys(bcd_key_spaced);
let sub_status = sub_keys
.iter()
.map(|sub_key| {
self.feature_status_internal_with_feature_name(
&sub_key.bcd_key,
&sub_key.feature,
)
.and_then(|status| status.baseline)
})
.collect::<Vec<_>>();
if sub_status
.iter()
.all(|baseline| baseline == &status.baseline)
{
return Some(Baseline {
support: status,
asterisk: false,
});
}
match status.baseline {
Some(BaselineHighLow::False(false)) => {
let Support {
chrome,
chrome_android,
firefox,
firefox_android,
safari,
safari_ios,
..
} = &status.support;
if chrome == chrome_android
&& firefox == firefox_android
&& safari == safari_ios
{
return Some(Baseline {
support: status,
asterisk: false,
});
}
if let Some(by_key) = &status.by_compat_key {
if let Some(key_status) = by_key.get(bcd_key) {
if key_status.baseline == status.baseline {
return Some(status);
}
}
}
Some(BaselineHighLow::Low) => {
if sub_status
.iter()
.all(|ss| matches!(ss, Some(BaselineHighLow::Low | BaselineHighLow::High)))
{
return Some(Baseline {
support: status,
asterisk: false,
});
}
}
_ => {}
}
Some(Baseline {
support: status,
asterisk: true,
})
} else {
None
})
}
}

fn feature_status_internal(&self, bcd_key_spaced: &str) -> Option<&SupportStatusWithByKey> {
if let Ok(i) = self
.bcd_keys
.binary_search_by(|ks| ks.bcd_key.as_str().cmp(bcd_key_spaced))
{
let feature_name = &self.bcd_keys[i].feature;
return self.feature_status_internal_with_feature_name(bcd_key_spaced, feature_name);
}
None
}

fn feature_status_internal_with_feature_name(
&self,
bcd_key_spaced: &str,
feature_name: &str,
) -> Option<&SupportStatusWithByKey> {
if let Some(feature_data) = self.features.get(feature_name) {
if feature_data.discouraged.is_some() {
return None;
}
if let Some(ref status) = feature_data.status {
if let Some(by_key) = &status.by_compat_key {
if let Some(key_status) = by_key.get(&unspaced(bcd_key_spaced)) {
if key_status.baseline == status.baseline {
return Some(status);
}
}
}
}
}
None
}
}

Expand Down Expand Up @@ -112,7 +259,15 @@ pub struct FeatureData {
pub snapshot: Vec<String>,
/** Whether developers are formally discouraged from using this feature */
#[serde(skip_serializing_if = "Option::is_none")]
pub discouraged: Option<Value>,
pub discouraged: Option<Discouraged>,
}

#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct Discouraged {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
according_to: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
alternatives: Vec<String>,
}

#[derive(Deserialize, Serialize, Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
Expand All @@ -126,7 +281,25 @@ pub enum BrowserIdentifier {
Safari,
SafariIos,
}

#[derive(
Deserialize, Serialize, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, JsonSchema,
)]
pub struct Support {
#[serde(default, skip_serializing_if = "Option::is_none")]
chrome: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
chrome_android: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
edge: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
firefox: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
firefox_android: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
safari: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
safari_ios: Option<String>,
}
#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BaselineHighLow {
Expand All @@ -148,7 +321,7 @@ pub struct SupportStatus {
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_high_date: Option<String>,
/// Browser versions that most-recently introduced the feature
pub support: BTreeMap<BrowserIdentifier, String>,
pub support: Support,
}

#[derive(Deserialize, Serialize, Clone, Debug, JsonSchema)]
Expand All @@ -163,7 +336,7 @@ pub struct SupportStatusWithByKey {
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_high_date: Option<String>,
/// Browser versions that most-recently introduced the feature
pub support: BTreeMap<BrowserIdentifier, String>,
pub support: Support,
#[serde(default, skip_serializing)]
pub by_compat_key: Option<BTreeMap<String, SupportStatus>>,
}
Expand Down
4 changes: 2 additions & 2 deletions crates/rari-doc/src/baseline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//! support status for specific browser compatibility keys.
use std::sync::LazyLock;

use rari_data::baseline::{SupportStatusWithByKey, WebFeatures};
use rari_data::baseline::{Baseline, WebFeatures};
use rari_types::globals::data_dir;
use tracing::warn;

Expand Down Expand Up @@ -36,7 +36,7 @@ static WEB_FEATURES: LazyLock<Option<WebFeatures>> = LazyLock::new(|| {
///
/// * `Option<&'static SupportStatusWithByKey>` - Returns `Some(&SupportStatusWithByKey)` if the key is found,
/// or `None` if the key is not found or `WEB_FEATURES` is not initialized.
pub(crate) fn get_baseline(browser_compat: &[String]) -> Option<&'static SupportStatusWithByKey> {
pub(crate) fn get_baseline<'a>(browser_compat: &[String]) -> Option<Baseline<'a>> {
if let Some(ref web_features) = *WEB_FEATURES {
return match &browser_compat {
&[bcd_key] => web_features.feature_status(bcd_key.as_str()),
Expand Down
6 changes: 3 additions & 3 deletions crates/rari-doc/src/pages/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use std::path::PathBuf;

use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use rari_data::baseline::SupportStatusWithByKey;
use rari_data::baseline::Baseline;
use rari_types::fm_types::PageType;
use rari_types::locale::{Locale, Native};
use schemars::JsonSchema;
Expand Down Expand Up @@ -264,7 +264,7 @@ pub struct JsonDoc {
pub title: String,
pub toc: Vec<TocEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline: Option<&'static SupportStatusWithByKey>,
pub baseline: Option<Baseline<'static>>,
#[serde(rename = "browserCompat", skip_serializing_if = "Vec::is_empty")]
pub browser_compat: Vec<String>,
#[serde(rename = "pageType")]
Expand Down Expand Up @@ -349,7 +349,7 @@ pub struct JsonDocMetadata {
pub summary: Option<String>,
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline: Option<&'static SupportStatusWithByKey>,
pub baseline: Option<Baseline<'static>>,
#[serde(rename = "browserCompat", skip_serializing_if = "Vec::is_empty")]
pub browser_compat: Vec<String>,
#[serde(rename = "pageType")]
Expand Down
Loading