1use std::collections::BTreeMap;
22use std::fmt;
23use std::sync::OnceLock;
24
25use serde::{Deserialize, Serialize};
26
27const SUPPORT_MATRIX_JSON: &str = include_str!("../support-matrix.json");
29
30pub const SUPPORT_MATRIX_SCHEMA_VERSION: u32 = 1;
32
33pub const FEATURE_STATES: [&str; 5] = [
35 "planned",
36 "source-supported",
37 "semantic-supported",
38 "lowering-dependent",
39 "end-to-end-supported",
40];
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
44pub struct MatrixReference {
45 pub name: String,
46 pub version: String,
47 #[serde(rename = "contentCommit")]
48 pub content_commit: String,
49 pub integrity: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct MatrixSnapshot {
55 pub date: String,
56 pub note: String,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61pub struct Feature {
62 pub id: String,
63 pub name: String,
64 pub category: String,
65 pub state: String,
66 #[serde(default)]
67 pub evidence: Vec<String>,
68 #[serde(default)]
69 pub notes: String,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74pub struct MatrixSummary {
75 #[serde(rename = "byState")]
76 pub by_state: BTreeMap<String, u32>,
77 #[serde(rename = "byCategory")]
78 pub by_category: BTreeMap<String, u32>,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct SupportMatrix {
85 #[serde(rename = "schemaVersion")]
86 pub schema_version: u32,
87 pub artifact: String,
88 pub reference: MatrixReference,
89 pub snapshot: MatrixSnapshot,
90 pub states: BTreeMap<String, String>,
92 pub categories: Vec<String>,
93 pub features: Vec<Feature>,
94 pub summary: MatrixSummary,
95}
96
97impl SupportMatrix {
98 fn load(data: &str) -> Result<SupportMatrix, SupportMatrixError> {
102 let matrix: SupportMatrix = serde_json::from_str(data)
103 .map_err(|error| SupportMatrixError(format!("invalid support matrix JSON: {error}")))?;
104 if matrix.schema_version != SUPPORT_MATRIX_SCHEMA_VERSION {
105 return Err(SupportMatrixError(format!(
106 "unsupported support-matrix schema version {} \
107 (this module understands v{SUPPORT_MATRIX_SCHEMA_VERSION})",
108 matrix.schema_version
109 )));
110 }
111 Ok(matrix)
112 }
113
114 pub fn builtin() -> Result<&'static SupportMatrix, SupportMatrixError> {
120 static MATRIX: OnceLock<Result<SupportMatrix, SupportMatrixError>> = OnceLock::new();
121 MATRIX
122 .get_or_init(|| SupportMatrix::load(SUPPORT_MATRIX_JSON))
123 .as_ref()
124 .map_err(Clone::clone)
125 }
126
127 pub fn feature(&self, id: &str) -> Option<&Feature> {
129 self.features.iter().find(|feature| feature.id == id)
130 }
131
132 pub fn feature_state(&self, id: &str) -> Option<&str> {
135 self.feature(id).map(|feature| feature.state.as_str())
136 }
137
138 pub fn features_by_category(&self, category: &str) -> Vec<&Feature> {
140 self.features
141 .iter()
142 .filter(|feature| feature.category == category)
143 .collect()
144 }
145
146 pub fn features_by_state(&self, state: &str) -> Vec<&Feature> {
148 self.features
149 .iter()
150 .filter(|feature| feature.state == state)
151 .collect()
152 }
153
154 pub fn categories(&self) -> &[String] {
156 &self.categories
157 }
158
159 pub fn declared_states(&self) -> &BTreeMap<String, String> {
161 &self.states
162 }
163
164 pub fn summary(&self) -> &MatrixSummary {
166 &self.summary
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct SupportMatrixError(pub String);
173
174impl fmt::Display for SupportMatrixError {
175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176 write!(f, "{}", self.0)
177 }
178}
179
180impl std::error::Error for SupportMatrixError {}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn embedded_matrix_loads_and_validates() {
188 let matrix = SupportMatrix::builtin().expect("embedded matrix must load");
189 assert_eq!(matrix.schema_version, SUPPORT_MATRIX_SCHEMA_VERSION);
190 assert_eq!(matrix.reference.name, "overpy");
191 assert!(matrix.reference.version.contains('.'));
192 assert!(!matrix.features.is_empty());
193 }
194
195 #[test]
196 fn feature_lookup_by_id_and_state() {
197 let matrix = SupportMatrix::builtin().unwrap();
198 let lexing = matrix.feature("syntax/lexing").expect("declared feature");
199 assert_eq!(lexing.category, "syntax");
200 assert_eq!(lexing.state, "source-supported");
201 assert_eq!(
202 matrix.feature_state("syntax/lexing"),
203 Some("source-supported")
204 );
205 assert_eq!(
206 matrix.feature_state("compilation/workshop-lowering"),
207 Some("lowering-dependent")
208 );
209 assert_eq!(matrix.feature("syntax/nope"), None);
210 assert_eq!(matrix.feature_state("syntax/nope"), None);
211 }
212
213 #[test]
214 fn category_and_state_filters_match_the_summary() {
215 let matrix = SupportMatrix::builtin().unwrap();
216 let syntax = matrix.features_by_category("syntax");
217 assert_eq!(syntax.len(), 14);
218 assert!(syntax.iter().all(|feature| feature.category == "syntax"));
219 let lowering = matrix.features_by_state("lowering-dependent");
220 assert_eq!(
221 lowering.len(),
222 matrix.summary().by_state["lowering-dependent"] as usize
223 );
224 assert!(
225 lowering
226 .iter()
227 .all(|feature| feature.state == "lowering-dependent")
228 );
229 assert_eq!(matrix.summary().by_state["planned"], 0);
230 assert_eq!(matrix.summary().by_category["semantics"], 14);
231 let mut ids: Vec<&str> = matrix
233 .features
234 .iter()
235 .map(|feature| feature.id.as_str())
236 .collect();
237 ids.sort_unstable();
238 ids.dedup();
239 assert_eq!(ids.len(), matrix.features.len());
240 }
241
242 #[test]
243 fn declared_states_and_categories_are_exposed() {
244 let matrix = SupportMatrix::builtin().unwrap();
245 assert_eq!(matrix.declared_states().len(), FEATURE_STATES.len());
246 for state in FEATURE_STATES {
247 assert!(
248 matrix.declared_states().contains_key(state),
249 "declared state '{state}' missing from the matrix"
250 );
251 }
252 assert!(matrix.categories().contains(&"syntax".to_string()));
253 assert!(matrix.categories().contains(&"decompilation".to_string()));
254 }
255
256 #[test]
257 fn unknown_schema_version_is_rejected() {
258 let error = SupportMatrix::load(
259 r#"{"schemaVersion": 99, "artifact": "x",
260 "reference": {"name": "overpy", "version": "1", "contentCommit": "c", "integrity": "i"},
261 "snapshot": {"date": "d", "note": "n"},
262 "states": {}, "categories": [], "features": [],
263 "summary": {"byState": {}, "byCategory": {}}}"#,
264 )
265 .unwrap_err();
266 assert!(error.to_string().contains("schema version"));
267 }
268}