Skip to main content

gettext_mcp/
error.rs

1//! Crate-wide error type for the gettext-mcp server.
2//!
3//! Merges what used to live in `store.rs` as separate `ParseError` and
4//! `StoreError` enums into a single [`GettextError`] so every layer
5//! (parser, serializer, file I/O, store, MCP tools) can speak the same
6//! error vocabulary.
7
8use std::path::PathBuf;
9
10/// Unified error type returned by the parser, serializer, file I/O,
11/// store, and MCP tool layers.
12#[derive(Debug, thiserror::Error)]
13pub enum GettextError {
14    /// PO format error surfaced by the parser.
15    #[error("Invalid PO format: {0}")]
16    InvalidFormat(String),
17
18    /// Lookup failed: no entry with the given key exists.
19    #[error("Translation not found for key: {key}, context: {context:?}")]
20    TranslationNotFound {
21        key: String,
22        context: Option<String>,
23    },
24
25    /// A tool was called without `path` in dynamic mode, or without a
26    /// configured default file in single-file mode.
27    #[error("Path required for dynamic mode")]
28    PathRequired,
29
30    /// Path validation rejected the caller-supplied path (traversal,
31    /// outside base directory, etc.).
32    #[error("Invalid path: {0}")]
33    InvalidPath(String),
34
35    /// Generic invalid-argument error from the store or a tool handler.
36    #[error("Invalid input: {0}")]
37    InvalidInput(String),
38
39    /// Another process (likely Poedit or a translator's editor) holds an
40    /// exclusive lock on the file we tried to write.
41    #[error("File is locked by another process: {path}")]
42    FileLocked { path: PathBuf },
43
44    /// Underlying `std::io::Error` from a filesystem call.
45    #[error("IO error: {0}")]
46    Io(#[from] std::io::Error),
47}
48
49/// Backwards-compatible alias for code paths (and tests) that still spell
50/// the parser error as `ParseError`. Both names point at the same enum.
51pub type ParseError = GettextError;
52
53/// Backwards-compatible alias for the pre-refactor `StoreError` name.
54pub type StoreError = GettextError;
55
56impl From<GettextError> for rmcp::model::ErrorData {
57    fn from(e: GettextError) -> Self {
58        rmcp::model::ErrorData::new(rmcp::model::ErrorCode::INTERNAL_ERROR, e.to_string(), None)
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn translation_not_found_display() {
68        let err = GettextError::TranslationNotFound {
69            key: "Hello".into(),
70            context: Some("menu".into()),
71        };
72        let s = err.to_string();
73        assert!(s.contains("Hello"));
74        assert!(s.contains("menu"));
75    }
76
77    #[test]
78    fn file_locked_display_includes_path() {
79        let err = GettextError::FileLocked {
80            path: PathBuf::from("/tmp/messages.po"),
81        };
82        assert!(err.to_string().contains("/tmp/messages.po"));
83    }
84
85    #[test]
86    fn aliases_are_the_same_type() {
87        // Compile-time check: ParseError, StoreError, GettextError are the same.
88        fn assert_same<T>(_: &T, _: &T) {}
89        let e1: GettextError = GettextError::PathRequired;
90        let e2: ParseError = GettextError::PathRequired;
91        let e3: StoreError = GettextError::PathRequired;
92        assert_same(&e1, &e2);
93        assert_same(&e1, &e3);
94    }
95}