1use crate::{DdError, ErrorCode, Result};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct Rule {
7 pub name: String,
8 pub when: String,
9 pub filters: Vec<(String, String)>,
10 pub action: String,
11}
12
13pub fn parse_rules(src: &str) -> Result<Vec<Rule>> {
14 let mut rules = Vec::new();
15 let mut cur: Option<Rule> = None;
16 for raw in src.lines() {
17 let line = raw.trim();
18 if line.is_empty() || line.starts_with('#') || line == "}" {
19 if line == "}"
20 && let Some(r) = cur.take()
21 {
22 rules.push(r);
23 }
24 continue;
25 }
26 if let Some(rest) = line.strip_prefix("rule ") {
27 let name = rest.trim().trim_end_matches('{').trim().to_string();
28 cur = Some(Rule {
29 name,
30 when: String::new(),
31 filters: Vec::new(),
32 action: String::new(),
33 });
34 continue;
35 }
36 let Some(rule) = cur.as_mut() else {
37 return Err(DdError::protocol(
38 ErrorCode::Ddp1001InvalidFrame,
39 "rule statement outside block",
40 ));
41 };
42 if let Some(w) = line.strip_prefix("when ") {
43 rule.when = w.trim().to_string();
44 } else if let Some(i) = line.strip_prefix("if ") {
45 let i = i.trim();
46 if let Some((k, v)) = i.split_once("==") {
47 rule.filters
48 .push((k.trim().to_string(), v.trim().trim_matches('"').to_string()));
49 }
50 } else if let Some(t) = line.strip_prefix("then ") {
51 rule.action = t.trim().to_string();
52 }
53 }
54 if let Some(r) = cur.take() {
55 rules.push(r);
56 }
57 Ok(rules)
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[test]
65 fn parse_pin_rule() {
66 let src = r#"
67rule auto-pin-releases {
68 when drop.received
69 if drop.channel == "software/releases"
70 if drop.publisher.trust == "verified"
71 then drop.pin
72}
73"#;
74 let r = parse_rules(src).unwrap();
75 assert_eq!(r.len(), 1);
76 assert_eq!(r[0].name, "auto-pin-releases");
77 assert_eq!(r[0].action, "drop.pin");
78 assert_eq!(r[0].filters.len(), 2);
79 }
80}