use crate::{
request::Request,
storage::{KeyValueStore, StorageResult},
};
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
sync::Arc,
};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ErrorSnapshot {
pub content_type: String,
pub bytes: bytes::Bytes,
}
pub struct ErrorSnapshotter {
kvs: Arc<dyn KeyValueStore>,
}
impl ErrorSnapshotter {
pub fn new(kvs: Arc<dyn KeyValueStore>) -> Self {
Self { kvs }
}
pub fn base_key(request: &Request) -> String {
let mut h = DefaultHasher::new();
request.unique_key.hash(&mut h);
format!("ERROR_SNAPSHOT_{:016x}", h.finish())
}
pub async fn capture(
&self,
request: &Request,
suffix: &str,
bytes: bytes::Bytes,
content_type: &str,
) -> StorageResult<String> {
let key = format!("{}.{suffix}", Self::base_key(request));
self.kvs.set_bytes(&key, bytes, content_type).await?;
Ok(key)
}
pub async fn load(&self, key: &str) -> StorageResult<Option<ErrorSnapshot>> {
Ok(self.kvs.get_bytes(key).await?.map(|entry| ErrorSnapshot {
content_type: entry.content_type,
bytes: entry.value,
}))
}
}
#[cfg(test)]
mod tests {
use super::{ErrorSnapshot, ErrorSnapshotter};
use crate::storage::{KeyList, KeyValueStore, KvEntry, ListKeysOptions, StorageResult};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
#[derive(Default)]
struct MapKvs(Mutex<HashMap<String, KvEntry>>);
#[async_trait::async_trait]
impl KeyValueStore for MapKvs {
async fn get_bytes(&self, key: &str) -> StorageResult<Option<KvEntry>> {
Ok(self.0.lock().unwrap().get(key).cloned())
}
async fn set_bytes(
&self,
key: &str,
bytes: bytes::Bytes,
content_type: &str,
) -> StorageResult<()> {
self.0.lock().unwrap().insert(
key.into(),
KvEntry {
key: key.into(),
value: bytes,
content_type: content_type.into(),
},
);
Ok(())
}
async fn delete(&self, key: &str) -> StorageResult<()> {
self.0.lock().unwrap().remove(key);
Ok(())
}
async fn list_keys(&self, _: ListKeysOptions) -> StorageResult<KeyList> {
Ok(KeyList {
keys: Vec::new(),
is_truncated: false,
next_exclusive_start_key: None,
})
}
}
fn request(url: &str) -> crate::request::Request {
crate::request::Request::get(url).build().unwrap()
}
#[test]
fn base_key_is_deterministic_and_request_specific() {
let first = request("https://example.com/a");
let same = request("https://example.com/a");
let different = request("https://example.com/b");
assert_eq!(
ErrorSnapshotter::base_key(&first),
ErrorSnapshotter::base_key(&same)
);
assert_ne!(
ErrorSnapshotter::base_key(&first),
ErrorSnapshotter::base_key(&different)
);
}
#[tokio::test]
async fn capture_then_load_round_trips() {
let snapshotter = ErrorSnapshotter::new(Arc::new(MapKvs::default()));
let req = request("https://example.com/a");
let key = snapshotter
.capture(
&req,
"html",
bytes::Bytes::from_static(b"<html></html>"),
"text/html",
)
.await
.unwrap();
assert_eq!(
snapshotter.load(&key).await.unwrap(),
Some(ErrorSnapshot {
content_type: "text/html".into(),
bytes: bytes::Bytes::from_static(b"<html></html>"),
})
);
}
#[tokio::test]
async fn repeated_capture_same_suffix_overwrites() {
let snapshotter = ErrorSnapshotter::new(Arc::new(MapKvs::default()));
let req = request("https://example.com/a");
let first_key = snapshotter
.capture(
&req,
"body",
bytes::Bytes::from_static(b"first"),
"text/plain",
)
.await
.unwrap();
let second_key = snapshotter
.capture(
&req,
"body",
bytes::Bytes::from_static(b"second"),
"application/octet-stream",
)
.await
.unwrap();
assert_eq!(first_key, second_key);
assert_eq!(
snapshotter.load(&first_key).await.unwrap(),
Some(ErrorSnapshot {
content_type: "application/octet-stream".into(),
bytes: bytes::Bytes::from_static(b"second"),
})
);
}
#[tokio::test]
async fn load_missing_key_returns_none() {
let snapshotter = ErrorSnapshotter::new(Arc::new(MapKvs::default()));
assert_eq!(snapshotter.load("missing").await.unwrap(), None);
}
}