Skip to main content

claude_hooks/
error.rs

1//! Error types for claude-hooks
2//!
3//! This module defines the error hierarchy using thiserror for structured
4//! error handling across settings, registry, and hook operations.
5
6use std::path::PathBuf;
7use thiserror::Error;
8
9use crate::types::HookEvent;
10
11/// Top-level error type
12#[derive(Debug, Error)]
13pub enum Error {
14    /// Settings file error
15    #[error(transparent)]
16    Settings(#[from] SettingsError),
17
18    /// Registry error
19    #[error(transparent)]
20    Registry(#[from] RegistryError),
21
22    /// Hook logic error
23    #[error(transparent)]
24    Hook(#[from] HookError),
25}
26
27/// Settings file errors
28#[derive(Debug, Error)]
29pub enum SettingsError {
30    /// Settings file not found
31    #[error("Settings file not found: {0}")]
32    NotFound(PathBuf),
33
34    /// I/O error reading settings
35    #[error("Failed to read settings: {0}")]
36    Io(#[source] std::io::Error),
37
38    /// Failed to parse settings file
39    #[error("Failed to parse settings: {0}")]
40    Parse(String),
41
42    /// Failed to write settings atomically
43    #[error("Failed to write settings atomically: {path} - Safety copy at: {temp_path}")]
44    WriteAtomic {
45        /// Path to the settings file
46        path: PathBuf,
47        /// Path to the temporary safety copy
48        temp_path: PathBuf,
49    },
50}
51
52/// Registry errors
53#[derive(Debug, Error)]
54pub enum RegistryError {
55    /// I/O error reading registry
56    #[error("Failed to read registry: {0}")]
57    Io(#[source] std::io::Error),
58
59    /// Failed to parse registry file
60    #[error("Failed to parse registry: {0}")]
61    Parse(String),
62
63    /// Failed to write registry
64    #[error("Failed to write registry: {0}")]
65    Write(String),
66}
67
68/// Hook logic errors
69#[derive(Debug, Error)]
70pub enum HookError {
71    /// Hook already exists
72    #[error("Hook already exists: {event:?} - {command}")]
73    AlreadyExists {
74        /// The hook event
75        event: HookEvent,
76        /// The command string
77        command: String,
78    },
79
80    /// Hook not managed by claude-hooks
81    #[error("Hook not managed by claude-hooks: {event:?} - {command}")]
82    NotManaged {
83        /// The hook event
84        event: HookEvent,
85        /// The command string
86        command: String,
87    },
88
89    /// Invalid hook handler
90    #[error("Invalid hook handler: {0}")]
91    InvalidHandler(String),
92}
93
94/// Result type alias for claude-hooks operations
95pub 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}