use std::any::Any;
use std::collections::BTreeMap;
use std::fmt::{Display, Write as _};
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
use axum::extract::FromRequestParts;
use axum::http::request::Parts;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RequestCacheKey {
resolver: &'static str,
fields: String,
}
impl RequestCacheKey {
#[must_use]
pub const fn new(resolver: &'static str) -> Self {
Self {
resolver,
fields: String::new(),
}
}
#[must_use]
pub fn field<T>(mut self, name: &str, value: &T) -> Self
where
T: Display + ?Sized,
{
let rendered = value.to_string();
let _ = write!(self.fields, "{name}={}:{rendered};", rendered.len());
self
}
#[must_use]
pub const fn resolver(&self) -> &'static str {
self.resolver
}
}
impl Display for RequestCacheKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}({})", self.resolver, self.fields)
}
}
#[derive(Clone, Default)]
pub struct RequestCache {
entries: Arc<Mutex<BTreeMap<RequestCacheKey, Arc<dyn Any + Send + Sync>>>>,
}
impl RequestCache {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get<T>(&self, key: &RequestCacheKey) -> Option<T>
where
T: Clone + Send + Sync + 'static,
{
let entries = self.entries();
entries.get(key)?.downcast_ref::<T>().cloned()
}
pub fn insert<T>(&self, key: &RequestCacheKey, value: T)
where
T: Send + Sync + 'static,
{
self.entries().insert(key.clone(), Arc::new(value));
}
#[must_use]
pub fn len(&self) -> usize {
self.entries().len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries().is_empty()
}
#[must_use]
pub fn keys(&self) -> Vec<RequestCacheKey> {
self.entries().keys().cloned().collect()
}
fn entries(&self) -> MutexGuard<'_, BTreeMap<RequestCacheKey, Arc<dyn Any + Send + Sync>>> {
self.entries.lock().unwrap_or_else(PoisonError::into_inner)
}
}
impl std::fmt::Debug for RequestCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RequestCache")
.field("entries", &self.len())
.finish_non_exhaustive()
}
}
impl<S> FromRequestParts<S> for RequestCache
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
if let Some(existing) = parts.extensions.get::<Self>() {
return Ok(existing.clone());
}
let cache = Self::new();
parts.extensions.insert(cache.clone());
Ok(cache)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key(resolver: &'static str, user: u64) -> RequestCacheKey {
RequestCacheKey::new(resolver).field("user_id", &user)
}
fn empty_parts() -> Parts {
axum::http::Request::builder()
.body(())
.expect("a request with no headers is always buildable")
.into_parts()
.0
}
#[test]
fn a_value_inserted_under_a_key_is_returned_for_that_key() {
let cache = RequestCache::new();
cache.insert(&key("load_profile", 7), "Ada".to_string());
assert_eq!(
cache.get::<String>(&key("load_profile", 7)),
Some("Ada".to_string())
);
}
#[test]
fn a_different_key_field_value_is_a_different_entry() {
let cache = RequestCache::new();
cache.insert(&key("load_profile", 7), "Ada".to_string());
assert_eq!(cache.get::<String>(&key("load_profile", 8)), None);
}
#[test]
fn a_different_resolver_name_is_a_different_entry() {
let cache = RequestCache::new();
cache.insert(&key("load_profile", 7), "Ada".to_string());
assert_eq!(cache.get::<String>(&key("load_settings", 7)), None);
}
#[test]
fn a_clone_shares_entries_with_the_original() {
let cache = RequestCache::new();
let handle = cache.clone();
handle.insert(&key("load_profile", 1), 42u32);
assert_eq!(cache.get::<u32>(&key("load_profile", 1)), Some(42));
}
#[test]
fn two_stores_never_share_entries() {
let one = RequestCache::new();
let other = RequestCache::new();
one.insert(&key("load_profile", 1), 42u32);
assert_eq!(other.get::<u32>(&key("load_profile", 1)), None);
}
#[test]
fn a_type_mismatch_reads_as_a_miss_rather_than_a_wrong_value() {
let cache = RequestCache::new();
cache.insert(&key("load_profile", 1), 42u32);
assert_eq!(cache.get::<String>(&key("load_profile", 1)), None);
}
#[test]
fn a_field_value_cannot_forge_a_second_field() {
let forged = RequestCacheKey::new("load").field("a", "1;b=1:2;");
let genuine = RequestCacheKey::new("load").field("a", "1").field("b", "2");
assert_ne!(forged, genuine);
}
#[test]
fn keys_are_listed_in_deterministic_order() {
let cache = RequestCache::new();
cache.insert(&key("zeta", 1), 1u8);
cache.insert(&key("alpha", 1), 2u8);
let names: Vec<&str> = cache.keys().iter().map(RequestCacheKey::resolver).collect();
assert_eq!(names, ["alpha", "zeta"]);
}
#[test]
fn a_key_renders_its_resolver_and_fields() {
assert_eq!(
key("load_profile", 7).to_string(),
"load_profile(user_id=1:7;)"
);
}
#[tokio::test]
async fn extracting_twice_from_one_request_yields_the_same_store() {
let mut parts = empty_parts();
let first = RequestCache::from_request_parts(&mut parts, &())
.await
.expect("extraction is infallible");
first.insert(&key("load_profile", 1), 42u32);
let second = RequestCache::from_request_parts(&mut parts, &())
.await
.expect("extraction is infallible");
assert_eq!(second.get::<u32>(&key("load_profile", 1)), Some(42));
}
#[tokio::test]
async fn a_second_request_starts_with_an_empty_store() {
let mut first_parts = empty_parts();
let first = RequestCache::from_request_parts(&mut first_parts, &())
.await
.expect("extraction is infallible");
first.insert(&key("load_profile", 1), 42u32);
let mut second_parts = empty_parts();
let second = RequestCache::from_request_parts(&mut second_parts, &())
.await
.expect("extraction is infallible");
assert!(second.is_empty());
assert_eq!(second.get::<u32>(&key("load_profile", 1)), None);
}
}