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}