diff --git a/crates/rari-data/src/baseline.rs b/crates/rari-data/src/baseline.rs index 780e2b8e..84ddcb4d 100644 --- a/crates/rari-data/src/baseline.rs +++ b/crates/rari-data/src/baseline.rs @@ -21,19 +21,21 @@ impl WebFeatures { Ok(serde_json::from_str(&json_str)?) } - pub fn feature_status(&self, features: &[&str]) -> Option<&SupportStatus> { - if features.is_empty() { - return None; - } - + 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 .iter() - .any(|key| features.contains(&key.as_str())) + .any(|key| key == bcd_key) { - return Some(status); + 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); + } + } + } } } None @@ -59,7 +61,7 @@ pub struct FeatureData { 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: Option, /** Sources of support data for this feature */ #[serde( deserialize_with = "t_or_vec", @@ -67,6 +69,21 @@ pub struct FeatureData { skip_serializing_if = "Vec::is_empty" )] pub compat_features: Vec, + pub description: String, + pub description_html: String, + #[serde( + deserialize_with = "t_or_vec", + default, + skip_serializing_if = "Vec::is_empty" + )] + pub group: Vec, + pub name: String, + #[serde( + deserialize_with = "t_or_vec", + default, + skip_serializing_if = "Vec::is_empty" + )] + pub snapshot: Vec, } #[derive(Deserialize, Serialize, Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] @@ -81,7 +98,7 @@ pub enum BrowserIdentifier { SafariIos, } -#[derive(Deserialize, Serialize, Clone, Copy, Debug)] +#[derive(Deserialize, Serialize, Clone, Copy, Debug, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum BaselineHighLow { High, @@ -105,6 +122,23 @@ pub struct SupportStatus { pub support: BTreeMap, } +#[derive(Deserialize, Serialize, Clone, Debug)] +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, + /// Date the feature achieved Baseline low status + #[serde(skip_serializing_if = "Option::is_none")] + pub baseline_low_date: Option, + /// Date the feature achieved Baseline high status + #[serde(skip_serializing_if = "Option::is_none")] + pub baseline_high_date: Option, + /// Browser versions that most-recently introduced the feature + pub support: BTreeMap, + #[serde(default, skip_serializing)] + pub by_compat_key: Option>, +} + pub fn t_or_vec<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, diff --git a/crates/rari-deps/src/error.rs b/crates/rari-deps/src/error.rs index 2019a764..07a47d46 100644 --- a/crates/rari-deps/src/error.rs +++ b/crates/rari-deps/src/error.rs @@ -10,8 +10,12 @@ pub enum DepsError { RariIoError(#[from] rari_utils::error::RariIoError), #[error(transparent)] FetchError(#[from] reqwest::Error), + #[error(transparent)] + HeaderError(#[from] reqwest::header::ToStrError), #[error("no version for webref")] WebRefMissingVersionError, #[error("no tarball for webref")] WebRefMissingTarballError, + #[error("Invalid github version")] + InvalidGitHubVersion, } diff --git a/crates/rari-deps/src/github_release.rs b/crates/rari-deps/src/github_release.rs new file mode 100644 index 00000000..a9f23afc --- /dev/null +++ b/crates/rari-deps/src/github_release.rs @@ -0,0 +1,93 @@ +use std::fs::{self, File}; +use std::io::{BufWriter, Read, Write}; +use std::path::{Path, PathBuf}; + +use chrono::{DateTime, Duration, Utc}; +use rari_utils::io::read_to_string; +use reqwest::redirect::Policy; +use serde::{Deserialize, Serialize}; + +use crate::error::DepsError; + +#[derive(Deserialize, Serialize, Default, Debug)] +pub struct Current { + pub latest_last_check: Option>, + pub version: String, +} + +/// Download and unpack an npm package for a given version (defaults to latest). +pub fn get_artifact( + base_url: &str, + artifact: &str, + name: &str, + version: Option<&str>, + out_path: &Path, +) -> Result, DepsError> { + let version = version.unwrap_or("latest"); + let package_path = out_path.join(name); + let last_check_path = package_path.join("last_check.json"); + let now = Utc::now(); + let current = read_to_string(last_check_path) + .ok() + .and_then(|current| serde_json::from_str::(¤t).ok()) + .unwrap_or_default(); + if version != current.version + || version == "latest" + && current.latest_last_check.unwrap_or_default() < now - Duration::days(1) + { + let prev_url = format!( + "{base_url}/releases/download/{}/{artifact}", + current.version + ); + let url = if version == "latest" { + let client = reqwest::blocking::ClientBuilder::default() + .redirect(Policy::none()) + .build()?; + let res = client + .get(format!("{base_url}/releases/latest/download/{artifact}")) + .send()?; + res.headers() + .get("location") + .ok_or(DepsError::InvalidGitHubVersion)? + .to_str()? + .to_string() + } else { + format!("{base_url}/releases/download/{version}/{artifact}") + }; + + let artifact_path = package_path.join(artifact); + let download_update = if artifact_path.exists() { + prev_url != url + } else { + true + }; + + if download_update { + if package_path.exists() { + fs::remove_dir_all(&package_path)?; + } + fs::create_dir_all(&package_path)?; + let mut buf = vec![]; + let _ = reqwest::blocking::get(url)?.read_to_end(&mut buf)?; + + let file = File::create(artifact_path).unwrap(); + let mut buffed = BufWriter::new(file); + + buffed.write_all(&buf)?; + } + + if version == "latest" { + fs::write( + package_path.join("last_check.json"), + serde_json::to_string_pretty(&Current { + version: version.to_string(), + latest_last_check: Some(now), + })?, + )?; + } + if download_update { + return Ok(Some(package_path)); + } + } + Ok(None) +} diff --git a/crates/rari-deps/src/lib.rs b/crates/rari-deps/src/lib.rs index 581d66e7..b34a23f5 100644 --- a/crates/rari-deps/src/lib.rs +++ b/crates/rari-deps/src/lib.rs @@ -1,6 +1,7 @@ pub mod bcd; pub mod error; pub mod external_json; +pub mod github_release; pub mod mdn_data; pub mod npm; pub mod web_ext_examples; diff --git a/crates/rari-deps/src/web_features.rs b/crates/rari-deps/src/web_features.rs index fabfec6a..79eb4ba4 100644 --- a/crates/rari-deps/src/web_features.rs +++ b/crates/rari-deps/src/web_features.rs @@ -1,9 +1,16 @@ use std::path::Path; use crate::error::DepsError; -use crate::npm::get_package; +use crate::github_release::get_artifact; pub fn update_web_features(base_path: &Path) -> Result<(), DepsError> { - get_package("web-features", None, base_path)?; + //get_package("web-features", None, base_path)?; + get_artifact( + "https://github.com/web-platform-dx/web-features", + "data.extended.json", + "baseline", + None, + base_path, + )?; Ok(()) } diff --git a/crates/rari-doc/src/baseline.rs b/crates/rari-doc/src/baseline.rs index 4cd03d4b..79d8fd88 100644 --- a/crates/rari-doc/src/baseline.rs +++ b/crates/rari-doc/src/baseline.rs @@ -1,16 +1,12 @@ use std::sync::LazyLock; -use rari_data::baseline::{SupportStatus, WebFeatures}; +use rari_data::baseline::{SupportStatusWithByKey, WebFeatures}; use rari_types::globals::data_dir; use tracing::warn; static WEB_FEATURES: LazyLock> = LazyLock::new(|| { - let web_features = WebFeatures::from_file( - &data_dir() - .join("web-features") - .join("package") - .join("data.json"), - ); + let web_features = + WebFeatures::from_file(&data_dir().join("baseline").join("data.extended.json")); match web_features { Ok(web_features) => Some(web_features), Err(e) => { @@ -20,58 +16,12 @@ static WEB_FEATURES: LazyLock> = LazyLock::new(|| { } }); -static DISALLOW_LIST: &[&str] = &[ - // https://github.com/web-platform-dx/web-features/blob/cf718ad/feature-group-definitions/async-clipboard.yml - "api.Clipboard.read", - "api.Clipboard.readText", - "api.Clipboard.write", - "api.Clipboard.writeText", - "api.ClipboardEvent", - "api.ClipboardEvent.ClipboardEvent", - "api.ClipboardEvent.clipboardData", - "api.ClipboardItem", - "api.ClipboardItem.ClipboardItem", - "api.ClipboardItem.getType", - "api.ClipboardItem.presentationStyle", - "api.ClipboardItem.types", - "api.Navigator.clipboard", - "api.Permissions.permission_clipboard-read", - // https://github.com/web-platform-dx/web-features/blob/cf718ad/feature-group-definitions/custom-elements.yml - "api.CustomElementRegistry", - "api.CustomElementRegistry.builtin_element_support", - "api.CustomElementRegistry.define", - "api.Window.customElements", - "css.selectors.defined", - "css.selectors.host", - "css.selectors.host-context", - "css.selectors.part", - // https://github.com/web-platform-dx/web-features/blob/cf718ad/feature-group-definitions/input-event.yml - "api.Element.input_event", - "api.InputEvent.InputEvent", - "api.InputEvent.data", - "api.InputEvent.dataTransfer", - "api.InputEvent.getTargetRanges", - "api.InputEvent.inputType", - // https://github.com/web-platform-dx/web-features/issues/1038 - // https://github.com/web-platform-dx/web-features/blob/64d2cfd/features/screen-orientation-lock.dist.yml - "api.ScreenOrientation.lock", - "api.ScreenOrientation.unlock", -]; - -pub fn get_baseline(browser_compat: &[String]) -> Option<&'static SupportStatus> { +pub fn get_baseline(browser_compat: &[String]) -> Option<&'static SupportStatusWithByKey> { if let Some(ref web_features) = *WEB_FEATURES { - if browser_compat.is_empty() { - return None; - } - let filtered_browser_compat = browser_compat.iter().filter_map( - |query| - // temporary blocklist while we wait for per-key baseline statuses - // or another solution to the baseline/bcd table discrepancy problem - if !DISALLOW_LIST.contains(&query.as_str()) { - Some(query.as_str()) - } else {None} - ).collect::>(); - return web_features.feature_status(&filtered_browser_compat); + return match &browser_compat { + &[bcd_key] => web_features.feature_status(bcd_key.as_str()), + _ => None, + }; } None } diff --git a/crates/rari-doc/src/docs/json.rs b/crates/rari-doc/src/docs/json.rs index d576013f..0ea3548e 100644 --- a/crates/rari-doc/src/docs/json.rs +++ b/crates/rari-doc/src/docs/json.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; use chrono::NaiveDateTime; -use rari_data::baseline::SupportStatus; +use rari_data::baseline::SupportStatusWithByKey; use rari_types::locale::{Locale, Native}; use serde::Serialize; @@ -111,7 +111,7 @@ pub struct JsonDoc { pub title: String, pub toc: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub baseline: Option<&'static SupportStatus>, + pub baseline: Option<&'static SupportStatusWithByKey>, #[serde(rename = "browserCompat", skip_serializing_if = "Vec::is_empty")] pub browser_compat: Vec, } diff --git a/crates/rari-doc/src/docs/title.rs b/crates/rari-doc/src/docs/title.rs index 3d38cf41..bb8572db 100644 --- a/crates/rari-doc/src/docs/title.rs +++ b/crates/rari-doc/src/docs/title.rs @@ -53,7 +53,7 @@ pub fn root_doc_url(url: &str) -> Option<&str> { if url[m[1]..].starts_with("/blog") || url[m[1]..].starts_with("/curriculum") { return None; } - if url[m[1]..].starts_with("/docs/Web") { + if url[m[1]..].starts_with("/docs/Web/") { return Some(&url[..*m.get(4).unwrap_or(&url.len())]); } Some(&url[..*m.get(3).unwrap_or(&url.len())])