confium_signatif/
scope.rs1use std::collections::BTreeMap;
19use std::collections::BTreeSet;
20
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
27pub enum ScopeValue {
28 #[default]
30 Wildcard,
31 Set(BTreeSet<String>),
33 Single(String),
35}
36
37impl ScopeValue {
38 pub fn narrows_within(&self, parent: &ScopeValue) -> bool {
42 match (self, parent) {
43 (_, ScopeValue::Wildcard) => true,
44 (ScopeValue::Single(a), ScopeValue::Single(b)) => a == b,
45 (ScopeValue::Single(a), ScopeValue::Set(sup)) => sup.contains(a),
46 (ScopeValue::Set(sub), ScopeValue::Set(sup)) => sub.is_subset(sup),
47 (ScopeValue::Set(_), ScopeValue::Single(_)) => false,
48 (ScopeValue::Wildcard, _) => false,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
56pub struct ScopeDimensions {
57 pub domain: ScopeValue,
59 pub subdomain: ScopeValue,
61 pub class: ScopeValue,
63 pub instance: ScopeValue,
65 pub identity: ScopeValue,
67 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
69 pub extra: BTreeMap<String, ScopeValue>,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
73 pub conditions: Vec<serde_json::Value>,
74}
75
76impl ScopeDimensions {
77 pub fn unconstrained() -> Self {
79 Self::default()
80 }
81
82 pub fn dimensions(&self) -> impl Iterator<Item = (&'static str, &ScopeValue)> {
84 [
85 ("domain", &self.domain),
86 ("subdomain", &self.subdomain),
87 ("class", &self.class),
88 ("instance", &self.instance),
89 ("identity", &self.identity),
90 ]
91 .into_iter()
92 }
93
94 pub fn get(&self, dimension: &str) -> Option<&ScopeValue> {
96 match dimension {
97 "domain" => Some(&self.domain),
98 "subdomain" => Some(&self.subdomain),
99 "class" => Some(&self.class),
100 "instance" => Some(&self.instance),
101 "identity" => Some(&self.identity),
102 other => self.extra.get(other),
103 }
104 }
105
106 pub fn set(&mut self, dimension: &str, value: ScopeValue) {
108 match dimension {
109 "domain" => self.domain = value,
110 "subdomain" => self.subdomain = value,
111 "class" => self.class = value,
112 "instance" => self.instance = value,
113 "identity" => self.identity = value,
114 other => {
115 self.extra.insert(other.to_string(), value);
116 }
117 }
118 }
119
120 pub fn narrows_within(&self, parent: &ScopeDimensions) -> bool {
124 self.first_widened_dimension(parent).is_none()
125 }
126
127 pub fn first_widened_dimension(&self, parent: &ScopeDimensions) -> Option<String> {
130 for (name, child_value) in self.dimensions() {
131 if let Some(parent_value) = parent.get(name) {
132 if !child_value.narrows_within(parent_value) {
133 return Some(name.to_string());
134 }
135 }
136 }
137 for (name, child_value) in &self.extra {
138 if let Some(parent_value) = parent.get(name) {
139 if !child_value.narrows_within(parent_value) {
140 return Some(name.clone());
141 }
142 }
143 }
144 None
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 fn single(s: &str) -> ScopeValue {
153 ScopeValue::Single(s.into())
154 }
155
156 fn set(items: &[&str]) -> ScopeValue {
157 ScopeValue::Set(items.iter().map(|s| s.to_string()).collect())
158 }
159
160 #[test]
161 fn lattice_narrowing() {
162 let wildcard = ScopeValue::Wildcard;
163 let two = set(&["pharma", "food"]);
164 let one = set(&["pharma"]);
165 let exact = single("pharma");
166
167 assert!(exact.narrows_within(&one));
168 assert!(one.narrows_within(&two));
169 assert!(two.narrows_within(&wildcard));
170 assert!(exact.narrows_within(&wildcard));
171
172 assert!(!two.narrows_within(&one));
173 assert!(!one.narrows_within(&exact));
174 assert!(!single("food").narrows_within(&one));
175 }
176
177 #[test]
178 fn monotonic_narrowing_invariant() {
179 let mut parent = ScopeDimensions::unconstrained();
180 parent.set("domain", set(&["pharma", "food"]));
181 parent.set("class", single("certificate"));
182
183 let mut child = parent.clone();
184 child.set("domain", single("pharma"));
185 assert!(child.narrows_within(&parent));
186
187 let mut widening = parent.clone();
188 widening.set("domain", ScopeValue::Wildcard);
189 assert_eq!(
190 widening.first_widened_dimension(&parent),
191 Some("domain".to_string())
192 );
193 assert!(!widening.narrows_within(&parent));
194 }
195
196 #[test]
197 fn extension_dimensions_extend_without_breaking() {
198 let parent = ScopeDimensions::unconstrained();
199 let mut child = ScopeDimensions::unconstrained();
200 child.set("cnml:instrument-class", single("mass"));
201 assert!(child.narrows_within(&parent));
203 }
204}