mcpls_core/
error.rs

1//! Error types for mcpls-core.
2//!
3//! This module defines the canonical error type for the library,
4//! following the Microsoft Rust Guidelines for error handling.
5
6use std::path::PathBuf;
7
8/// The main error type for mcpls-core operations.
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    /// LSP server failed to initialize.
12    #[error("LSP server initialization failed: {message}")]
13    LspInitFailed {
14        /// Description of the initialization failure.
15        message: String,
16    },
17
18    /// LSP server returned an error response.
19    #[error("LSP server error: {code} - {message}")]
20    LspServerError {
21        /// JSON-RPC error code.
22        code: i32,
23        /// Error message from the server.
24        message: String,
25    },
26
27    /// MCP server error.
28    #[error("MCP server error: {0}")]
29    McpServer(String),
30
31    /// Document was not found or could not be opened.
32    #[error("document not found: {0}")]
33    DocumentNotFound(PathBuf),
34
35    /// No LSP server configured for the given language.
36    #[error("no LSP server configured for language: {0}")]
37    NoServerForLanguage(String),
38
39    /// No LSP server is currently configured.
40    #[error("no LSP server configured")]
41    NoServerConfigured,
42
43    /// Configuration error.
44    #[error("configuration error: {0}")]
45    Config(String),
46
47    /// Configuration file not found.
48    #[error("configuration file not found: {0}")]
49    ConfigNotFound(PathBuf),
50
51    /// Invalid configuration format.
52    #[error("invalid configuration: {0}")]
53    InvalidConfig(String),
54
55    /// I/O error.
56    #[error("I/O error: {0}")]
57    Io(#[from] std::io::Error),
58
59    /// JSON serialization/deserialization error.
60    #[error("JSON error: {0}")]
61    Json(#[from] serde_json::Error),
62
63    /// TOML parsing error.
64    #[error("TOML parsing error: {0}")]
65    Toml(#[from] toml::de::Error),
66
67    /// LSP client transport error.
68    #[error("transport error: {0}")]
69    Transport(String),
70
71    /// Request timeout.
72    #[error("request timed out after {0} seconds")]
73    Timeout(u64),
74
75    /// Server shutdown requested.
76    #[error("server shutdown requested")]
77    Shutdown,
78
79    /// LSP server failed to spawn.
80    #[error("failed to spawn LSP server '{command}': {source}")]
81    ServerSpawnFailed {
82        /// Command that failed to spawn.
83        command: String,
84        /// Underlying IO error.
85        #[source]
86        source: std::io::Error,
87    },
88
89    /// LSP protocol error during message parsing.
90    #[error("LSP protocol error: {0}")]
91    LspProtocolError(String),
92
93    /// Invalid URI format.
94    #[error("invalid URI: {0}")]
95    InvalidUri(String),
96
97    /// Position encoding error.
98    #[error("position encoding error: {0}")]
99    EncodingError(String),
100
101    /// Server process terminated unexpectedly.
102    #[error("LSP server process terminated unexpectedly")]
103    ServerTerminated,
104
105    /// Invalid tool parameters provided.
106    #[error("invalid tool parameters: {0}")]
107    InvalidToolParams(String),
108
109    /// File I/O error occurred.
110    #[error("file I/O error for {path:?}: {source}")]
111    FileIo {
112        /// Path to the file.
113        path: PathBuf,
114        /// Underlying I/O error.
115        #[source]
116        source: std::io::Error,
117    },
118
119    /// Path is outside allowed workspace boundaries.
120    #[error("path outside workspace: {0}")]
121    PathOutsideWorkspace(PathBuf),
122
123    /// Document limit exceeded.
124    #[error("document limit exceeded: {current}/{max}")]
125    DocumentLimitExceeded {
126        /// Current number of documents.
127        current: usize,
128        /// Maximum allowed documents.
129        max: usize,
130    },
131
132    /// File size limit exceeded.
133    #[error("file size limit exceeded: {size} bytes (max: {max} bytes)")]
134    FileSizeLimitExceeded {
135        /// Actual file size.
136        size: u64,
137        /// Maximum allowed size.
138        max: u64,
139    },
140}
141
142/// A specialized Result type for mcpls-core operations.
143pub type Result<T> = std::result::Result<T, Error>;
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_error_display_lsp_init_failed() {
151        let err = Error::LspInitFailed {
152            message: "server not found".to_string(),
153        };
154        assert_eq!(
155            err.to_string(),
156            "LSP server initialization failed: server not found"
157        );
158    }
159
160    #[test]
161    fn test_error_display_lsp_server_error() {
162        let err = Error::LspServerError {
163            code: -32600,
164            message: "Invalid request".to_string(),
165        };
166        assert_eq!(
167            err.to_string(),
168            "LSP server error: -32600 - Invalid request"
169        );
170    }
171
172    #[test]
173    fn test_error_display_document_not_found() {
174        let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
175        assert!(err.to_string().contains("document not found"));
176        assert!(err.to_string().contains("file.rs"));
177    }
178
179    #[test]
180    fn test_error_display_no_server_for_language() {
181        let err = Error::NoServerForLanguage("rust".to_string());
182        assert_eq!(
183            err.to_string(),
184            "no LSP server configured for language: rust"
185        );
186    }
187
188    #[test]
189    fn test_error_display_timeout() {
190        let err = Error::Timeout(30);
191        assert_eq!(err.to_string(), "request timed out after 30 seconds");
192    }
193
194    #[test]
195    fn test_error_display_document_limit() {
196        let err = Error::DocumentLimitExceeded {
197            current: 150,
198            max: 100,
199        };
200        assert_eq!(err.to_string(), "document limit exceeded: 150/100");
201    }
202
203    #[test]
204    fn test_error_display_file_size_limit() {
205        let err = Error::FileSizeLimitExceeded {
206            size: 20_000_000,
207            max: 10_000_000,
208        };
209        assert!(err.to_string().contains("file size limit exceeded"));
210    }
211
212    #[test]
213    fn test_error_from_io() {
214        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
215        let err: Error = io_err.into();
216        assert!(matches!(err, Error::Io(_)));
217    }
218
219    #[test]
220    #[allow(clippy::unwrap_used)]
221    fn test_error_from_json() {
222        let json_str = "{invalid json}";
223        let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
224        let err: Error = json_err.into();
225        assert!(matches!(err, Error::Json(_)));
226    }
227
228    #[test]
229    #[allow(clippy::unwrap_used)]
230    fn test_error_from_toml() {
231        let toml_str = "[invalid toml";
232        let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
233        let err: Error = toml_err.into();
234        assert!(matches!(err, Error::Toml(_)));
235    }
236
237    #[test]
238    fn test_result_type_alias() {
239        fn _returns_error() -> Result<i32> {
240            Err(Error::Config("test error".to_string()))
241        }
242
243        let result: Result<i32> = Ok(42);
244        assert!(result.is_ok());
245        if let Ok(value) = result {
246            assert_eq!(value, 42);
247        }
248    }
249
250    #[test]
251    fn test_error_source_chain() {
252        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
253        let err = Error::ServerSpawnFailed {
254            command: "rust-analyzer".to_string(),
255            source: io_err,
256        };
257
258        let source = std::error::Error::source(&err);
259        assert!(source.is_some());
260    }
261}