use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "op", content = "args")]
#[non_exhaustive]
pub enum Expr {
True,
False,
And(Vec<Expr>),
Or(Vec<Expr>),
Not(Box<Expr>),
HasCapability(String),
HasAllCapabilities(Vec<String>),
HasAnyCapability(Vec<String>),
IssuerIs(String),
IssuerIn(Vec<String>),
SubjectIs(String),
DelegatedBy(String),
NotRevoked,
NotExpired,
ExpiresAfter(i64),
IssuedWithin(i64),
RoleIs(String),
RoleIn(Vec<String>),
RepoIs(String),
RepoIn(Vec<String>),
RefMatches(String),
PathAllowed(Vec<String>),
EnvIs(String),
EnvIn(Vec<String>),
WorkloadIssuerIs(String),
WorkloadClaimEquals {
key: String,
value: String,
},
IsAgent,
IsHuman,
IsWorkload,
MaxChainDepth(u32),
AttrEquals {
key: String,
value: String,
},
AttrIn {
key: String,
values: Vec<String>,
},
MinAssurance(String),
AssuranceLevelIs(String),
ApprovalGate {
inner: Box<Expr>,
approvers: Vec<String>,
ttl_seconds: u64,
scope: Option<String>,
},
}
impl Expr {
pub fn and(conditions: impl IntoIterator<Item = Expr>) -> Self {
Expr::And(conditions.into_iter().collect())
}
pub fn or(conditions: impl IntoIterator<Item = Expr>) -> Self {
Expr::Or(conditions.into_iter().collect())
}
pub fn negate(expr: Expr) -> Self {
Expr::Not(Box::new(expr))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_true() {
let expr = Expr::True;
let json = serde_json::to_string(&expr).unwrap();
assert_eq!(json, r#"{"op":"True"}"#);
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_has_capability() {
let expr = Expr::HasCapability("sign_commit".into());
let json = serde_json::to_string(&expr).unwrap();
assert!(json.contains(r#""op":"HasCapability""#));
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_and() {
let expr = Expr::And(vec![Expr::NotRevoked, Expr::NotExpired]);
let json = serde_json::to_string(&expr).unwrap();
assert!(json.contains(r#""op":"And""#));
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_not() {
let expr = Expr::Not(Box::new(Expr::HasCapability("admin".into())));
let json = serde_json::to_string(&expr).unwrap();
assert!(json.contains(r#""op":"Not""#));
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_issuer_in() {
let expr = Expr::IssuerIn(vec!["did:keri:E1".into(), "did:keri:E2".into()]);
let json = serde_json::to_string(&expr).unwrap();
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_ref_matches() {
let expr = Expr::RefMatches("refs/heads/*".into());
let json = serde_json::to_string(&expr).unwrap();
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_workload_claim_equals() {
let expr = Expr::WorkloadClaimEquals {
key: "repo".into(),
value: "my-org/my-repo".into(),
};
let json = serde_json::to_string(&expr).unwrap();
assert!(json.contains("WorkloadClaimEquals"));
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_attr_equals() {
let expr = Expr::AttrEquals {
key: "team".into(),
value: "platform".into(),
};
let json = serde_json::to_string(&expr).unwrap();
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_attr_in() {
let expr = Expr::AttrIn {
key: "team".into(),
values: vec!["platform".into(), "security".into()],
};
let json = serde_json::to_string(&expr).unwrap();
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn serde_complex_nested() {
let expr = Expr::And(vec![
Expr::NotRevoked,
Expr::NotExpired,
Expr::Or(vec![
Expr::HasCapability("admin".into()),
Expr::And(vec![
Expr::HasCapability("write".into()),
Expr::RepoIs("my-org/my-repo".into()),
]),
]),
]);
let json = serde_json::to_string(&expr).unwrap();
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr);
}
#[test]
fn helper_and() {
let expr = Expr::and([Expr::True, Expr::False]);
match expr {
Expr::And(children) => assert_eq!(children.len(), 2),
_ => panic!("expected And"),
}
}
#[test]
fn helper_or() {
let expr = Expr::or([Expr::True, Expr::False]);
match expr {
Expr::Or(children) => assert_eq!(children.len(), 2),
_ => panic!("expected Or"),
}
}
#[test]
fn helper_negate() {
let expr = Expr::negate(Expr::True);
match expr {
Expr::Not(inner) => assert_eq!(*inner, Expr::True),
_ => panic!("expected Not"),
}
}
#[test]
fn serde_all_variants() {
let variants = vec![
Expr::True,
Expr::False,
Expr::And(vec![]),
Expr::Or(vec![]),
Expr::Not(Box::new(Expr::True)),
Expr::HasCapability("cap".into()),
Expr::HasAllCapabilities(vec!["a".into(), "b".into()]),
Expr::HasAnyCapability(vec!["a".into(), "b".into()]),
Expr::IssuerIs("did:keri:E1".into()),
Expr::IssuerIn(vec!["did:keri:E1".into()]),
Expr::SubjectIs("did:keri:E1".into()),
Expr::DelegatedBy("did:keri:E1".into()),
Expr::NotRevoked,
Expr::NotExpired,
Expr::ExpiresAfter(3600),
Expr::IssuedWithin(86400),
Expr::RoleIs("admin".into()),
Expr::RoleIn(vec!["admin".into(), "user".into()]),
Expr::RepoIs("org/repo".into()),
Expr::RepoIn(vec!["org/repo".into()]),
Expr::RefMatches("refs/heads/*".into()),
Expr::PathAllowed(vec!["src/**".into()]),
Expr::EnvIs("production".into()),
Expr::EnvIn(vec!["staging".into(), "production".into()]),
Expr::WorkloadIssuerIs("did:keri:E1".into()),
Expr::WorkloadClaimEquals {
key: "k".into(),
value: "v".into(),
},
Expr::IsAgent,
Expr::IsHuman,
Expr::IsWorkload,
Expr::MaxChainDepth(3),
Expr::AttrEquals {
key: "k".into(),
value: "v".into(),
},
Expr::AttrIn {
key: "k".into(),
values: vec!["v1".into(), "v2".into()],
},
Expr::MinAssurance("authenticated".into()),
Expr::AssuranceLevelIs("sovereign".into()),
Expr::ApprovalGate {
inner: Box::new(Expr::HasCapability("deploy".into())),
approvers: vec!["did:keri:EHuman123".into()],
ttl_seconds: 300,
scope: Some("identity".into()),
},
];
for expr in variants {
let json = serde_json::to_string(&expr).unwrap();
let parsed: Expr = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, expr, "roundtrip failed for {:?}", expr);
}
}
}