Skip to main content

jellyflow_runtime/runtime/conformance/
capability.rs

1use serde::{Deserialize, Serialize};
2
3/// Support level an adapter can claim for a conformance capability.
4///
5/// The ordering is intentional: a `full` adapter satisfies `partial`, while a
6/// `projection` proof does not satisfy editable or layout-pass expectations.
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
8#[serde(rename_all = "snake_case")]
9pub enum ConformanceSupportLevel {
10    #[default]
11    None,
12    Projection,
13    Partial,
14    Full,
15}
16
17impl ConformanceSupportLevel {
18    pub fn satisfies(self, minimum: Self) -> bool {
19        self >= minimum
20    }
21
22    pub fn is_supported(self) -> bool {
23        self != Self::None
24    }
25}
26
27/// Renderer-neutral capability names used by adapter conformance reports.
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "snake_case")]
30pub enum ConformanceCapabilityKind {
31    MeasuredHandles,
32    MeasuredAnchors,
33    DynamicInternals,
34    ControlProjection,
35    EditableControls,
36    RepeatableCollections,
37    Actions,
38    Menus,
39    DroppedWireMenu,
40    Inspector,
41    Blackboard,
42    TypedDiagnostics,
43    VisualRegression,
44    KeyboardAccessibility,
45    LayoutPassMeasurement,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct ConformanceCapabilityClaim {
50    pub capability: ConformanceCapabilityKind,
51    pub level: ConformanceSupportLevel,
52    #[serde(default, skip_serializing_if = "Option::is_none")]
53    pub notes: Option<String>,
54}
55
56impl ConformanceCapabilityClaim {
57    pub fn new(capability: ConformanceCapabilityKind, level: ConformanceSupportLevel) -> Self {
58        Self {
59            capability,
60            level,
61            notes: None,
62        }
63    }
64
65    pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
66        self.notes = Some(notes.into());
67        self
68    }
69
70    pub fn projection(capability: ConformanceCapabilityKind) -> Self {
71        Self::new(capability, ConformanceSupportLevel::Projection)
72    }
73
74    pub fn partial(capability: ConformanceCapabilityKind) -> Self {
75        Self::new(capability, ConformanceSupportLevel::Partial)
76    }
77
78    pub fn full(capability: ConformanceCapabilityKind) -> Self {
79        Self::new(capability, ConformanceSupportLevel::Full)
80    }
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct ConformanceCapabilityRequirement {
85    pub capability: ConformanceCapabilityKind,
86    pub minimum: ConformanceSupportLevel,
87}
88
89impl ConformanceCapabilityRequirement {
90    pub fn new(capability: ConformanceCapabilityKind, minimum: ConformanceSupportLevel) -> Self {
91        Self {
92            capability,
93            minimum,
94        }
95    }
96}
97
98#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
99pub struct ConformanceCapabilityMatrix {
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    pub adapter: Option<String>,
102    #[serde(default, skip_serializing_if = "Vec::is_empty")]
103    pub claims: Vec<ConformanceCapabilityClaim>,
104}
105
106impl ConformanceCapabilityMatrix {
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    pub fn for_adapter(adapter: impl Into<String>) -> Self {
112        Self {
113            adapter: Some(adapter.into()),
114            claims: Vec::new(),
115        }
116    }
117
118    pub fn is_empty(&self) -> bool {
119        self.adapter.is_none() && self.claims.is_empty()
120    }
121
122    pub fn with_claim(mut self, claim: ConformanceCapabilityClaim) -> Self {
123        self.set_claim(claim);
124        self
125    }
126
127    pub fn set_claim(&mut self, claim: ConformanceCapabilityClaim) {
128        if let Some(existing) = self
129            .claims
130            .iter_mut()
131            .find(|existing| existing.capability == claim.capability)
132        {
133            *existing = claim;
134        } else {
135            self.claims.push(claim);
136        }
137    }
138
139    pub fn claim(
140        &self,
141        capability: ConformanceCapabilityKind,
142    ) -> Option<&ConformanceCapabilityClaim> {
143        self.claims
144            .iter()
145            .find(|claim| claim.capability == capability)
146    }
147
148    pub fn level(&self, capability: ConformanceCapabilityKind) -> ConformanceSupportLevel {
149        self.claim(capability)
150            .map(|claim| claim.level)
151            .unwrap_or_default()
152    }
153
154    pub fn satisfies(
155        &self,
156        capability: ConformanceCapabilityKind,
157        minimum: ConformanceSupportLevel,
158    ) -> bool {
159        self.level(capability).satisfies(minimum)
160    }
161
162    pub fn gaps<'a>(
163        &self,
164        requirements: impl IntoIterator<Item = &'a ConformanceCapabilityRequirement>,
165    ) -> Vec<ConformanceCapabilityGap> {
166        requirements
167            .into_iter()
168            .filter_map(|requirement| {
169                let actual = self.level(requirement.capability);
170                (!actual.satisfies(requirement.minimum)).then_some(ConformanceCapabilityGap {
171                    capability: requirement.capability,
172                    required: requirement.minimum,
173                    actual,
174                })
175            })
176            .collect()
177    }
178}
179
180#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
181pub struct ConformanceCapabilityGap {
182    pub capability: ConformanceCapabilityKind,
183    pub required: ConformanceSupportLevel,
184    pub actual: ConformanceSupportLevel,
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn support_levels_are_ordered_by_strength() {
193        assert!(ConformanceSupportLevel::Full.satisfies(ConformanceSupportLevel::Partial));
194        assert!(ConformanceSupportLevel::Partial.satisfies(ConformanceSupportLevel::Projection));
195        assert!(!ConformanceSupportLevel::Projection.satisfies(ConformanceSupportLevel::Partial));
196    }
197
198    #[test]
199    fn matrix_reports_missing_capabilities_as_none() {
200        let matrix = ConformanceCapabilityMatrix::for_adapter("unit").with_claim(
201            ConformanceCapabilityClaim::partial(ConformanceCapabilityKind::ControlProjection),
202        );
203        let requirements = [
204            ConformanceCapabilityRequirement::new(
205                ConformanceCapabilityKind::ControlProjection,
206                ConformanceSupportLevel::Projection,
207            ),
208            ConformanceCapabilityRequirement::new(
209                ConformanceCapabilityKind::LayoutPassMeasurement,
210                ConformanceSupportLevel::Full,
211            ),
212        ];
213
214        let gaps = matrix.gaps(&requirements);
215
216        assert_eq!(gaps.len(), 1);
217        assert_eq!(
218            gaps[0].capability,
219            ConformanceCapabilityKind::LayoutPassMeasurement
220        );
221        assert_eq!(gaps[0].actual, ConformanceSupportLevel::None);
222    }
223}