use std::collections::BTreeMap;
use std::fmt;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
const SUPPORT_MATRIX_JSON: &str = include_str!("../support-matrix.json");
pub const SUPPORT_MATRIX_SCHEMA_VERSION: u32 = 1;
pub const FEATURE_STATES: [&str; 5] = [
"planned",
"source-supported",
"semantic-supported",
"lowering-dependent",
"end-to-end-supported",
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MatrixReference {
pub name: String,
pub version: String,
#[serde(rename = "contentCommit")]
pub content_commit: String,
pub integrity: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MatrixSnapshot {
pub date: String,
pub note: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Feature {
pub id: String,
pub name: String,
pub category: String,
pub state: String,
#[serde(default)]
pub evidence: Vec<String>,
#[serde(default)]
pub notes: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MatrixSummary {
#[serde(rename = "byState")]
pub by_state: BTreeMap<String, u32>,
#[serde(rename = "byCategory")]
pub by_category: BTreeMap<String, u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SupportMatrix {
#[serde(rename = "schemaVersion")]
pub schema_version: u32,
pub artifact: String,
pub reference: MatrixReference,
pub snapshot: MatrixSnapshot,
pub states: BTreeMap<String, String>,
pub categories: Vec<String>,
pub features: Vec<Feature>,
pub summary: MatrixSummary,
}
impl SupportMatrix {
fn load(data: &str) -> Result<SupportMatrix, SupportMatrixError> {
let matrix: SupportMatrix = serde_json::from_str(data)
.map_err(|error| SupportMatrixError(format!("invalid support matrix JSON: {error}")))?;
if matrix.schema_version != SUPPORT_MATRIX_SCHEMA_VERSION {
return Err(SupportMatrixError(format!(
"unsupported support-matrix schema version {} \
(this module understands v{SUPPORT_MATRIX_SCHEMA_VERSION})",
matrix.schema_version
)));
}
Ok(matrix)
}
pub fn builtin() -> Result<&'static SupportMatrix, SupportMatrixError> {
static MATRIX: OnceLock<Result<SupportMatrix, SupportMatrixError>> = OnceLock::new();
MATRIX
.get_or_init(|| SupportMatrix::load(SUPPORT_MATRIX_JSON))
.as_ref()
.map_err(Clone::clone)
}
pub fn feature(&self, id: &str) -> Option<&Feature> {
self.features.iter().find(|feature| feature.id == id)
}
pub fn feature_state(&self, id: &str) -> Option<&str> {
self.feature(id).map(|feature| feature.state.as_str())
}
pub fn features_by_category(&self, category: &str) -> Vec<&Feature> {
self.features
.iter()
.filter(|feature| feature.category == category)
.collect()
}
pub fn features_by_state(&self, state: &str) -> Vec<&Feature> {
self.features
.iter()
.filter(|feature| feature.state == state)
.collect()
}
pub fn categories(&self) -> &[String] {
&self.categories
}
pub fn declared_states(&self) -> &BTreeMap<String, String> {
&self.states
}
pub fn summary(&self) -> &MatrixSummary {
&self.summary
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SupportMatrixError(pub String);
impl fmt::Display for SupportMatrixError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for SupportMatrixError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_matrix_loads_and_validates() {
let matrix = SupportMatrix::builtin().expect("embedded matrix must load");
assert_eq!(matrix.schema_version, SUPPORT_MATRIX_SCHEMA_VERSION);
assert_eq!(matrix.reference.name, "overpy");
assert!(matrix.reference.version.contains('.'));
assert!(!matrix.features.is_empty());
}
#[test]
fn feature_lookup_by_id_and_state() {
let matrix = SupportMatrix::builtin().unwrap();
let lexing = matrix.feature("syntax/lexing").expect("declared feature");
assert_eq!(lexing.category, "syntax");
assert_eq!(lexing.state, "source-supported");
assert_eq!(
matrix.feature_state("syntax/lexing"),
Some("source-supported")
);
assert_eq!(
matrix.feature_state("compilation/workshop-lowering"),
Some("lowering-dependent")
);
assert_eq!(matrix.feature("syntax/nope"), None);
assert_eq!(matrix.feature_state("syntax/nope"), None);
}
#[test]
fn category_and_state_filters_match_the_summary() {
let matrix = SupportMatrix::builtin().unwrap();
let syntax = matrix.features_by_category("syntax");
assert_eq!(syntax.len(), 14);
assert!(syntax.iter().all(|feature| feature.category == "syntax"));
let lowering = matrix.features_by_state("lowering-dependent");
assert_eq!(
lowering.len(),
matrix.summary().by_state["lowering-dependent"] as usize
);
assert!(
lowering
.iter()
.all(|feature| feature.state == "lowering-dependent")
);
assert_eq!(matrix.summary().by_state["planned"], 0);
assert_eq!(matrix.summary().by_category["semantics"], 14);
let mut ids: Vec<&str> = matrix
.features
.iter()
.map(|feature| feature.id.as_str())
.collect();
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), matrix.features.len());
}
#[test]
fn declared_states_and_categories_are_exposed() {
let matrix = SupportMatrix::builtin().unwrap();
assert_eq!(matrix.declared_states().len(), FEATURE_STATES.len());
for state in FEATURE_STATES {
assert!(
matrix.declared_states().contains_key(state),
"declared state '{state}' missing from the matrix"
);
}
assert!(matrix.categories().contains(&"syntax".to_string()));
assert!(matrix.categories().contains(&"decompilation".to_string()));
}
#[test]
fn unknown_schema_version_is_rejected() {
let error = SupportMatrix::load(
r#"{"schemaVersion": 99, "artifact": "x",
"reference": {"name": "overpy", "version": "1", "contentCommit": "c", "integrity": "i"},
"snapshot": {"date": "d", "note": "n"},
"states": {}, "categories": [], "features": [],
"summary": {"byState": {}, "byCategory": {}}}"#,
)
.unwrap_err();
assert!(error.to_string().contains("schema version"));
}
}