#![forbid(unsafe_code)]
use oxiproto_core::OxiProtoError;
use std::io;
#[derive(Debug)]
pub enum BuildError {
Parse {
file: String,
line: u32,
col: u32,
message: String,
},
Codegen {
message: String,
},
Io(io::Error),
}
impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::Parse {
file,
line,
col,
message,
} => {
if file.is_empty() {
write!(f, "parse error: {message}")
} else if *col == 0 {
write!(f, "{file}:{line}: {message}")
} else {
write!(f, "{file}:{line}:{col}: {message}")
}
}
BuildError::Codegen { message } => write!(f, "codegen error: {message}"),
BuildError::Io(e) => write!(f, "I/O error: {e}"),
}
}
}
impl std::error::Error for BuildError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
BuildError::Io(e) => Some(e),
BuildError::Parse { .. } | BuildError::Codegen { .. } => None,
}
}
}
impl From<OxiProtoError> for BuildError {
fn from(e: OxiProtoError) -> Self {
match &e {
OxiProtoError::ParseError(msg) => {
BuildError::from_parse_string(msg)
}
OxiProtoError::CodegenError(msg) => BuildError::Codegen {
message: msg.clone(),
},
OxiProtoError::IoError(io_err) => {
BuildError::Io(io::Error::new(io_err.kind(), io_err.to_string()))
}
OxiProtoError::WireFormatError(w) => BuildError::Codegen {
message: w.to_string(),
},
_ => BuildError::Codegen {
message: e.to_string(),
},
}
}
}
impl From<BuildError> for OxiProtoError {
fn from(e: BuildError) -> Self {
OxiProtoError::ParseError(e.to_string())
}
}
impl From<io::Error> for BuildError {
fn from(e: io::Error) -> Self {
BuildError::Io(e)
}
}
impl BuildError {
pub(crate) fn from_parse_string(msg: &str) -> Self {
let parts: Vec<&str> = msg.splitn(5, ':').collect();
if parts.len() >= 3 {
let (file_raw, line_idx, col_idx) = if parts[0].len() == 1
&& parts[0]
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic())
{
if parts.len() >= 5 {
let file = format!("{}:{}", parts[0], parts[1]);
(file, 2usize, 3usize)
} else {
return Self::fallback(msg);
}
} else {
(parts[0].to_owned(), 1usize, 2usize)
};
if let Ok(line) = parts[line_idx].trim().parse::<u32>() {
if let Ok(col) = parts[col_idx].trim().parse::<u32>() {
let message = parts[(col_idx + 1)..]
.join(":")
.trim_start_matches(' ')
.to_owned();
return BuildError::Parse {
file: file_raw,
line,
col,
message: if message.is_empty() {
msg.to_owned()
} else {
message
},
};
}
let message = parts[(line_idx + 1)..]
.join(":")
.trim_start_matches(' ')
.to_owned();
return BuildError::Parse {
file: file_raw,
line,
col: 0,
message: if message.is_empty() {
msg.to_owned()
} else {
message
},
};
}
}
Self::fallback(msg)
}
fn fallback(msg: &str) -> Self {
BuildError::Parse {
file: String::new(),
line: 0,
col: 0,
message: msg.to_owned(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_file_line_col() {
let e = BuildError::from_parse_string("foo.proto:3:7: unexpected token");
match e {
BuildError::Parse {
file,
line,
col,
message,
} => {
assert_eq!(file, "foo.proto");
assert_eq!(line, 3);
assert_eq!(col, 7);
assert!(message.contains("unexpected"));
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn parse_file_line_no_col() {
let e = BuildError::from_parse_string("bar.proto:10: missing semicolon");
match e {
BuildError::Parse {
file,
line,
col,
message,
} => {
assert_eq!(file, "bar.proto");
assert_eq!(line, 10);
assert_eq!(col, 0);
assert!(message.contains("semicolon"));
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn parse_fallback_on_plain_message() {
let e = BuildError::from_parse_string("something went wrong");
match e {
BuildError::Parse {
file,
line,
col,
message,
} => {
assert!(file.is_empty());
assert_eq!(line, 0);
assert_eq!(col, 0);
assert_eq!(message, "something went wrong");
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn display_with_location() {
let e = BuildError::Parse {
file: "test.proto".to_owned(),
line: 5,
col: 3,
message: "oops".to_owned(),
};
assert_eq!(e.to_string(), "test.proto:5:3: oops");
}
#[test]
fn display_without_location() {
let e = BuildError::Codegen {
message: "bad output".to_owned(),
};
assert_eq!(e.to_string(), "codegen error: bad output");
}
}