use std::fmt::Debug;
use std::sync::Arc;
use http_extensions::ResponseExt;
use seatbelt::{Recovery, RecoveryInfo};
use tick::Clock;
use crate::HttpResponse;
pub struct HttpRecovery(Inner);
impl Debug for HttpRecovery {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let variant = match &self.0 {
Inner::Default => "Default",
Inner::Custom(_) => "Custom",
};
f.debug_tuple("HttpRecovery").field(&variant).finish()
}
}
impl Default for HttpRecovery {
fn default() -> Self {
Self(Inner::Default)
}
}
impl HttpRecovery {
#[must_use]
pub fn custom(recovery: impl Fn(&HttpResponse) -> RecoveryInfo + Send + Sync + 'static) -> Self {
Self(Inner::Custom(Arc::new(move |response, _clock| recovery(response))))
}
#[must_use]
pub fn custom_with_clock(recovery: impl Fn(&HttpResponse, &Clock) -> RecoveryInfo + Send + Sync + 'static) -> Self {
Self(Inner::Custom(Arc::new(recovery)))
}
pub(crate) fn recovery(&self, response: &HttpResponse, clock: &Clock) -> RecoveryInfo {
match &self.0 {
Inner::Default => response.recovery_with_clock(clock),
Inner::Custom(f) => f(response, clock),
}
}
}
pub(super) fn detect_recovery(result: &http_extensions::Result<HttpResponse>, recovery: &HttpRecovery, clock: &Clock) -> RecoveryInfo {
match result {
Ok(response) => recovery.recovery(response, clock),
Err(error) => error.recovery(),
}
}
impl<F> From<F> for HttpRecovery
where
F: Fn(&HttpResponse) -> RecoveryInfo + Send + Sync + 'static,
{
fn from(f: F) -> Self {
Self::custom(f)
}
}
type CustomDelegate = Arc<dyn Fn(&HttpResponse, &Clock) -> RecoveryInfo + Send + Sync>;
enum Inner {
Default,
Custom(CustomDelegate),
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use std::time::Duration;
use http::StatusCode;
use http_extensions::HttpResponseBuilder;
use seatbelt::RecoveryKind;
use super::*;
#[test]
fn default_recovery() {
let http_recovery = HttpRecovery::default();
let mut response = HttpResponseBuilder::new_fake().build().unwrap();
let clock = Clock::new_frozen();
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
assert_eq!(http_recovery.recovery(&response, &clock).kind(), RecoveryKind::Retry);
*response.status_mut() = StatusCode::BAD_REQUEST;
assert_eq!(http_recovery.recovery(&response, &clock).kind(), RecoveryKind::Never);
}
#[test]
fn custom_recovery() {
let http_recovery = HttpRecovery::from(|response: &HttpResponse| {
if response.status() == StatusCode::BAD_REQUEST {
RecoveryInfo::retry()
} else {
RecoveryInfo::never()
}
});
let response = HttpResponseBuilder::new_fake().status(StatusCode::BAD_REQUEST).build().unwrap();
assert_eq!(http_recovery.recovery(&response, &Clock::new_frozen()).kind(), RecoveryKind::Retry);
}
#[test]
fn custom_recovery_with_clock() {
let http_recovery = HttpRecovery::custom_with_clock(http_extensions::ResponseExt::recovery_with_clock);
let response = HttpResponseBuilder::new_fake()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Retry-After", "42")
.build()
.unwrap();
let clock = Clock::new_frozen();
let recovery = http_recovery.recovery(&response, &clock);
assert_eq!(recovery.kind(), RecoveryKind::Retry);
assert_eq!(recovery.get_delay(), Some(Duration::from_secs(42)));
}
#[test]
fn custom_with_clock_uses_provided_closure() {
let http_recovery = HttpRecovery::custom_with_clock(|response, _clock| {
if response.status() == StatusCode::BAD_REQUEST {
RecoveryInfo::retry()
} else {
RecoveryInfo::never()
}
});
let response = HttpResponseBuilder::new_fake().status(StatusCode::BAD_REQUEST).build().unwrap();
let clock = Clock::new_frozen();
assert_eq!(http_recovery.recovery(&response, &clock).kind(), RecoveryKind::Retry);
}
#[test]
fn debug_distinguishes_default_and_custom() {
assert_eq!(format!("{:?}", HttpRecovery::default()), "HttpRecovery(\"Default\")");
assert_eq!(
format!("{:?}", HttpRecovery::custom(|_| RecoveryInfo::never())),
"HttpRecovery(\"Custom\")"
);
assert_eq!(
format!("{:?}", HttpRecovery::custom_with_clock(|_, _| RecoveryInfo::never())),
"HttpRecovery(\"Custom\")"
);
}
#[test]
fn default_recovery_respects_retry_after() {
let http_recovery = HttpRecovery::default();
let response = HttpResponseBuilder::new_fake()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.header("Retry-After", "10")
.build()
.unwrap();
let clock = Clock::new_frozen();
let recovery = http_recovery.recovery(&response, &clock);
assert_eq!(recovery.kind(), RecoveryKind::Retry);
assert_eq!(recovery.get_delay(), Some(Duration::from_secs(10)));
}
}