1use crate::error::PolicyError;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum Expr {
55 Claim(String),
57 Not(Box<Expr>),
59 And(Vec<Expr>),
61 Or(Vec<Expr>),
63}
64
65impl Expr {
66 pub fn and(parts: Vec<Expr>) -> Expr {
73 let mut flat = Vec::with_capacity(parts.len());
74 for p in parts {
75 match p {
76 Expr::And(children) => flat.extend(children),
77 other => flat.push(other),
78 }
79 }
80 if flat.len() == 1 {
81 flat.into_iter().next().unwrap()
82 } else {
83 Expr::And(flat)
84 }
85 }
86
87 pub fn or(parts: Vec<Expr>) -> Expr {
94 let mut flat = Vec::with_capacity(parts.len());
95 for p in parts {
96 match p {
97 Expr::Or(children) => flat.extend(children),
98 other => flat.push(other),
99 }
100 }
101 if flat.len() == 1 {
102 flat.into_iter().next().unwrap()
103 } else {
104 Expr::Or(flat)
105 }
106 }
107
108 #[allow(clippy::should_implement_trait)]
115 pub fn not(inner: Expr) -> Expr {
116 Expr::Not(Box::new(inner))
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum Tri {
127 True,
129 False,
131 Unknown,
133}
134
135impl std::ops::Not for Tri {
136 type Output = Tri;
137 fn not(self) -> Self {
138 match self {
139 Tri::True => Tri::False,
140 Tri::False => Tri::True,
141 Tri::Unknown => Tri::Unknown,
142 }
143 }
144}
145
146pub fn evaluate<F>(expr: &Expr, lookup: &F) -> Tri
152where
153 F: Fn(&str) -> Tri,
154{
155 match expr {
156 Expr::Claim(name) => lookup(name),
157 Expr::Not(inner) => !evaluate(inner, lookup),
158 Expr::And(children) => {
159 let mut all_true = true;
162 for c in children {
163 match evaluate(c, lookup) {
164 Tri::False => return Tri::False,
165 Tri::True => {}
166 Tri::Unknown => all_true = false,
167 }
168 }
169 if all_true { Tri::True } else { Tri::Unknown }
170 }
171 Expr::Or(children) => {
172 let mut all_false = true;
175 for c in children {
176 match evaluate(c, lookup) {
177 Tri::True => return Tri::True,
178 Tri::False => {}
179 Tri::Unknown => all_false = false,
180 }
181 }
182 if all_false { Tri::False } else { Tri::Unknown }
183 }
184 }
185}
186
187#[derive(Debug, PartialEq, Eq)]
192enum Token {
193 Ident(String),
194 And,
195 Or,
196 Not,
197 Implies,
198 LParen,
199 RParen,
200}
201
202fn tokenize(input: &str) -> Result<Vec<Token>, PolicyError> {
203 let mut out = Vec::new();
204 let mut chars = input.chars().peekable();
205 while let Some(&c) = chars.peek() {
206 if c.is_whitespace() {
207 chars.next();
208 continue;
209 }
210 if c == '(' {
211 chars.next();
212 out.push(Token::LParen);
213 continue;
214 }
215 if c == ')' {
216 chars.next();
217 out.push(Token::RParen);
218 continue;
219 }
220 if is_ident_start(c) {
221 let mut s = String::new();
222 while let Some(&c) = chars.peek() {
223 if is_ident_continue(c) {
224 s.push(c);
225 chars.next();
226 } else {
227 break;
228 }
229 }
230 out.push(match s.to_ascii_lowercase().as_str() {
231 "and" => Token::And,
232 "or" => Token::Or,
233 "not" => Token::Not,
234 "implies" => Token::Implies,
235 _ => Token::Ident(s),
236 });
237 continue;
238 }
239 return Err(PolicyError::ExprParse(format!(
240 "unexpected character {c:?} in expression"
241 )));
242 }
243 Ok(out)
244}
245
246fn is_ident_start(c: char) -> bool {
247 c.is_ascii_alphabetic() || c == '_'
248}
249
250fn is_ident_continue(c: char) -> bool {
251 c.is_ascii_alphanumeric() || c == '_' || c == '-'
252}
253
254struct Parser {
259 tokens: std::vec::IntoIter<Token>,
260 peeked: Option<Token>,
261}
262
263impl Parser {
264 fn new(tokens: Vec<Token>) -> Self {
265 Self {
266 tokens: tokens.into_iter(),
267 peeked: None,
268 }
269 }
270
271 fn peek(&mut self) -> Option<&Token> {
272 if self.peeked.is_none() {
273 self.peeked = self.tokens.next();
274 }
275 self.peeked.as_ref()
276 }
277
278 fn consume(&mut self) -> Option<Token> {
279 if let Some(t) = self.peeked.take() {
280 return Some(t);
281 }
282 self.tokens.next()
283 }
284
285 fn parse_expr(&mut self) -> Result<Expr, PolicyError> {
286 self.parse_implies()
287 }
288
289 fn parse_implies(&mut self) -> Result<Expr, PolicyError> {
290 let left = self.parse_or()?;
291 if matches!(self.peek(), Some(Token::Implies)) {
292 self.consume();
293 let right = self.parse_implies()?;
297 return Ok(Expr::or(vec![Expr::not(left), right]));
298 }
299 Ok(left)
300 }
301
302 fn parse_or(&mut self) -> Result<Expr, PolicyError> {
303 let mut parts = vec![self.parse_and()?];
304 while matches!(self.peek(), Some(Token::Or)) {
305 self.consume();
306 parts.push(self.parse_and()?);
307 }
308 Ok(Expr::or(parts))
309 }
310
311 fn parse_and(&mut self) -> Result<Expr, PolicyError> {
312 let mut parts = vec![self.parse_not()?];
313 while matches!(self.peek(), Some(Token::And)) {
314 self.consume();
315 parts.push(self.parse_not()?);
316 }
317 Ok(Expr::and(parts))
318 }
319
320 fn parse_not(&mut self) -> Result<Expr, PolicyError> {
321 if matches!(self.peek(), Some(Token::Not)) {
322 self.consume();
323 let inner = self.parse_not()?;
324 return Ok(Expr::not(inner));
325 }
326 self.parse_atom()
327 }
328
329 fn parse_atom(&mut self) -> Result<Expr, PolicyError> {
330 match self.consume() {
331 Some(Token::Ident(s)) => Ok(Expr::Claim(s)),
332 Some(Token::LParen) => {
333 let inner = self.parse_expr()?;
334 match self.consume() {
335 Some(Token::RParen) => Ok(inner),
336 _ => Err(PolicyError::ExprParse("missing closing ')'".into())),
337 }
338 }
339 Some(t) => Err(PolicyError::ExprParse(format!(
340 "unexpected token {t:?}; expected claim or '('"
341 ))),
342 None => Err(PolicyError::ExprParse(
343 "unexpected end of expression".into(),
344 )),
345 }
346 }
347}
348
349pub fn parse(input: &str) -> Result<Expr, PolicyError> {
355 let tokens = tokenize(input)?;
356 let mut p = Parser::new(tokens);
357 let expr = p.parse_expr()?;
358 if p.peek().is_some() {
359 return Err(PolicyError::ExprParse(format!(
360 "trailing tokens after expression: {:?}",
361 p.consume()
362 )));
363 }
364 Ok(expr)
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 fn ev(expr: &str, claims: &[(&str, bool)]) -> Tri {
372 let e = parse(expr).unwrap();
373 let lookup = |name: &str| match claims.iter().find(|(n, _)| *n == name) {
374 Some((_, true)) => Tri::True,
375 Some((_, false)) => Tri::False,
376 None => Tri::Unknown,
377 };
378 evaluate(&e, &lookup)
379 }
380
381 #[test]
382 fn single_claim() {
383 assert_eq!(ev("safe-to-deploy", &[("safe-to-deploy", true)]), Tri::True);
384 assert_eq!(
385 ev("safe-to-deploy", &[("safe-to-deploy", false)]),
386 Tri::False
387 );
388 assert_eq!(ev("safe-to-deploy", &[]), Tri::Unknown);
389 }
390
391 #[test]
392 fn and_basic() {
393 assert_eq!(ev("a and b", &[("a", true), ("b", true)]), Tri::True);
394 assert_eq!(ev("a and b", &[("a", true), ("b", false)]), Tri::False);
395 assert_eq!(ev("a and b", &[("a", true)]), Tri::Unknown);
396 }
397
398 #[test]
399 fn and_short_circuits_on_false() {
400 assert_eq!(ev("a and b", &[("a", false)]), Tri::False);
402 }
403
404 #[test]
405 fn or_basic() {
406 assert_eq!(ev("a or b", &[("a", false), ("b", true)]), Tri::True);
407 assert_eq!(ev("a or b", &[("a", false), ("b", false)]), Tri::False);
408 assert_eq!(ev("a or b", &[("a", false)]), Tri::Unknown);
409 }
410
411 #[test]
412 fn or_short_circuits_on_true() {
413 assert_eq!(ev("a or b", &[("a", true)]), Tri::True);
414 }
415
416 #[test]
417 fn not_inverts() {
418 assert_eq!(ev("not a", &[("a", true)]), Tri::False);
419 assert_eq!(ev("not a", &[("a", false)]), Tri::True);
420 assert_eq!(ev("not a", &[]), Tri::Unknown);
421 }
422
423 #[test]
424 fn precedence_not_binds_tightest() {
425 assert_eq!(ev("not a and b", &[("a", false), ("b", true)]), Tri::True);
427 assert_eq!(ev("not a and b", &[("a", true), ("b", true)]), Tri::False);
428 }
429
430 #[test]
431 fn precedence_and_binds_tighter_than_or() {
432 assert_eq!(
434 ev("a or b and c", &[("a", false), ("b", true), ("c", true)]),
435 Tri::True
436 );
437 assert_eq!(
438 ev("a or b and c", &[("a", false), ("b", true), ("c", false)]),
439 Tri::False
440 );
441 }
442
443 #[test]
444 fn parens_override_precedence() {
445 assert_eq!(
447 ev("(a or b) and c", &[("a", true), ("c", false)]),
448 Tri::False
449 );
450 }
451
452 #[test]
453 fn nested_not_and_or() {
454 assert_eq!(
455 ev(
456 "not (a and b) or c",
457 &[("a", true), ("b", true), ("c", false)]
458 ),
459 Tri::False
460 );
461 assert_eq!(
462 ev("not (a and b) or c", &[("a", true), ("b", false)]),
463 Tri::True
464 );
465 }
466
467 #[test]
468 fn case_insensitive_keywords() {
469 assert_eq!(ev("a AND b", &[("a", true), ("b", true)]), Tri::True);
470 assert_eq!(ev("NOT a", &[("a", true)]), Tri::False);
471 assert_eq!(ev("a IMPLIES b", &[("a", true), ("b", true)]), Tri::True);
472 }
473
474 #[test]
475 fn implies_truth_table() {
476 assert_eq!(ev("a implies b", &[("a", true), ("b", true)]), Tri::True);
478 assert_eq!(ev("a implies b", &[("a", true), ("b", false)]), Tri::False);
479 assert_eq!(ev("a implies b", &[("a", false), ("b", false)]), Tri::True);
481 assert_eq!(ev("a implies b", &[("a", false)]), Tri::True);
482 assert_eq!(ev("a implies b", &[("a", true)]), Tri::Unknown);
484 assert_eq!(ev("a implies b", &[("b", true)]), Tri::True);
487 assert_eq!(ev("a implies b", &[("b", false)]), Tri::Unknown);
489 }
490
491 #[test]
492 fn implies_lower_than_or_and_and() {
493 assert_eq!(
495 ev(
496 "a implies b and c",
497 &[("a", true), ("b", true), ("c", false)]
498 ),
499 Tri::False
500 );
501 assert_eq!(
504 ev(
505 "a or b implies c",
506 &[("a", true), ("b", false), ("c", false)]
507 ),
508 Tri::False
509 );
510 assert_eq!(
512 ev(
513 "a or b implies c",
514 &[("a", false), ("b", false), ("c", false)]
515 ),
516 Tri::True
517 );
518 }
519
520 #[test]
521 fn implies_right_associative() {
522 assert_eq!(
532 ev(
533 "a implies b implies c",
534 &[("a", false), ("b", true), ("c", false)]
535 ),
536 Tri::True
537 );
538 }
539
540 #[test]
541 fn implies_desugars_to_or_not() {
542 let lhs = parse("a implies b").unwrap();
544 let rhs = parse("(not a) or b").unwrap();
545 assert_eq!(lhs, rhs);
546 }
547
548 #[test]
549 fn nary_and_chain_flattens() {
550 let e = parse("a and b and c").unwrap();
553 let Expr::And(children) = e else {
554 panic!("expected top-level And");
555 };
556 assert_eq!(children.len(), 3);
557 assert_eq!(children[0], Expr::Claim("a".into()));
558 assert_eq!(children[1], Expr::Claim("b".into()));
559 assert_eq!(children[2], Expr::Claim("c".into()));
560 }
561
562 #[test]
563 fn nary_or_chain_flattens() {
564 let e = parse("a or b or c").unwrap();
565 let Expr::Or(children) = e else {
566 panic!("expected top-level Or");
567 };
568 assert_eq!(children.len(), 3);
569 }
570
571 #[test]
572 fn parens_with_same_op_get_spliced() {
573 let e = parse("(a or b) or c").unwrap();
575 let Expr::Or(children) = e else {
576 panic!("expected flat Or");
577 };
578 assert_eq!(children.len(), 3);
579
580 let e = parse("a and (b and c)").unwrap();
583 let Expr::And(children) = e else {
584 panic!("expected flat And");
585 };
586 assert_eq!(children.len(), 3);
587 }
588
589 #[test]
590 fn implies_with_or_rhs_splices_into_one_or() {
591 let e = parse("a implies b or c").unwrap();
595 let Expr::Or(children) = e else {
596 panic!("expected top-level Or");
597 };
598 assert_eq!(children.len(), 3);
599 assert_eq!(children[0], Expr::not(Expr::Claim("a".into())));
600 assert_eq!(children[1], Expr::Claim("b".into()));
601 assert_eq!(children[2], Expr::Claim("c".into()));
602 }
603
604 #[test]
605 fn mixed_op_nesting_is_preserved() {
606 let e = parse("a and (b or c) and d").unwrap();
609 let Expr::And(children) = e else {
610 panic!("expected top-level And");
611 };
612 assert_eq!(children.len(), 3);
613 assert!(matches!(&children[1], Expr::Or(inner) if inner.len() == 2));
614 }
615
616 #[test]
617 fn single_element_collapses() {
618 let x = Expr::Claim("x".into());
620 assert_eq!(Expr::and(vec![x.clone()]), x);
621 assert_eq!(Expr::or(vec![x.clone()]), x);
622 }
623
624 #[test]
625 fn parse_errors() {
626 assert!(parse("a and").is_err());
627 assert!(parse("(a or b").is_err());
628 assert!(parse("and a").is_err());
629 assert!(parse("a b").is_err()); assert!(parse("").is_err());
631 assert!(parse("a #").is_err()); assert!(parse("a implies").is_err()); assert!(parse("implies b").is_err()); }
635}