1use std::path::PathBuf;
7use thiserror::Error;
8
9use crate::types::HookEvent;
10
11#[derive(Debug, Error)]
13pub enum Error {
14 #[error(transparent)]
16 Settings(#[from] SettingsError),
17
18 #[error(transparent)]
20 Registry(#[from] RegistryError),
21
22 #[error(transparent)]
24 Hook(#[from] HookError),
25}
26
27#[derive(Debug, Error)]
29pub enum SettingsError {
30 #[error("Settings file not found: {0}")]
32 NotFound(PathBuf),
33
34 #[error("Failed to read settings: {0}")]
36 Io(#[source] std::io::Error),
37
38 #[error("Failed to parse settings: {0}")]
40 Parse(String),
41
42 #[error("Failed to write settings atomically: {path} - Safety copy at: {temp_path}")]
44 WriteAtomic {
45 path: PathBuf,
47 temp_path: PathBuf,
49 },
50}
51
52#[derive(Debug, Error)]
54pub enum RegistryError {
55 #[error("Failed to read registry: {0}")]
57 Io(#[source] std::io::Error),
58
59 #[error("Failed to parse registry: {0}")]
61 Parse(String),
62
63 #[error("Failed to write registry: {0}")]
65 Write(String),
66}
67
68#[derive(Debug, Error)]
70pub enum HookError {
71 #[error("Hook already exists: {event:?} - {command}")]
73 AlreadyExists {
74 event: HookEvent,
76 command: String,
78 },
79
80 #[error("Hook not managed by claude-hooks: {event:?} - {command}")]
82 NotManaged {
83 event: HookEvent,
85 command: String,
87 },
88
89 #[error("Invalid hook handler: {0}")]
91 InvalidHandler(String),
92}
93
94pub type Result<T> = std::result::Result<T, Error>;
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn test_settings_error_not_found_display() {
103 let path = PathBuf::from("/home/user/.claude/settings.json");
104 let err = SettingsError::NotFound(path);
105 let display = format!("{}", err);
106 assert!(
107 display.contains("/home/user/.claude/settings.json"),
108 "Error should contain path"
109 );
110 }
111
112 #[test]
113 fn test_settings_error_write_atomic_display() {
114 let path = PathBuf::from("/home/user/.claude/settings.json");
115 let temp_path = PathBuf::from("/home/user/.claude/settings.json.tmp.20260203-143022");
116 let err = SettingsError::WriteAtomic { path, temp_path };
117 let display = format!("{}", err);
118 assert!(
119 display.contains("/home/user/.claude/settings.json"),
120 "Error should contain path"
121 );
122 assert!(
123 display.contains("settings.json.tmp.20260203-143022"),
124 "Error should contain temp path"
125 );
126 assert!(display.contains("Safety copy"), "Error should mention safety copy");
127 }
128
129 #[test]
130 fn test_hook_error_already_exists_display() {
131 let err = HookError::AlreadyExists {
132 event: HookEvent::Stop,
133 command: "/path/to/stop.sh".to_string(),
134 };
135 let display = format!("{}", err);
136 assert!(display.contains("Stop"), "Error should contain event");
137 assert!(
138 display.contains("/path/to/stop.sh"),
139 "Error should contain command"
140 );
141 }
142
143 #[test]
144 fn test_hook_error_not_managed_display() {
145 let err = HookError::NotManaged {
146 event: HookEvent::SessionStart,
147 command: "/path/to/start.sh".to_string(),
148 };
149 let display = format!("{}", err);
150 assert!(display.contains("SessionStart"), "Error should contain event");
151 assert!(
152 display.contains("/path/to/start.sh"),
153 "Error should contain command"
154 );
155 assert!(
156 display.contains("not managed"),
157 "Error should indicate not managed"
158 );
159 }
160
161 #[test]
162 fn test_registry_error_parse_display() {
163 let err = RegistryError::Parse("Invalid JSON at line 5".to_string());
164 let display = format!("{}", err);
165 assert!(
166 display.contains("Invalid JSON at line 5"),
167 "Error should contain parse details"
168 );
169 }
170}