Skip to main content

opy_rs/
support.rs

1//! Compatibility support-matrix accessor (read-only).
2//!
3//! Loads the packaged `support-matrix.json` (copied from the evidence
4//! workstream) and exposes feature-state queries by id and category, so
5//! consumers and the CLI can report OPY support status without duplicating
6//! the matrix.
7//!
8//! The matrix is embedded at build time via [`include_str!`] rather than
9//! loaded at runtime: the shipped artifact always carries the exact matrix CI
10//! validated, no runtime file lookup or dependency is required, and cargo
11//! rebuilds the crate automatically when the file changes. The module is a
12//! strict read-only consumer — it never writes, rewrites, or caches a
13//! modified copy of the matrix.
14//!
15//! The five declared feature states (`planned`, `source-supported`,
16//! `semantic-supported`, `lowering-dependent`, `end-to-end-supported`) are
17//! documented in the matrix itself; Workshop-dependent items stay
18//! `lowering-dependent` and are never approximated here (repo ownership
19//! boundary: see `AGENTS.md`).
20
21use std::collections::BTreeMap;
22use std::fmt;
23use std::sync::OnceLock;
24
25use serde::{Deserialize, Serialize};
26
27/// The embedded support matrix shipped with this crate.
28const SUPPORT_MATRIX_JSON: &str = include_str!("../support-matrix.json");
29
30/// The matrix schema version this module understands.
31pub const SUPPORT_MATRIX_SCHEMA_VERSION: u32 = 1;
32
33/// The declared feature states (see the matrix's `states` map for wording).
34pub const FEATURE_STATES: [&str; 5] = [
35    "planned",
36    "source-supported",
37    "semantic-supported",
38    "lowering-dependent",
39    "end-to-end-supported",
40];
41
42/// The pinned reference identity recorded in the matrix.
43#[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/// The snapshot provenance recorded in the matrix.
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub struct MatrixSnapshot {
55    pub date: String,
56    pub note: String,
57}
58
59/// One support-matrix feature entry.
60#[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/// The matrix summary (counts by state and category).
73#[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/// The machine-readable support matrix, mirroring
82/// `compatibility/support-matrix.json`.
83#[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    /// The declared state domain: state name → description.
91    pub states: BTreeMap<String, String>,
92    pub categories: Vec<String>,
93    pub features: Vec<Feature>,
94    pub summary: MatrixSummary,
95}
96
97impl SupportMatrix {
98    /// Load and validate a matrix payload. Rejects an unknown schema version;
99    /// state/category consistency is additionally enforced by the evidence
100    /// workstream's harness test suite.
101    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    /// The embedded support matrix, parsed once and cached.
115    ///
116    /// An embedded matrix that fails to parse or validate is a build/provenance
117    /// error in this repository, so this is a hard error rather than a
118    /// recoverable lookup failure.
119    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    /// The feature entry with `id`, when declared.
128    pub fn feature(&self, id: &str) -> Option<&Feature> {
129        self.features.iter().find(|feature| feature.id == id)
130    }
131
132    /// The support state of a feature id (e.g. `planned`,
133    /// `semantic-supported`, `lowering-dependent`).
134    pub fn feature_state(&self, id: &str) -> Option<&str> {
135        self.feature(id).map(|feature| feature.state.as_str())
136    }
137
138    /// Every feature entry of a category (e.g. `syntax`, `semantics`).
139    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    /// Every feature entry in a state (e.g. `lowering-dependent`).
147    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    /// The declared categories.
155    pub fn categories(&self) -> &[String] {
156        &self.categories
157    }
158
159    /// The declared state domain (state name → description).
160    pub fn declared_states(&self) -> &BTreeMap<String, String> {
161        &self.states
162    }
163
164    /// The feature-count summary by state and category.
165    pub fn summary(&self) -> &MatrixSummary {
166        &self.summary
167    }
168}
169
170/// A support-matrix load failure.
171#[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        // Every feature id is unique.
232        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}