jellyflow_runtime/runtime/conformance/
capability.rs1use serde::{Deserialize, Serialize};
2
3#[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#[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}