use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HookEvent {
Start,
Stop,
BeforePrompt,
AfterPrompt,
BeforeToolUse,
AfterToolUse,
BeforeEdit,
AfterEdit,
BeforeRevert,
AfterRevert,
BeforeRun,
AfterRun,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HookHandler {
#[serde(rename = "type")]
pub r#type: String,
pub command: String,
pub matcher: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "async")]
pub r#async: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryEntry {
pub event: HookEvent,
pub matcher: String,
#[serde(rename = "type")]
pub r#type: String,
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none", rename = "async")]
pub r#async: Option<bool>,
pub scope: String,
pub enabled: bool,
pub added_at: String,
pub installed_by: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub optional: Option<bool>,
}
impl RegistryEntry {
pub fn matches(&self, event: HookEvent, command: &str) -> bool {
self.event == event && self.command == command
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListEntry {
pub event: HookEvent,
pub handler: HookHandler,
pub managed: bool,
pub metadata: Option<RegistryMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegistryMetadata {
pub added_at: String,
pub installed_by: String,
pub description: Option<String>,
pub reason: Option<String>,
pub optional: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_event_serialization() {
let event = HookEvent::Stop;
let json = serde_json::to_string(&event).expect("serialization failed");
assert_eq!(json, r#""Stop""#);
}
#[test]
fn test_hook_event_deserialization() {
let json = r#""Start""#;
let event: HookEvent = serde_json::from_str(json).expect("deserialization failed");
assert_eq!(event, HookEvent::Start);
}
#[test]
fn test_all_hook_events_serialize() {
let events = vec![
(HookEvent::Start, r#""Start""#),
(HookEvent::Stop, r#""Stop""#),
(HookEvent::BeforePrompt, r#""BeforePrompt""#),
(HookEvent::AfterPrompt, r#""AfterPrompt""#),
(HookEvent::BeforeToolUse, r#""BeforeToolUse""#),
(HookEvent::AfterToolUse, r#""AfterToolUse""#),
(HookEvent::BeforeEdit, r#""BeforeEdit""#),
(HookEvent::AfterEdit, r#""AfterEdit""#),
(HookEvent::BeforeRevert, r#""BeforeRevert""#),
(HookEvent::AfterRevert, r#""AfterRevert""#),
(HookEvent::BeforeRun, r#""BeforeRun""#),
(HookEvent::AfterRun, r#""AfterRun""#),
];
for (event, expected) in events {
let json = serde_json::to_string(&event).expect("serialization failed");
assert_eq!(json, expected, "Event {:?} serialized incorrectly", event);
}
}
#[test]
fn test_hook_handler_roundtrip() {
let handler = HookHandler {
r#type: "command".to_string(),
command: "/path/to/stop.sh".to_string(),
matcher: String::new(),
timeout: Some(600),
r#async: None,
};
let json = serde_json::to_string(&handler).expect("serialization failed");
let deserialized: HookHandler =
serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(handler, deserialized);
}
#[test]
fn test_hook_handler_optional_fields() {
let handler_full = HookHandler {
r#type: "command".to_string(),
command: "/path/to/script.sh".to_string(),
matcher: String::new(),
timeout: Some(300),
r#async: Some(true),
};
let json = serde_json::to_string(&handler_full).expect("serialization failed");
let deserialized: HookHandler =
serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(handler_full, deserialized);
let handler_minimal = HookHandler {
r#type: "command".to_string(),
command: "/path/to/script.sh".to_string(),
matcher: String::new(),
timeout: None,
r#async: None,
};
let json = serde_json::to_string(&handler_minimal).expect("serialization failed");
let deserialized: HookHandler =
serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(handler_minimal, deserialized);
}
#[test]
fn test_registry_entry_roundtrip() {
let entry = RegistryEntry {
event: HookEvent::Stop,
matcher: String::new(),
r#type: "command".to_string(),
command: "/path/to/stop.sh".to_string(),
timeout: Some(600),
r#async: None,
scope: "user".to_string(),
enabled: true,
added_at: "20260203-143022".to_string(),
installed_by: "acd".to_string(),
description: Some("Test hook".to_string()),
reason: Some("Testing".to_string()),
optional: Some(false),
};
let json = serde_json::to_string(&entry).expect("serialization failed");
let deserialized: RegistryEntry =
serde_json::from_str(&json).expect("deserialization failed");
assert_eq!(entry, deserialized);
}
#[test]
fn test_registry_entry_matches() {
let entry = RegistryEntry {
event: HookEvent::Stop,
matcher: String::new(),
r#type: "command".to_string(),
command: "/path/to/stop.sh".to_string(),
timeout: None,
r#async: None,
scope: "user".to_string(),
enabled: true,
added_at: "20260203-143022".to_string(),
installed_by: "acd".to_string(),
description: None,
reason: None,
optional: None,
};
assert!(entry.matches(HookEvent::Stop, "/path/to/stop.sh"));
assert!(!entry.matches(HookEvent::Stop, "/different/path"));
assert!(!entry.matches(HookEvent::Start, "/path/to/stop.sh"));
assert!(!entry.matches(HookEvent::Start, "/different/path"));
}
}