use crate::ApiError;
use std::sync::RwLock;
type ErrorHook = Box<dyn Fn(&ApiError) + Send + Sync>;
static ERROR_HOOK: RwLock<Option<ErrorHook>> = RwLock::new(None);
pub fn on_error<F>(hook: F)
where
F: Fn(&ApiError) + Send + Sync + 'static,
{
let mut guard = ERROR_HOOK
.write()
.expect("Failed to get write lock for ErrorHook");
*guard = Some(Box::new(hook));
}
pub(crate) fn invoke_hook(error: &ApiError) {
let guard = ERROR_HOOK
.read()
.expect("Failed get read lock for ErrorHook");
if let Some(hook) = guard.as_ref() {
hook(error);
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
use serial_test::serial;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use std::sync::{Arc, Mutex};
#[test]
#[serial]
fn test_hook_is_called_when_error_is_built() {
let called = Arc::new(AtomicBool::new(false));
on_error({
let called = called.clone();
move |_err| {
called.store(true, Ordering::SeqCst);
}
});
let _error = ApiError::builder()
.status(StatusCode::BAD_REQUEST)
.title("Test Error")
.detail("This is a test")
.build();
assert!(
called.load(Ordering::SeqCst),
"Hook should have been called"
);
}
#[test]
#[serial]
fn test_hook_receives_correct_error_details() {
let captured_status = Arc::new(Mutex::new(None));
let captured_title = Arc::new(Mutex::new(None));
let captured_detail = Arc::new(Mutex::new(None));
on_error({
let captured_status = captured_status.clone();
let captured_title = captured_title.clone();
let captured_detail = captured_detail.clone();
move |err| {
*captured_status.lock().unwrap() = Some(err.status());
*captured_title.lock().unwrap() = Some(err.title().to_string());
*captured_detail.lock().unwrap() = err.detail().map(|s| s.to_string());
}
});
let _error = ApiError::builder()
.status(StatusCode::NOT_FOUND)
.title("Resource Not Found")
.detail("The requested resource does not exist")
.build();
assert_eq!(
*captured_status.lock().unwrap(),
Some(StatusCode::NOT_FOUND)
);
assert_eq!(
*captured_title.lock().unwrap(),
Some("Resource Not Found".to_string())
);
assert_eq!(
*captured_detail.lock().unwrap(),
Some("The requested resource does not exist".to_string())
);
}
#[test]
#[serial]
fn test_hook_can_be_replaced() {
let first_call = Arc::new(AtomicU8::new(0));
let second_call = Arc::new(AtomicU8::new(0));
on_error({
let first_call = first_call.clone();
move |_err| {
first_call.fetch_add(1, Ordering::SeqCst);
}
});
let _error1 = ApiError::builder()
.status(StatusCode::BAD_REQUEST)
.title("Error 1")
.build();
on_error({
let second_call = second_call.clone();
move |_err| {
second_call.fetch_add(1, Ordering::SeqCst);
}
});
let _error2 = ApiError::builder()
.status(StatusCode::BAD_REQUEST)
.title("Error 2")
.build();
assert_eq!(first_call.load(Ordering::SeqCst), 1);
assert_eq!(second_call.load(Ordering::SeqCst), 1);
}
#[test]
#[serial]
fn test_invoke_hook_without_setting_hook() {
let error = ApiError::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.title("Test")
.build();
assert_eq!(error.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
#[serial]
fn test_hook_with_multiple_errors() {
let counter = Arc::new(AtomicU8::new(0));
on_error({
let counter = counter.clone();
move |_err| {
counter.fetch_add(1, Ordering::SeqCst);
}
});
for i in 0..5 {
let _error = ApiError::builder()
.status(StatusCode::BAD_REQUEST)
.title(format!("Error {}", i))
.build();
}
assert_eq!(counter.load(Ordering::SeqCst), 5);
}
}