1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum PermissionBehavior {
12 Allow,
14
15 Deny,
17
18 Ask,
20}
21
22#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "camelCase")]
25pub enum PermissionUpdateDestination {
26 UserSettings,
28
29 ProjectSettings,
31
32 LocalSettings,
34
35 Session,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
41pub struct PermissionRuleValue {
42 #[serde(rename = "toolName")]
44 pub tool_name: String,
45
46 #[serde(rename = "ruleContent", skip_serializing_if = "Option::is_none")]
48 pub rule_content: Option<String>,
49}
50
51impl PermissionRuleValue {
52 pub fn new(tool_name: impl Into<String>) -> Self {
54 Self {
55 tool_name: tool_name.into(),
56 rule_content: None,
57 }
58 }
59
60 pub fn with_rule_content(mut self, content: impl Into<String>) -> Self {
62 self.rule_content = Some(content.into());
63 self
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct AddRulesUpdate {
70 pub rules: Vec<PermissionRuleValue>,
72
73 pub behavior: PermissionBehavior,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub destination: Option<PermissionUpdateDestination>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct ReplaceRulesUpdate {
84 pub rules: Vec<PermissionRuleValue>,
86
87 pub behavior: PermissionBehavior,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
92 pub destination: Option<PermissionUpdateDestination>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct RemoveRulesUpdate {
98 pub rules: Vec<PermissionRuleValue>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub destination: Option<PermissionUpdateDestination>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
108pub struct SetModeUpdate {
109 pub mode: crate::types::PermissionMode,
111
112 #[serde(skip_serializing_if = "Option::is_none")]
114 pub destination: Option<PermissionUpdateDestination>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119pub struct AddDirectoriesUpdate {
120 pub directories: Vec<String>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub destination: Option<PermissionUpdateDestination>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
130pub struct RemoveDirectoriesUpdate {
131 pub directories: Vec<String>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub destination: Option<PermissionUpdateDestination>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
144#[serde(tag = "type", rename_all = "camelCase")]
145pub enum PermissionUpdate {
146 AddRules(AddRulesUpdate),
148
149 ReplaceRules(ReplaceRulesUpdate),
151
152 RemoveRules(RemoveRulesUpdate),
154
155 SetMode(SetModeUpdate),
157
158 AddDirectories(AddDirectoriesUpdate),
160
161 RemoveDirectories(RemoveDirectoriesUpdate),
163}
164
165impl PermissionUpdate {
166 pub fn add_rules(rules: Vec<PermissionRuleValue>, behavior: PermissionBehavior) -> Self {
168 Self::AddRules(AddRulesUpdate {
169 rules,
170 behavior,
171 destination: None,
172 })
173 }
174
175 pub fn replace_rules(rules: Vec<PermissionRuleValue>, behavior: PermissionBehavior) -> Self {
177 Self::ReplaceRules(ReplaceRulesUpdate {
178 rules,
179 behavior,
180 destination: None,
181 })
182 }
183
184 pub fn remove_rules(rules: Vec<PermissionRuleValue>) -> Self {
186 Self::RemoveRules(RemoveRulesUpdate {
187 rules,
188 destination: None,
189 })
190 }
191
192 pub fn set_mode(mode: crate::types::PermissionMode) -> Self {
194 Self::SetMode(SetModeUpdate {
195 mode,
196 destination: None,
197 })
198 }
199
200 pub fn add_directories(directories: Vec<String>) -> Self {
202 Self::AddDirectories(AddDirectoriesUpdate {
203 directories,
204 destination: None,
205 })
206 }
207
208 pub fn remove_directories(directories: Vec<String>) -> Self {
210 Self::RemoveDirectories(RemoveDirectoriesUpdate {
211 directories,
212 destination: None,
213 })
214 }
215
216 pub fn with_destination(mut self, destination: PermissionUpdateDestination) -> Self {
218 match &mut self {
219 Self::AddRules(u) => u.destination = Some(destination),
220 Self::ReplaceRules(u) => u.destination = Some(destination),
221 Self::RemoveRules(u) => u.destination = Some(destination),
222 Self::SetMode(u) => u.destination = Some(destination),
223 Self::AddDirectories(u) => u.destination = Some(destination),
224 Self::RemoveDirectories(u) => u.destination = Some(destination),
225 }
226 self
227 }
228
229 pub fn destination(&self) -> Option<PermissionUpdateDestination> {
231 match self {
232 Self::AddRules(u) => u.destination,
233 Self::ReplaceRules(u) => u.destination,
234 Self::RemoveRules(u) => u.destination,
235 Self::SetMode(u) => u.destination,
236 Self::AddDirectories(u) => u.destination,
237 Self::RemoveDirectories(u) => u.destination,
238 }
239 }
240
241 pub fn validate(&self) -> Result<(), String> {
245 match self {
246 Self::AddRules(u) => {
247 if u.rules.is_empty() {
248 return Err("AddRules update must have at least one rule".to_string());
249 }
250 for rule in &u.rules {
251 if rule.tool_name.is_empty() {
252 return Err("Rule tool_name cannot be empty".to_string());
253 }
254 }
255 Ok(())
256 }
257 Self::ReplaceRules(u) => {
258 if u.rules.is_empty() {
259 return Err("ReplaceRules update must have at least one rule".to_string());
260 }
261 for rule in &u.rules {
262 if rule.tool_name.is_empty() {
263 return Err("Rule tool_name cannot be empty".to_string());
264 }
265 }
266 Ok(())
267 }
268 Self::RemoveRules(u) => {
269 if u.rules.is_empty() {
270 return Err("RemoveRules update must have at least one rule".to_string());
271 }
272 for rule in &u.rules {
273 if rule.tool_name.is_empty() {
274 return Err("Rule tool_name cannot be empty".to_string());
275 }
276 }
277 Ok(())
278 }
279 Self::SetMode(_) => Ok(()),
280 Self::AddDirectories(u) => {
281 if u.directories.is_empty() {
282 return Err(
283 "AddDirectories update must have at least one directory".to_string()
284 );
285 }
286 for dir in &u.directories {
287 if dir.is_empty() {
288 return Err("Directory path cannot be empty".to_string());
289 }
290 }
291 Ok(())
292 }
293 Self::RemoveDirectories(u) => {
294 if u.directories.is_empty() {
295 return Err(
296 "RemoveDirectories update must have at least one directory".to_string()
297 );
298 }
299 for dir in &u.directories {
300 if dir.is_empty() {
301 return Err("Directory path cannot be empty".to_string());
302 }
303 }
304 Ok(())
305 }
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use crate::types::PermissionMode;
314
315 #[test]
316 fn test_permission_rule_value() {
317 let rule = PermissionRuleValue::new("bash").with_rule_content(".*");
318
319 assert_eq!(rule.tool_name, "bash");
320 assert_eq!(rule.rule_content, Some(".*".to_string()));
321 }
322
323 #[test]
324 fn test_add_rules_update() {
325 let rules = vec![
326 PermissionRuleValue::new("bash"),
327 PermissionRuleValue::new("file_editor"),
328 ];
329
330 let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow)
331 .with_destination(PermissionUpdateDestination::Session);
332
333 assert!(matches!(update, PermissionUpdate::AddRules(_)));
334 assert_eq!(
335 update.destination(),
336 Some(PermissionUpdateDestination::Session)
337 );
338 assert!(update.validate().is_ok());
339 }
340
341 #[test]
342 fn test_set_mode_update() {
343 let update = PermissionUpdate::set_mode(PermissionMode::BypassPermissions)
344 .with_destination(PermissionUpdateDestination::ProjectSettings);
345
346 assert!(matches!(update, PermissionUpdate::SetMode(_)));
347 assert!(update.validate().is_ok());
348 }
349
350 #[test]
351 fn test_add_directories_update() {
352 let dirs = vec!["/home/user/project".to_string()];
353 let update = PermissionUpdate::add_directories(dirs);
354
355 assert!(matches!(update, PermissionUpdate::AddDirectories(_)));
356 assert!(update.validate().is_ok());
357 }
358
359 #[test]
360 fn test_validation_empty_rules() {
361 let update = PermissionUpdate::add_rules(vec![], PermissionBehavior::Allow);
362 assert!(update.validate().is_err());
363 }
364
365 #[test]
366 fn test_validation_empty_directories() {
367 let update = PermissionUpdate::add_directories(vec![]);
368 assert!(update.validate().is_err());
369 }
370
371 #[test]
372 fn test_validation_empty_tool_name() {
373 let rules = vec![PermissionRuleValue::new("")];
374 let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow);
375 assert!(update.validate().is_err());
376 }
377
378 #[test]
379 fn test_serialization_add_rules() {
380 let rules = vec![PermissionRuleValue::new("bash")];
381 let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow);
382
383 let json = serde_json::to_string(&update).unwrap();
384 assert!(json.contains("addRules"));
385 assert!(json.contains("bash"));
386
387 let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
388 assert_eq!(update, deserialized);
389 }
390
391 #[test]
392 fn test_serialization_set_mode() {
393 let update = PermissionUpdate::set_mode(PermissionMode::AcceptEdits);
394
395 let json = serde_json::to_string(&update).unwrap();
396 assert!(json.contains("setMode"));
397
398 let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
399 assert_eq!(update, deserialized);
400 }
401
402 #[test]
403 fn test_serialization_with_destination() {
404 let rules = vec![PermissionRuleValue::new("bash")];
405 let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow)
406 .with_destination(PermissionUpdateDestination::Session);
407
408 let json = serde_json::to_string(&update).unwrap();
409 assert!(json.contains("destination"));
410 assert!(json.contains("session"));
411
412 let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
413 assert_eq!(update, deserialized);
414 }
415}