1use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21#[serde(tag = "op", content = "args")]
22#[non_exhaustive]
23pub enum Expr {
24 True,
27 False,
29 And(Vec<Expr>),
31 Or(Vec<Expr>),
33 Not(Box<Expr>),
35
36 HasCapability(String),
39 HasAllCapabilities(Vec<String>),
41 HasAnyCapability(Vec<String>),
43
44 IssuerIs(String),
47 IssuerIn(Vec<String>),
49 SubjectIs(String),
51 DelegatedBy(String),
53
54 NotRevoked,
57 NotExpired,
59 ExpiresAfter(i64),
61 IssuedWithin(i64),
63
64 RoleIs(String),
67 RoleIn(Vec<String>),
69
70 RepoIs(String),
73 RepoIn(Vec<String>),
75 RefMatches(String),
77 PathAllowed(Vec<String>),
79 EnvIs(String),
81 EnvIn(Vec<String>),
83
84 WorkloadIssuerIs(String),
87 WorkloadClaimEquals {
89 key: String,
91 value: String,
93 },
94
95 IsAgent,
98 IsHuman,
100 IsWorkload,
102
103 MaxChainDepth(u32),
106
107 AttrEquals {
111 key: String,
113 value: String,
115 },
116 AttrIn {
118 key: String,
120 values: Vec<String>,
122 },
123
124 MinAssurance(String),
127 AssuranceLevelIs(String),
129
130 ApprovalGate {
134 inner: Box<Expr>,
136 approvers: Vec<String>,
138 ttl_seconds: u64,
140 scope: Option<String>,
142 },
143}
144
145impl Expr {
146 pub fn and(conditions: impl IntoIterator<Item = Expr>) -> Self {
148 Expr::And(conditions.into_iter().collect())
149 }
150
151 pub fn or(conditions: impl IntoIterator<Item = Expr>) -> Self {
153 Expr::Or(conditions.into_iter().collect())
154 }
155
156 pub fn negate(expr: Expr) -> Self {
158 Expr::Not(Box::new(expr))
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn serde_true() {
168 let expr = Expr::True;
169 let json = serde_json::to_string(&expr).unwrap();
170 assert_eq!(json, r#"{"op":"True"}"#);
171 let parsed: Expr = serde_json::from_str(&json).unwrap();
172 assert_eq!(parsed, expr);
173 }
174
175 #[test]
176 fn serde_has_capability() {
177 let expr = Expr::HasCapability("sign_commit".into());
178 let json = serde_json::to_string(&expr).unwrap();
179 assert!(json.contains(r#""op":"HasCapability""#));
180 let parsed: Expr = serde_json::from_str(&json).unwrap();
181 assert_eq!(parsed, expr);
182 }
183
184 #[test]
185 fn serde_and() {
186 let expr = Expr::And(vec![Expr::NotRevoked, Expr::NotExpired]);
187 let json = serde_json::to_string(&expr).unwrap();
188 assert!(json.contains(r#""op":"And""#));
189 let parsed: Expr = serde_json::from_str(&json).unwrap();
190 assert_eq!(parsed, expr);
191 }
192
193 #[test]
194 fn serde_not() {
195 let expr = Expr::Not(Box::new(Expr::HasCapability("admin".into())));
196 let json = serde_json::to_string(&expr).unwrap();
197 assert!(json.contains(r#""op":"Not""#));
198 let parsed: Expr = serde_json::from_str(&json).unwrap();
199 assert_eq!(parsed, expr);
200 }
201
202 #[test]
203 fn serde_issuer_in() {
204 let expr = Expr::IssuerIn(vec!["did:keri:E1".into(), "did:keri:E2".into()]);
205 let json = serde_json::to_string(&expr).unwrap();
206 let parsed: Expr = serde_json::from_str(&json).unwrap();
207 assert_eq!(parsed, expr);
208 }
209
210 #[test]
211 fn serde_ref_matches() {
212 let expr = Expr::RefMatches("refs/heads/*".into());
213 let json = serde_json::to_string(&expr).unwrap();
214 let parsed: Expr = serde_json::from_str(&json).unwrap();
215 assert_eq!(parsed, expr);
216 }
217
218 #[test]
219 fn serde_workload_claim_equals() {
220 let expr = Expr::WorkloadClaimEquals {
221 key: "repo".into(),
222 value: "my-org/my-repo".into(),
223 };
224 let json = serde_json::to_string(&expr).unwrap();
225 assert!(json.contains("WorkloadClaimEquals"));
226 let parsed: Expr = serde_json::from_str(&json).unwrap();
227 assert_eq!(parsed, expr);
228 }
229
230 #[test]
231 fn serde_attr_equals() {
232 let expr = Expr::AttrEquals {
233 key: "team".into(),
234 value: "platform".into(),
235 };
236 let json = serde_json::to_string(&expr).unwrap();
237 let parsed: Expr = serde_json::from_str(&json).unwrap();
238 assert_eq!(parsed, expr);
239 }
240
241 #[test]
242 fn serde_attr_in() {
243 let expr = Expr::AttrIn {
244 key: "team".into(),
245 values: vec!["platform".into(), "security".into()],
246 };
247 let json = serde_json::to_string(&expr).unwrap();
248 let parsed: Expr = serde_json::from_str(&json).unwrap();
249 assert_eq!(parsed, expr);
250 }
251
252 #[test]
253 fn serde_complex_nested() {
254 let expr = Expr::And(vec![
255 Expr::NotRevoked,
256 Expr::NotExpired,
257 Expr::Or(vec![
258 Expr::HasCapability("admin".into()),
259 Expr::And(vec![
260 Expr::HasCapability("write".into()),
261 Expr::RepoIs("my-org/my-repo".into()),
262 ]),
263 ]),
264 ]);
265 let json = serde_json::to_string(&expr).unwrap();
266 let parsed: Expr = serde_json::from_str(&json).unwrap();
267 assert_eq!(parsed, expr);
268 }
269
270 #[test]
271 fn helper_and() {
272 let expr = Expr::and([Expr::True, Expr::False]);
273 match expr {
274 Expr::And(children) => assert_eq!(children.len(), 2),
275 _ => panic!("expected And"),
276 }
277 }
278
279 #[test]
280 fn helper_or() {
281 let expr = Expr::or([Expr::True, Expr::False]);
282 match expr {
283 Expr::Or(children) => assert_eq!(children.len(), 2),
284 _ => panic!("expected Or"),
285 }
286 }
287
288 #[test]
289 fn helper_negate() {
290 let expr = Expr::negate(Expr::True);
291 match expr {
292 Expr::Not(inner) => assert_eq!(*inner, Expr::True),
293 _ => panic!("expected Not"),
294 }
295 }
296
297 #[test]
298 fn serde_all_variants() {
299 let variants = vec![
301 Expr::True,
302 Expr::False,
303 Expr::And(vec![]),
304 Expr::Or(vec![]),
305 Expr::Not(Box::new(Expr::True)),
306 Expr::HasCapability("cap".into()),
307 Expr::HasAllCapabilities(vec!["a".into(), "b".into()]),
308 Expr::HasAnyCapability(vec!["a".into(), "b".into()]),
309 Expr::IssuerIs("did:keri:E1".into()),
310 Expr::IssuerIn(vec!["did:keri:E1".into()]),
311 Expr::SubjectIs("did:keri:E1".into()),
312 Expr::DelegatedBy("did:keri:E1".into()),
313 Expr::NotRevoked,
314 Expr::NotExpired,
315 Expr::ExpiresAfter(3600),
316 Expr::IssuedWithin(86400),
317 Expr::RoleIs("admin".into()),
318 Expr::RoleIn(vec!["admin".into(), "user".into()]),
319 Expr::RepoIs("org/repo".into()),
320 Expr::RepoIn(vec!["org/repo".into()]),
321 Expr::RefMatches("refs/heads/*".into()),
322 Expr::PathAllowed(vec!["src/**".into()]),
323 Expr::EnvIs("production".into()),
324 Expr::EnvIn(vec!["staging".into(), "production".into()]),
325 Expr::WorkloadIssuerIs("did:keri:E1".into()),
326 Expr::WorkloadClaimEquals {
327 key: "k".into(),
328 value: "v".into(),
329 },
330 Expr::IsAgent,
331 Expr::IsHuman,
332 Expr::IsWorkload,
333 Expr::MaxChainDepth(3),
334 Expr::AttrEquals {
335 key: "k".into(),
336 value: "v".into(),
337 },
338 Expr::AttrIn {
339 key: "k".into(),
340 values: vec!["v1".into(), "v2".into()],
341 },
342 Expr::MinAssurance("authenticated".into()),
343 Expr::AssuranceLevelIs("sovereign".into()),
344 Expr::ApprovalGate {
345 inner: Box::new(Expr::HasCapability("deploy".into())),
346 approvers: vec!["did:keri:EHuman123".into()],
347 ttl_seconds: 300,
348 scope: Some("identity".into()),
349 },
350 ];
351
352 for expr in variants {
353 let json = serde_json::to_string(&expr).unwrap();
354 let parsed: Expr = serde_json::from_str(&json).unwrap();
355 assert_eq!(parsed, expr, "roundtrip failed for {:?}", expr);
356 }
357 }
358}