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];
56 let name = if let Some(stripped) = first_line.strip_prefix("policy") {
57 let name = stripped.trim();
58 if name.is_empty() {
59 return Err(ReplError::PolicyParsing(
60 "Policy name missing after 'policy' keyword".to_string(),
61 ));
62 }
63 name.to_string()
64 } else {
65 return Err(ReplError::PolicyParsing(format!(
66 "Policy text must begin with a 'policy <name>' header; got {:?}. \
67 Inline/implicit policies are no longer accepted.",
68 first_line
69 )));
70 };
71
72 let mut rules = Vec::new();
73 for line in &lines[1..] {
74 if *line == "end" {
75 break;
76 }
77
78 if line.starts_with('#') || line.starts_with("//") {
80 continue;
81 }
82
83 let rule = parse_rule_line(line)?;
84 rules.push(rule);
85 }
86
87 if rules.is_empty() {
88 return Err(ReplError::PolicyParsing(format!(
89 "Policy '{}' has no rules",
90 name
91 )));
92 }
93
94 Ok(ParsedPolicy { name, rules })
95}
96
97fn parse_rule_line(line: &str) -> Result<PolicyRule> {
99 let (main_part, condition) = if let Some(idx) = line.find(" when ") {
101 let (main, cond) = line.split_at(idx);
102 (main.trim(), Some(cond[5..].trim().to_string())) } else {
104 (line, None)
105 };
106
107 let mut parts = main_part.splitn(2, ' ');
108 let action = parts
109 .next()
110 .ok_or_else(|| ReplError::PolicyParsing(format!("Empty rule line: '{}'", line)))?
111 .to_string();
112
113 let target = parts
114 .next()
115 .ok_or_else(|| {
116 ReplError::PolicyParsing(format!(
117 "Rule '{}' missing target (expected '<action> <target>')",
118 action
119 ))
120 })?
121 .to_string();
122
123 let valid_actions = ["allow", "deny", "audit", "limit", "require", "apply"];
125 if !valid_actions.contains(&action.as_str()) {
126 return Err(ReplError::PolicyParsing(format!(
127 "Unknown policy action '{}'; expected one of: {}",
128 action,
129 valid_actions.join(", ")
130 )));
131 }
132
133 Ok(PolicyRule {
134 action,
135 target,
136 condition,
137 })
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn empty_policy_fails() {
146 assert!(parse_policy("").is_err());
147 }
148
149 #[test]
150 fn whitespace_only_fails() {
151 assert!(parse_policy(" \n \n ").is_err());
152 }
153
154 #[test]
155 fn simple_string_without_policy_header_errors() {
156 let err = parse_policy("no_external_calls").expect_err("must fail");
159 let msg = format!("{err}");
160 assert!(
161 msg.contains("must begin with a 'policy <name>' header"),
162 "got {msg}"
163 );
164 }
165
166 #[test]
167 fn structured_policy_parses() {
168 let input = r#"
169policy hipaa_guard
170 deny network_access when patient_data
171 allow read /approved/*
172 audit all_operations
173end
174"#;
175 let result = parse_policy(input).unwrap();
176 assert_eq!(result.name, "hipaa_guard");
177 assert_eq!(result.rules.len(), 3);
178
179 assert_eq!(result.rules[0].action, "deny");
180 assert_eq!(result.rules[0].target, "network_access");
181 assert_eq!(result.rules[0].condition.as_deref(), Some("patient_data"));
182
183 assert_eq!(result.rules[1].action, "allow");
184 assert_eq!(result.rules[1].target, "read /approved/*");
185 assert!(result.rules[1].condition.is_none());
186
187 assert_eq!(result.rules[2].action, "audit");
188 assert_eq!(result.rules[2].target, "all_operations");
189 }
190
191 #[test]
192 fn missing_policy_name_fails() {
193 assert!(parse_policy("policy\n allow all\nend").is_err());
194 }
195
196 #[test]
197 fn no_rules_fails() {
198 assert!(parse_policy("policy empty\nend").is_err());
199 }
200
201 #[test]
202 fn unknown_action_fails() {
203 let input = "policy test\n explode everything\nend";
204 assert!(parse_policy(input).is_err());
205 }
206
207 #[test]
208 fn comments_are_skipped() {
209 let input = r#"
210policy test
211 # This is a comment
212 allow read
213 // Another comment
214 deny write
215end
216"#;
217 let result = parse_policy(input).unwrap();
218 assert_eq!(result.rules.len(), 2);
219 }
220
221 #[test]
222 fn condition_parsing() {
223 let input = "policy gate\n limit api_calls when rate > 100\nend";
224 let result = parse_policy(input).unwrap();
225 assert_eq!(result.rules[0].condition.as_deref(), Some("rate > 100"));
226 }
227}