use crate::constants;
use crate::error::Error;
pub fn print_error_with_json(error: &Error, json_format: bool) {
if !json_format {
print_error(error);
return;
}
let json_error = error.to_json();
let Ok(json_output) = serde_json::to_string_pretty(&json_error) else {
print_error(error);
return;
};
eprintln!("{json_output}");
}
pub fn print_error(error: &Error) {
write_error(error, &mut std::io::stderr());
}
#[allow(clippy::too_many_lines)]
fn write_error<W: std::io::Write>(error: &Error, writer: &mut W) {
match error {
Error::Internal {
kind,
message,
context,
} => {
let _ = writeln!(writer, "{kind}: {message}");
let Some(ctx) = context else { return };
if let Some(suggestion) = &ctx.suggestion {
let _ = writeln!(writer, "\nHint: {suggestion}");
}
}
Error::Io(io_err) => match io_err.kind() {
std::io::ErrorKind::NotFound => {
let _ = writeln!(
writer,
"File Not Found\n{io_err}\n\nHint: {}",
constants::ERR_FILE_NOT_FOUND
);
}
std::io::ErrorKind::PermissionDenied => {
let _ = writeln!(
writer,
"Permission Denied\n{io_err}\n\nHint: {}",
constants::ERR_PERMISSION
);
}
_ => {
let _ = writeln!(writer, "File System Error\n{io_err}");
}
},
Error::Network(req_err) => {
if req_err.is_connect() {
let _ = writeln!(
writer,
"Connection Error\n{req_err}\n\nHint: {}",
constants::ERR_CONNECTION
);
return;
}
if req_err.is_timeout() {
let _ = writeln!(
writer,
"Timeout Error\n{req_err}\n\nHint: {}",
constants::ERR_TIMEOUT
);
return;
}
if !req_err.is_status() {
let _ = writeln!(writer, "Network Error\n{req_err}");
return;
}
let Some(status) = req_err.status() else {
let _ = writeln!(writer, "Network Error\n{req_err}");
return;
};
match status.as_u16() {
401 => {
let _ = writeln!(
writer,
"Authentication Error\n{req_err}\n\nHint: {}",
constants::ERR_API_CREDENTIALS
);
}
403 => {
let _ = writeln!(
writer,
"Permission Error\n{req_err}\n\nHint: {}",
constants::ERR_PERMISSION_DENIED
);
}
404 => {
let _ = writeln!(
writer,
"Not Found Error\n{req_err}\n\nHint: {}",
constants::ERR_ENDPOINT_NOT_FOUND
);
}
429 => {
let _ = writeln!(
writer,
"Rate Limited\n{req_err}\n\nHint: {}",
constants::ERR_RATE_LIMITED
);
}
500..=599 => {
let _ = writeln!(
writer,
"Server Error\n{req_err}\n\nHint: {}",
constants::ERR_SERVER_ERROR
);
}
_ => {
let _ = writeln!(writer, "HTTP Error\n{req_err}");
}
}
}
Error::Yaml(yaml_err) => {
let _ = writeln!(
writer,
"YAML Parsing Error\n{yaml_err}\n\nHint: {}",
constants::ERR_YAML_SYNTAX
);
}
Error::Json(json_err) => {
let _ = writeln!(
writer,
"JSON Parsing Error\n{json_err}\n\nHint: {}",
constants::ERR_JSON_SYNTAX
);
}
Error::Toml(toml_err) => {
let _ = writeln!(
writer,
"TOML Parsing Error\n{toml_err}\n\nHint: {}",
constants::ERR_TOML_SYNTAX
);
}
Error::Anyhow(anyhow_err) => {
let _ = writeln!(writer, "Error\n{anyhow_err}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn capture(error: &Error) -> String {
let mut buf = Vec::new();
write_error(error, &mut buf);
String::from_utf8(buf).expect("output is valid UTF-8")
}
#[test]
fn test_internal_without_suggestion() {
let err = Error::validation_error("bad input");
let out = capture(&err);
assert!(out.contains("Validation"));
assert!(out.contains("bad input"));
}
#[test]
fn test_internal_with_suggestion() {
let err = Error::spec_not_found("my-api");
let out = capture(&err);
assert!(out.contains("Specification"));
assert!(out.contains("my-api"));
assert!(out.contains("Hint:"));
}
#[test]
fn test_io_not_found() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
let err = Error::Io(io_err);
let out = capture(&err);
assert!(out.contains("File Not Found"));
assert!(out.contains(constants::ERR_FILE_NOT_FOUND));
}
#[test]
fn test_io_permission_denied() {
let io_err = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied");
let err = Error::Io(io_err);
let out = capture(&err);
assert!(out.contains("Permission Denied"));
assert!(out.contains(constants::ERR_PERMISSION));
}
#[test]
fn test_io_other() {
let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "broken pipe");
let err = Error::Io(io_err);
let out = capture(&err);
assert!(out.contains("File System Error"));
}
#[test]
fn test_yaml_error() {
let yaml_err = serde_yaml::from_str::<serde_yaml::Value>("key: - value").unwrap_err();
let err = Error::Yaml(yaml_err);
let out = capture(&err);
assert!(out.contains("YAML Parsing Error"));
assert!(out.contains(constants::ERR_YAML_SYNTAX));
}
#[test]
fn test_json_error() {
let json_err = serde_json::from_str::<serde_json::Value>("{bad").unwrap_err();
let err = Error::Json(json_err);
let out = capture(&err);
assert!(out.contains("JSON Parsing Error"));
assert!(out.contains(constants::ERR_JSON_SYNTAX));
}
#[test]
fn test_toml_error() {
let toml_err = toml::from_str::<toml::Value>("key = ").unwrap_err();
let err = Error::Toml(toml_err);
let out = capture(&err);
assert!(out.contains("TOML Parsing Error"));
assert!(out.contains(constants::ERR_TOML_SYNTAX));
}
#[test]
fn test_anyhow_error() {
let err = Error::Anyhow(anyhow::anyhow!("something went wrong"));
let out = capture(&err);
assert!(out.contains("Error"));
assert!(out.contains("something went wrong"));
}
async fn status_error(status: u16) -> reqwest::Error {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/err"))
.respond_with(ResponseTemplate::new(status))
.mount(&server)
.await;
reqwest::Client::new()
.get(format!("{}/err", server.uri()))
.send()
.await
.expect("request reached mock server")
.error_for_status()
.expect_err("status >= 400 must produce an error")
}
#[tokio::test]
async fn test_network_connect_error() {
let req_err = reqwest::Client::new()
.get("http://127.0.0.1:1/")
.send()
.await
.expect_err("port 1 must refuse connections");
assert!(req_err.is_connect(), "expected a connect error");
let out = capture(&Error::Network(req_err));
assert!(out.contains("Connection Error"));
assert!(out.contains(constants::ERR_CONNECTION));
}
#[tokio::test]
async fn test_network_timeout_error() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/slow"))
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(10)))
.mount(&server)
.await;
let req_err = reqwest::Client::builder()
.timeout(Duration::from_millis(1))
.build()
.unwrap()
.get(format!("{}/slow", server.uri()))
.send()
.await
.expect_err("request must time out");
assert!(req_err.is_timeout(), "expected a timeout error");
let out = capture(&Error::Network(req_err));
assert!(out.contains("Timeout Error"));
assert!(out.contains(constants::ERR_TIMEOUT));
}
#[tokio::test]
async fn test_network_401() {
let err = status_error(401).await;
let out = capture(&Error::Network(err));
assert!(out.contains("Authentication Error"));
assert!(out.contains(constants::ERR_API_CREDENTIALS));
}
#[tokio::test]
async fn test_network_403() {
let err = status_error(403).await;
let out = capture(&Error::Network(err));
assert!(out.contains("Permission Error"));
assert!(out.contains(constants::ERR_PERMISSION_DENIED));
}
#[tokio::test]
async fn test_network_404() {
let err = status_error(404).await;
let out = capture(&Error::Network(err));
assert!(out.contains("Not Found Error"));
assert!(out.contains(constants::ERR_ENDPOINT_NOT_FOUND));
}
#[tokio::test]
async fn test_network_429() {
let err = status_error(429).await;
let out = capture(&Error::Network(err));
assert!(out.contains("Rate Limited"));
assert!(out.contains(constants::ERR_RATE_LIMITED));
}
#[tokio::test]
async fn test_network_503() {
let err = status_error(503).await;
let out = capture(&Error::Network(err));
assert!(out.contains("Server Error"));
assert!(out.contains(constants::ERR_SERVER_ERROR));
}
#[tokio::test]
async fn test_network_400_fallback() {
let err = status_error(400).await;
let out = capture(&Error::Network(err));
assert!(out.contains("HTTP Error"));
}
}