use axum::Json;
use axum::http::header;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
use std::fmt::Write;
use std::time::Duration;
use tibba_error::Error;
type Result<T> = std::result::Result<T, Error>;
pub type JsonResult<T> = Result<Json<T>>;
const S_MAX_AGE_LIMIT_SECS: u64 = 3600;
pub struct CacheJson<T> {
duration: Duration,
public: bool,
data: T,
}
pub type CacheJsonResult<T> = Result<CacheJson<T>>;
impl<T> CacheJson<T> {
#[must_use]
pub fn new(duration: Duration, data: T) -> Self {
Self {
duration,
public: false,
data,
}
}
#[must_use]
pub fn with_public(mut self) -> Self {
self.public = true;
self
}
}
impl<T> From<(Duration, T)> for CacheJson<T> {
fn from(value: (Duration, T)) -> Self {
Self::new(value.0, value.1)
}
}
impl<T> IntoResponse for CacheJson<T>
where
T: Serialize,
{
fn into_response(self) -> Response {
let secs = self.duration.as_secs();
if secs == 0 {
return (
[(header::CACHE_CONTROL, "no-cache".to_string())],
Json(self.data),
)
.into_response();
}
let mut cache_control_value = String::with_capacity(64);
let visibility = if self.public { "public" } else { "private" };
let _ = write!(&mut cache_control_value, "{visibility}, max-age={secs}");
if self.public && secs > S_MAX_AGE_LIMIT_SECS {
let _ = write!(
&mut cache_control_value,
", s-maxage={S_MAX_AGE_LIMIT_SECS}"
);
}
(
[(header::CACHE_CONTROL, cache_control_value)],
Json(self.data),
)
.into_response()
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
fn cache_control<T: Serialize>(value: CacheJson<T>) -> String {
value
.into_response()
.headers()
.get(header::CACHE_CONTROL)
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string()
}
#[test]
fn defaults_to_private() {
let header = cache_control(CacheJson::new(Duration::from_secs(60), "x"));
assert_eq!(header, "private, max-age=60");
let header = cache_control(CacheJson::from((Duration::from_secs(60), "x")));
assert_eq!(header, "private, max-age=60");
}
#[test]
fn public_opt_in_caps_shared_cache_ttl() {
let header = cache_control(CacheJson::new(Duration::from_secs(60), "x").with_public());
assert_eq!(header, "public, max-age=60");
let header = cache_control(CacheJson::new(Duration::from_secs(7200), "x").with_public());
assert_eq!(header, "public, max-age=7200, s-maxage=3600");
}
#[test]
fn private_never_emits_s_maxage() {
let header = cache_control(CacheJson::new(Duration::from_secs(7200), "x"));
assert_eq!(header, "private, max-age=7200");
}
#[test]
fn zero_duration_means_no_cache() {
assert_eq!(
cache_control(CacheJson::new(Duration::ZERO, "x")),
"no-cache"
);
}
}