use serde::Deserialize;
use serde::Serialize;
use serde::de::Deserializer;
fn deserialize_tool_ref_opt<'de, D>(deserializer: D) -> Result<Option<PermissionToolRef>, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<serde_json::Value> = Option::deserialize(deserializer)?;
let Some(value) = value else {
return Ok(None);
};
match value {
serde_json::Value::Object(map) => {
let message_id = map.get("messageID").and_then(|v| v.as_str());
let call_id = map.get("callID").and_then(|v| v.as_str());
match (message_id, call_id) {
(Some(mid), Some(cid)) => Ok(Some(PermissionToolRef {
message_id: mid.to_owned(),
call_id: cid.to_owned(),
})),
_ => Ok(None),
}
}
_ => Ok(None),
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PermissionAction {
Allow,
Deny,
Ask,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PermissionRule {
pub permission: String,
pub pattern: String,
pub action: PermissionAction,
}
pub type Ruleset = Vec<PermissionRule>;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PermissionToolRef {
#[serde(rename = "messageID")]
pub message_id: String,
#[serde(rename = "callID")]
pub call_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionRequest {
pub id: String,
#[serde(rename = "sessionID")]
pub session_id: String,
pub permission: String,
pub patterns: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub always: Vec<String>,
#[serde(
default,
deserialize_with = "deserialize_tool_ref_opt",
skip_serializing_if = "Option::is_none"
)]
pub tool: Option<PermissionToolRef>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PermissionReply {
Once,
Always,
Reject,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionReplyRequest {
pub reply: PermissionReply,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_action_serialize() {
assert_eq!(
serde_json::to_string(&PermissionAction::Allow).unwrap(),
r#""allow""#
);
assert_eq!(
serde_json::to_string(&PermissionAction::Deny).unwrap(),
r#""deny""#
);
assert_eq!(
serde_json::to_string(&PermissionAction::Ask).unwrap(),
r#""ask""#
);
}
#[test]
fn test_permission_rule_deserialize() {
let json = r#"{"permission":"file.read","pattern":"**/*.rs","action":"allow"}"#;
let rule: PermissionRule = serde_json::from_str(json).unwrap();
assert_eq!(rule.permission, "file.read");
assert_eq!(rule.pattern, "**/*.rs");
assert_eq!(rule.action, PermissionAction::Allow);
}
#[test]
fn test_ruleset_deserialize() {
let json = r#"[
{"permission":"file.read","pattern":"**/*.rs","action":"allow"},
{"permission":"bash.execute","pattern":"*","action":"ask"}
]"#;
let ruleset: Ruleset = serde_json::from_str(json).unwrap();
assert_eq!(ruleset.len(), 2);
assert_eq!(ruleset[0].action, PermissionAction::Allow);
assert_eq!(ruleset[1].action, PermissionAction::Ask);
}
#[test]
fn test_permission_request_deserialize() {
let json = r#"{
"id": "req-123",
"sessionID": "sess-456",
"permission": "file.write",
"patterns": ["src/*.rs", "lib/*.rs"],
"always": ["src/*.rs"],
"tool": {"messageID": "msg-1", "callID": "call-1"}
}"#;
let req: PermissionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.id, "req-123");
assert_eq!(req.session_id, "sess-456");
assert_eq!(req.permission, "file.write");
assert_eq!(req.patterns.len(), 2);
assert_eq!(req.always.len(), 1);
assert!(req.tool.is_some());
let tool = req.tool.unwrap();
assert_eq!(tool.message_id, "msg-1");
assert_eq!(tool.call_id, "call-1");
}
#[test]
fn test_permission_request_minimal() {
let json = r#"{
"id": "req-123",
"sessionID": "sess-456",
"permission": "file.read",
"patterns": ["**/*"]
}"#;
let req: PermissionRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.id, "req-123");
assert!(req.always.is_empty());
assert!(req.tool.is_none());
assert!(req.metadata.is_none());
}
#[test]
fn test_permission_reply_serialize() {
assert_eq!(
serde_json::to_string(&PermissionReply::Once).unwrap(),
r#""once""#
);
assert_eq!(
serde_json::to_string(&PermissionReply::Always).unwrap(),
r#""always""#
);
assert_eq!(
serde_json::to_string(&PermissionReply::Reject).unwrap(),
r#""reject""#
);
}
#[test]
fn test_permission_reply_request_serialize() {
let req = PermissionReplyRequest {
reply: PermissionReply::Always,
message: Some("User approved".to_string()),
};
let json = serde_json::to_string(&req).unwrap();
assert!(json.contains(r#""reply":"always""#));
assert!(json.contains(r#""message":"User approved""#));
}
#[test]
fn test_permission_reply_request_minimal() {
let json = r#"{"reply":"once"}"#;
let req: PermissionReplyRequest = serde_json::from_str(json).unwrap();
assert_eq!(req.reply, PermissionReply::Once);
assert!(req.message.is_none());
}
#[test]
fn test_permission_request_tool_absent_deserializes_to_none() {
let json = r#"{
"id": "perm_test123",
"sessionID": "sess_abc",
"permission": "file.write",
"patterns": ["/tmp/**"]
}"#;
let req: PermissionRequest =
serde_json::from_str(json).expect("should deserialize without tool field");
assert!(
req.tool.is_none(),
"absent tool field should deserialize to None"
);
}
#[test]
fn test_permission_request_tool_null_deserializes_to_none() {
let json = r#"{
"id": "perm_test123",
"sessionID": "sess_abc",
"permission": "file.write",
"patterns": ["/tmp/**"],
"tool": null
}"#;
let req: PermissionRequest =
serde_json::from_str(json).expect("should deserialize with null tool");
assert!(req.tool.is_none(), "null tool should deserialize to None");
}
#[test]
fn test_permission_request_tool_empty_object_deserializes_to_none() {
let json = r#"{
"id": "perm_test123",
"sessionID": "sess_abc",
"permission": "file.write",
"patterns": ["/tmp/**"],
"tool": {}
}"#;
let req: PermissionRequest =
serde_json::from_str(json).expect("should deserialize with empty tool object");
assert!(
req.tool.is_none(),
"empty object tool should deserialize to None"
);
}
#[test]
fn test_permission_request_tool_valid_object_deserializes_to_some() {
let json = r#"{
"id": "perm_test123",
"sessionID": "sess_abc",
"permission": "file.write",
"patterns": ["/tmp/**"],
"tool": {
"messageID": "msg_xyz",
"callID": "call_789"
}
}"#;
let req: PermissionRequest =
serde_json::from_str(json).expect("should deserialize with valid tool");
let tool = req.tool.expect("tool should be Some");
assert_eq!(tool.message_id, "msg_xyz");
assert_eq!(tool.call_id, "call_789");
}
#[test]
fn test_permission_request_tool_partial_object_deserializes_to_none() {
let json = r#"{
"id": "perm_test123",
"sessionID": "sess_abc",
"permission": "file.write",
"patterns": ["/tmp/**"],
"tool": {
"messageID": "msg_xyz"
}
}"#;
let req: PermissionRequest =
serde_json::from_str(json).expect("should deserialize with partial tool");
assert!(
req.tool.is_none(),
"partial tool object (missing callId) should deserialize to None"
);
}
#[test]
fn test_permission_request_tool_partial_object_missing_message_id() {
let json = r#"{
"id": "perm_test123",
"sessionID": "sess_abc",
"permission": "file.write",
"patterns": ["/tmp/**"],
"tool": {
"callID": "call_789"
}
}"#;
let req: PermissionRequest =
serde_json::from_str(json).expect("should deserialize with partial tool");
assert!(
req.tool.is_none(),
"partial tool object (missing messageId) should deserialize to None"
);
}
}