1use std::sync::Arc;
15
16#[derive(Clone, Debug)]
18pub struct Hook {
19 pub tool: String,
21 pub path_contains: String,
23 pub exclude: Vec<String>,
25 pub message: String,
27}
28
29#[derive(Clone, Debug, Default)]
31pub struct HookRegistry {
32 hooks: Vec<Hook>,
33}
34
35impl HookRegistry {
36 pub fn new() -> Self {
37 Self { hooks: Vec::new() }
38 }
39
40 pub fn add(&mut self, hook: Hook) -> &mut Self {
42 self.hooks.push(hook);
43 self
44 }
45
46 pub fn check(&self, tool_name: &str, path: &str) -> Vec<String> {
49 let norm = path.trim_start_matches('/').to_lowercase();
50 self.hooks
51 .iter()
52 .filter(|h| {
53 (h.tool == tool_name || h.tool == "*")
54 && norm.contains(&h.path_contains)
55 && !h.exclude.iter().any(|ex| norm.contains(ex))
56 })
57 .map(|h| h.message.clone())
58 .collect()
59 }
60
61 pub fn len(&self) -> usize {
62 self.hooks.len()
63 }
64
65 pub fn is_empty(&self) -> bool {
66 self.hooks.is_empty()
67 }
68}
69
70pub type Shared = Arc<HookRegistry>;
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn matches_tool_and_path() {
79 let mut reg = HookRegistry::new();
80 reg.add(Hook {
81 tool: "write".into(),
82 path_contains: "cards/".into(),
83 exclude: vec!["template".into()],
84 message: "Update thread".into(),
85 });
86
87 assert_eq!(
88 reg.check("write", "distill/cards/article.md"),
89 vec!["Update thread"]
90 );
91 assert!(reg.check("write", "distill/cards/_template.md").is_empty());
92 assert!(reg.check("read", "distill/cards/article.md").is_empty());
93 assert!(reg.check("write", "contacts/john.json").is_empty());
94 }
95
96 #[test]
97 fn wildcard_tool() {
98 let mut reg = HookRegistry::new();
99 reg.add(Hook {
100 tool: "*".into(),
101 path_contains: "inbox/".into(),
102 exclude: vec![],
103 message: "Inbox touched".into(),
104 });
105
106 assert_eq!(reg.check("read", "inbox/msg.txt"), vec!["Inbox touched"]);
107 assert_eq!(reg.check("delete", "inbox/msg.txt"), vec!["Inbox touched"]);
108 }
109
110 #[test]
111 fn empty_registry() {
112 let reg = HookRegistry::new();
113 assert!(reg.check("write", "anything").is_empty());
114 assert!(reg.is_empty());
115 }
116}