Skip to main content

xcstrings_mcp/
error.rs

1use std::path::PathBuf;
2
3#[derive(Debug, thiserror::Error)]
4pub enum XcStringsError {
5    #[error("file not found: {path}")]
6    FileNotFound { path: PathBuf },
7
8    #[error("invalid path {path}: {reason}")]
9    InvalidPath { path: PathBuf, reason: String },
10
11    #[error("not an .xcstrings file: {path}")]
12    NotXcStrings { path: PathBuf },
13
14    #[error("invalid format: {0}")]
15    InvalidFormat(String),
16
17    #[error("JSON parse error: {0}")]
18    JsonParse(String),
19
20    #[error("locale not found: {0}")]
21    LocaleNotFound(String),
22
23    #[error("locale already exists: {0}")]
24    LocaleAlreadyExists(String),
25
26    #[error("no active file — call parse_xcstrings first")]
27    NoActiveFile,
28
29    #[error("invalid batch size: {0}")]
30    InvalidBatchSize(String),
31
32    #[error("file too large: {size_mb}MB (max {max_mb}MB)")]
33    FileTooLarge { size_mb: u64, max_mb: u64 },
34
35    #[error("file is locked by another process (likely Xcode): {path}")]
36    FileLocked { path: PathBuf },
37
38    #[error("cannot remove source locale: {0}")]
39    CannotRemoveSourceLocale(String),
40
41    #[error("glossary error: {0}")]
42    GlossaryError(String),
43
44    #[error(".strings parse error at line {line}: {message}")]
45    StringsParse { line: usize, message: String },
46
47    #[error(".stringsdict parse error: {0}")]
48    StringsdictParse(String),
49
50    #[error("XLIFF parse error: {0}")]
51    XliffParse(String),
52
53    #[error("XLIFF format error: {0}")]
54    XliffFormat(String),
55
56    #[error("file already exists: {path}")]
57    FileAlreadyExists { path: PathBuf },
58
59    #[error("key not found: {0}")]
60    KeyNotFound(String),
61
62    #[error("key already exists: {0}")]
63    KeyAlreadyExists(String),
64
65    #[error(transparent)]
66    Io(#[from] std::io::Error),
67
68    #[error(transparent)]
69    Serde(#[from] serde_json::Error),
70}
71
72impl From<XcStringsError> for rmcp::model::ErrorData {
73    fn from(e: XcStringsError) -> Self {
74        rmcp::model::ErrorData::new(rmcp::model::ErrorCode::INTERNAL_ERROR, e.to_string(), None)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn error_converts_to_mcp_error() {
84        let err = XcStringsError::NoActiveFile;
85        let mcp_err: rmcp::model::ErrorData = err.into();
86        assert!(mcp_err.message.contains("no active file"));
87    }
88
89    #[test]
90    fn error_display() {
91        let err = XcStringsError::FileNotFound {
92            path: PathBuf::from("/test.xcstrings"),
93        };
94        assert!(err.to_string().contains("/test.xcstrings"));
95
96        let err = XcStringsError::FileTooLarge {
97            size_mb: 100,
98            max_mb: 50,
99        };
100        assert!(err.to_string().contains("100"));
101        assert!(err.to_string().contains("50"));
102    }
103
104    #[test]
105    fn strings_parse_display_includes_line_and_message() {
106        let err = XcStringsError::StringsParse {
107            line: 42,
108            message: "unexpected token".into(),
109        };
110        let display = err.to_string();
111        assert!(
112            display.contains("42"),
113            "should contain line number: {display}"
114        );
115        assert!(
116            display.contains("unexpected token"),
117            "should contain message: {display}"
118        );
119    }
120
121    #[test]
122    fn key_not_found_display() {
123        let err = XcStringsError::KeyNotFound("test_key".into());
124        assert!(err.to_string().contains("test_key"));
125    }
126
127    #[test]
128    fn key_already_exists_display() {
129        let err = XcStringsError::KeyAlreadyExists("test_key".into());
130        assert!(err.to_string().contains("test_key"));
131    }
132
133    #[test]
134    fn stringsdict_parse_display_includes_message() {
135        let err = XcStringsError::StringsdictParse("missing required key".into());
136        let display = err.to_string();
137        assert!(
138            display.contains("missing required key"),
139            "should contain message: {display}"
140        );
141    }
142}