1#![forbid(unsafe_code)]
2
3use oxiproto_core::OxiProtoError;
6use std::io;
7
8#[derive(Debug)]
14pub enum BuildError {
15 Parse {
17 file: String,
19 line: u32,
21 col: u32,
23 message: String,
25 },
26 Codegen {
28 message: String,
30 },
31 Io(io::Error),
33}
34
35impl std::fmt::Display for BuildError {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 match self {
38 BuildError::Parse {
39 file,
40 line,
41 col,
42 message,
43 } => {
44 if file.is_empty() {
45 write!(f, "parse error: {message}")
46 } else if *col == 0 {
47 write!(f, "{file}:{line}: {message}")
48 } else {
49 write!(f, "{file}:{line}:{col}: {message}")
50 }
51 }
52 BuildError::Codegen { message } => write!(f, "codegen error: {message}"),
53 BuildError::Io(e) => write!(f, "I/O error: {e}"),
54 }
55 }
56}
57
58impl std::error::Error for BuildError {
59 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60 match self {
61 BuildError::Io(e) => Some(e),
62 BuildError::Parse { .. } | BuildError::Codegen { .. } => None,
63 }
64 }
65}
66
67impl From<OxiProtoError> for BuildError {
68 fn from(e: OxiProtoError) -> Self {
69 match &e {
70 OxiProtoError::ParseError(msg) => {
71 BuildError::from_parse_string(msg)
73 }
74 OxiProtoError::CodegenError(msg) => BuildError::Codegen {
75 message: msg.clone(),
76 },
77 OxiProtoError::IoError(io_err) => {
78 BuildError::Io(io::Error::new(io_err.kind(), io_err.to_string()))
80 }
81 OxiProtoError::WireFormatError(w) => BuildError::Codegen {
82 message: w.to_string(),
83 },
84 _ => BuildError::Codegen {
86 message: e.to_string(),
87 },
88 }
89 }
90}
91
92impl From<BuildError> for OxiProtoError {
93 fn from(e: BuildError) -> Self {
94 OxiProtoError::ParseError(e.to_string())
95 }
96}
97
98impl From<io::Error> for BuildError {
99 fn from(e: io::Error) -> Self {
100 BuildError::Io(e)
101 }
102}
103
104impl BuildError {
105 pub(crate) fn from_parse_string(msg: &str) -> Self {
109 let parts: Vec<&str> = msg.splitn(5, ':').collect();
113
114 if parts.len() >= 3 {
117 let (file_raw, line_idx, col_idx) = if parts[0].len() == 1
120 && parts[0]
121 .chars()
122 .next()
123 .is_some_and(|c| c.is_ascii_alphabetic())
124 {
125 if parts.len() >= 5 {
127 let file = format!("{}:{}", parts[0], parts[1]);
128 (file, 2usize, 3usize)
129 } else {
130 return Self::fallback(msg);
132 }
133 } else {
134 (parts[0].to_owned(), 1usize, 2usize)
135 };
136
137 if let Ok(line) = parts[line_idx].trim().parse::<u32>() {
138 if let Ok(col) = parts[col_idx].trim().parse::<u32>() {
140 let message = parts[(col_idx + 1)..]
142 .join(":")
143 .trim_start_matches(' ')
144 .to_owned();
145 return BuildError::Parse {
146 file: file_raw,
147 line,
148 col,
149 message: if message.is_empty() {
150 msg.to_owned()
151 } else {
152 message
153 },
154 };
155 }
156 let message = parts[(line_idx + 1)..]
158 .join(":")
159 .trim_start_matches(' ')
160 .to_owned();
161 return BuildError::Parse {
162 file: file_raw,
163 line,
164 col: 0,
165 message: if message.is_empty() {
166 msg.to_owned()
167 } else {
168 message
169 },
170 };
171 }
172 }
173
174 Self::fallback(msg)
175 }
176
177 fn fallback(msg: &str) -> Self {
178 BuildError::Parse {
179 file: String::new(),
180 line: 0,
181 col: 0,
182 message: msg.to_owned(),
183 }
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn parse_file_line_col() {
193 let e = BuildError::from_parse_string("foo.proto:3:7: unexpected token");
194 match e {
195 BuildError::Parse {
196 file,
197 line,
198 col,
199 message,
200 } => {
201 assert_eq!(file, "foo.proto");
202 assert_eq!(line, 3);
203 assert_eq!(col, 7);
204 assert!(message.contains("unexpected"));
205 }
206 other => panic!("unexpected variant: {other:?}"),
207 }
208 }
209
210 #[test]
211 fn parse_file_line_no_col() {
212 let e = BuildError::from_parse_string("bar.proto:10: missing semicolon");
213 match e {
214 BuildError::Parse {
215 file,
216 line,
217 col,
218 message,
219 } => {
220 assert_eq!(file, "bar.proto");
221 assert_eq!(line, 10);
222 assert_eq!(col, 0);
223 assert!(message.contains("semicolon"));
224 }
225 other => panic!("unexpected variant: {other:?}"),
226 }
227 }
228
229 #[test]
230 fn parse_fallback_on_plain_message() {
231 let e = BuildError::from_parse_string("something went wrong");
232 match e {
233 BuildError::Parse {
234 file,
235 line,
236 col,
237 message,
238 } => {
239 assert!(file.is_empty());
240 assert_eq!(line, 0);
241 assert_eq!(col, 0);
242 assert_eq!(message, "something went wrong");
243 }
244 other => panic!("unexpected variant: {other:?}"),
245 }
246 }
247
248 #[test]
249 fn display_with_location() {
250 let e = BuildError::Parse {
251 file: "test.proto".to_owned(),
252 line: 5,
253 col: 3,
254 message: "oops".to_owned(),
255 };
256 assert_eq!(e.to_string(), "test.proto:5:3: oops");
257 }
258
259 #[test]
260 fn display_without_location() {
261 let e = BuildError::Codegen {
262 message: "bad output".to_owned(),
263 };
264 assert_eq!(e.to_string(), "codegen error: bad output");
265 }
266}