1use crate::error::{ReplError, Result};
2
3#[derive(Debug, Clone)]
5pub struct ParsedPolicy {
6 pub name: String,
8 pub rules: Vec<PolicyRule>,
10}
11
12#[derive(Debug, Clone)]
14pub struct PolicyRule {
15 pub action: String,
17 pub target: String,
19 pub condition: Option<String>,
21}
22
23pub fn parse_policy(policy: &str) -> Result<ParsedPolicy> {
35 if policy.is_empty() {
36 return Err(ReplError::PolicyParsing("Empty policy".to_string()));
37 }
38
39 let lines: Vec<&str> = policy
40 .lines()
41 .map(|l| l.trim())
42 .filter(|l| !l.is_empty())
43 .collect();
44 if lines.is_empty() {
45 return Err(ReplError::PolicyParsing(
46 "Policy contains only whitespace".to_string(),
47 ));
48 }
49
50 let first_line = lines[0];
52 let name = if let Some(stripped) = first_line.strip_prefix("policy") {
53 let name = stripped.trim();
54 if name.is_empty() {
55 return Err(ReplError::PolicyParsing(
56 "Policy name missing after 'policy' keyword".to_string(),
57 ));
58 }
59 name.to_string()
60 } else {
61 return Ok(ParsedPolicy {
64 name: "inline".to_string(),
65 rules: vec![PolicyRule {
66 action: "apply".to_string(),
67 target: policy.to_string(),
68 condition: None,
69 }],
70 });
71 };
72
73 let mut rules = Vec::new();
74 for line in &lines[1..] {
75 if *line == "end" {
76 break;
77 }
78
79 if line.starts_with('#') || line.starts_with("//") {
81 continue;
82 }
83
84 let rule = parse_rule_line(line)?;
85 rules.push(rule);
86 }
87
88 if rules.is_empty() {
89 return Err(ReplError::PolicyParsing(format!(
90 "Policy '{}' has no rules",
91 name
92 )));
93 }
94
95 Ok(ParsedPolicy { name, rules })
96}
97
98fn parse_rule_line(line: &str) -> Result<PolicyRule> {
100 let (main_part, condition) = if let Some(idx) = line.find(" when ") {
102 let (main, cond) = line.split_at(idx);
103 (main.trim(), Some(cond[5..].trim().to_string())) } else {
105 (line, None)
106 };
107
108 let mut parts = main_part.splitn(2, ' ');
109 let action = parts
110 .next()
111 .ok_or_else(|| ReplError::PolicyParsing(format!("Empty rule line: '{}'", line)))?
112 .to_string();
113
114 let target = parts
115 .next()
116 .ok_or_else(|| {
117 ReplError::PolicyParsing(format!(
118 "Rule '{}' missing target (expected '<action> <target>')",
119 action
120 ))
121 })?
122 .to_string();
123
124 let valid_actions = ["allow", "deny", "audit", "limit", "require", "apply"];
126 if !valid_actions.contains(&action.as_str()) {
127 return Err(ReplError::PolicyParsing(format!(
128 "Unknown policy action '{}'; expected one of: {}",
129 action,
130 valid_actions.join(", ")
131 )));
132 }
133
134 Ok(PolicyRule {
135 action,
136 target,
137 condition,
138 })
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn empty_policy_fails() {
147 assert!(parse_policy("").is_err());
148 }
149
150 #[test]
151 fn whitespace_only_fails() {
152 assert!(parse_policy(" \n \n ").is_err());
153 }
154
155 #[test]
156 fn simple_string_becomes_inline_policy() {
157 let result = parse_policy("no_external_calls").unwrap();
158 assert_eq!(result.name, "inline");
159 assert_eq!(result.rules.len(), 1);
160 assert_eq!(result.rules[0].action, "apply");
161 assert_eq!(result.rules[0].target, "no_external_calls");
162 }
163
164 #[test]
165 fn structured_policy_parses() {
166 let input = r#"
167policy hipaa_guard
168 deny network_access when patient_data
169 allow read /approved/*
170 audit all_operations
171end
172"#;
173 let result = parse_policy(input).unwrap();
174 assert_eq!(result.name, "hipaa_guard");
175 assert_eq!(result.rules.len(), 3);
176
177 assert_eq!(result.rules[0].action, "deny");
178 assert_eq!(result.rules[0].target, "network_access");
179 assert_eq!(result.rules[0].condition.as_deref(), Some("patient_data"));
180
181 assert_eq!(result.rules[1].action, "allow");
182 assert_eq!(result.rules[1].target, "read /approved/*");
183 assert!(result.rules[1].condition.is_none());
184
185 assert_eq!(result.rules[2].action, "audit");
186 assert_eq!(result.rules[2].target, "all_operations");
187 }
188
189 #[test]
190 fn missing_policy_name_fails() {
191 assert!(parse_policy("policy\n allow all\nend").is_err());
192 }
193
194 #[test]
195 fn no_rules_fails() {
196 assert!(parse_policy("policy empty\nend").is_err());
197 }
198
199 #[test]
200 fn unknown_action_fails() {
201 let input = "policy test\n explode everything\nend";
202 assert!(parse_policy(input).is_err());
203 }
204
205 #[test]
206 fn comments_are_skipped() {
207 let input = r#"
208policy test
209 # This is a comment
210 allow read
211 // Another comment
212 deny write
213end
214"#;
215 let result = parse_policy(input).unwrap();
216 assert_eq!(result.rules.len(), 2);
217 }
218
219 #[test]
220 fn condition_parsing() {
221 let input = "policy gate\n limit api_calls when rate > 100\nend";
222 let result = parse_policy(input).unwrap();
223 assert_eq!(result.rules[0].condition.as_deref(), Some("rate > 100"));
224 }
225}