use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PermissionBehavior {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub enum PermissionUpdateDestination {
UserSettings,
ProjectSettings,
LocalSettings,
Session,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PermissionRuleValue {
#[serde(rename = "toolName")]
pub tool_name: String,
#[serde(rename = "ruleContent", skip_serializing_if = "Option::is_none")]
pub rule_content: Option<String>,
}
impl PermissionRuleValue {
pub fn new(tool_name: impl Into<String>) -> Self {
Self {
tool_name: tool_name.into(),
rule_content: None,
}
}
pub fn with_rule_content(mut self, content: impl Into<String>) -> Self {
self.rule_content = Some(content.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AddRulesUpdate {
pub rules: Vec<PermissionRuleValue>,
pub behavior: PermissionBehavior,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ReplaceRulesUpdate {
pub rules: Vec<PermissionRuleValue>,
pub behavior: PermissionBehavior,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RemoveRulesUpdate {
pub rules: Vec<PermissionRuleValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SetModeUpdate {
pub mode: crate::types::PermissionMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct AddDirectoriesUpdate {
pub directories: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct RemoveDirectoriesUpdate {
pub directories: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub destination: Option<PermissionUpdateDestination>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum PermissionUpdate {
AddRules(AddRulesUpdate),
ReplaceRules(ReplaceRulesUpdate),
RemoveRules(RemoveRulesUpdate),
SetMode(SetModeUpdate),
AddDirectories(AddDirectoriesUpdate),
RemoveDirectories(RemoveDirectoriesUpdate),
}
impl PermissionUpdate {
pub fn add_rules(rules: Vec<PermissionRuleValue>, behavior: PermissionBehavior) -> Self {
Self::AddRules(AddRulesUpdate {
rules,
behavior,
destination: None,
})
}
pub fn replace_rules(rules: Vec<PermissionRuleValue>, behavior: PermissionBehavior) -> Self {
Self::ReplaceRules(ReplaceRulesUpdate {
rules,
behavior,
destination: None,
})
}
pub fn remove_rules(rules: Vec<PermissionRuleValue>) -> Self {
Self::RemoveRules(RemoveRulesUpdate {
rules,
destination: None,
})
}
pub fn set_mode(mode: crate::types::PermissionMode) -> Self {
Self::SetMode(SetModeUpdate {
mode,
destination: None,
})
}
pub fn add_directories(directories: Vec<String>) -> Self {
Self::AddDirectories(AddDirectoriesUpdate {
directories,
destination: None,
})
}
pub fn remove_directories(directories: Vec<String>) -> Self {
Self::RemoveDirectories(RemoveDirectoriesUpdate {
directories,
destination: None,
})
}
pub fn with_destination(mut self, destination: PermissionUpdateDestination) -> Self {
match &mut self {
Self::AddRules(u) => u.destination = Some(destination),
Self::ReplaceRules(u) => u.destination = Some(destination),
Self::RemoveRules(u) => u.destination = Some(destination),
Self::SetMode(u) => u.destination = Some(destination),
Self::AddDirectories(u) => u.destination = Some(destination),
Self::RemoveDirectories(u) => u.destination = Some(destination),
}
self
}
pub fn destination(&self) -> Option<PermissionUpdateDestination> {
match self {
Self::AddRules(u) => u.destination,
Self::ReplaceRules(u) => u.destination,
Self::RemoveRules(u) => u.destination,
Self::SetMode(u) => u.destination,
Self::AddDirectories(u) => u.destination,
Self::RemoveDirectories(u) => u.destination,
}
}
pub fn validate(&self) -> Result<(), String> {
match self {
Self::AddRules(u) => {
if u.rules.is_empty() {
return Err("AddRules update must have at least one rule".to_string());
}
for rule in &u.rules {
if rule.tool_name.is_empty() {
return Err("Rule tool_name cannot be empty".to_string());
}
}
Ok(())
}
Self::ReplaceRules(u) => {
if u.rules.is_empty() {
return Err("ReplaceRules update must have at least one rule".to_string());
}
for rule in &u.rules {
if rule.tool_name.is_empty() {
return Err("Rule tool_name cannot be empty".to_string());
}
}
Ok(())
}
Self::RemoveRules(u) => {
if u.rules.is_empty() {
return Err("RemoveRules update must have at least one rule".to_string());
}
for rule in &u.rules {
if rule.tool_name.is_empty() {
return Err("Rule tool_name cannot be empty".to_string());
}
}
Ok(())
}
Self::SetMode(_) => Ok(()),
Self::AddDirectories(u) => {
if u.directories.is_empty() {
return Err(
"AddDirectories update must have at least one directory".to_string()
);
}
for dir in &u.directories {
if dir.is_empty() {
return Err("Directory path cannot be empty".to_string());
}
}
Ok(())
}
Self::RemoveDirectories(u) => {
if u.directories.is_empty() {
return Err(
"RemoveDirectories update must have at least one directory".to_string()
);
}
for dir in &u.directories {
if dir.is_empty() {
return Err("Directory path cannot be empty".to_string());
}
}
Ok(())
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::PermissionMode;
#[test]
fn test_permission_rule_value() {
let rule = PermissionRuleValue::new("bash").with_rule_content(".*");
assert_eq!(rule.tool_name, "bash");
assert_eq!(rule.rule_content, Some(".*".to_string()));
}
#[test]
fn test_add_rules_update() {
let rules = vec![
PermissionRuleValue::new("bash"),
PermissionRuleValue::new("file_editor"),
];
let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow)
.with_destination(PermissionUpdateDestination::Session);
assert!(matches!(update, PermissionUpdate::AddRules(_)));
assert_eq!(
update.destination(),
Some(PermissionUpdateDestination::Session)
);
assert!(update.validate().is_ok());
}
#[test]
fn test_set_mode_update() {
let update = PermissionUpdate::set_mode(PermissionMode::BypassPermissions)
.with_destination(PermissionUpdateDestination::ProjectSettings);
assert!(matches!(update, PermissionUpdate::SetMode(_)));
assert!(update.validate().is_ok());
}
#[test]
fn test_add_directories_update() {
let dirs = vec!["/home/user/project".to_string()];
let update = PermissionUpdate::add_directories(dirs);
assert!(matches!(update, PermissionUpdate::AddDirectories(_)));
assert!(update.validate().is_ok());
}
#[test]
fn test_validation_empty_rules() {
let update = PermissionUpdate::add_rules(vec![], PermissionBehavior::Allow);
assert!(update.validate().is_err());
}
#[test]
fn test_validation_empty_directories() {
let update = PermissionUpdate::add_directories(vec![]);
assert!(update.validate().is_err());
}
#[test]
fn test_validation_empty_tool_name() {
let rules = vec![PermissionRuleValue::new("")];
let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow);
assert!(update.validate().is_err());
}
#[test]
fn test_serialization_add_rules() {
let rules = vec![PermissionRuleValue::new("bash")];
let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow);
let json = serde_json::to_string(&update).unwrap();
assert!(json.contains("addRules"));
assert!(json.contains("bash"));
let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
assert_eq!(update, deserialized);
}
#[test]
fn test_serialization_set_mode() {
let update = PermissionUpdate::set_mode(PermissionMode::AcceptEdits);
let json = serde_json::to_string(&update).unwrap();
assert!(json.contains("setMode"));
let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
assert_eq!(update, deserialized);
}
#[test]
fn test_serialization_with_destination() {
let rules = vec![PermissionRuleValue::new("bash")];
let update = PermissionUpdate::add_rules(rules, PermissionBehavior::Allow)
.with_destination(PermissionUpdateDestination::Session);
let json = serde_json::to_string(&update).unwrap();
assert!(json.contains("destination"));
assert!(json.contains("session"));
let deserialized: PermissionUpdate = serde_json::from_str(&json).unwrap();
assert_eq!(update, deserialized);
}
}