use async_trait::async_trait;
use super::entry::ReplayEntry;
#[derive(Debug, thiserror::Error)]
pub enum ReplayStoreError {
#[error("IO error: {0}")]
Io(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Entry not found: {0}")]
NotFound(String),
#[error("Store full")]
StoreFull,
#[error("Store error: {0}")]
Other(String),
}
pub type ReplayStoreResult<T> = Result<T, ReplayStoreError>;
#[derive(Debug, Clone, Default)]
pub struct ReplayQuery {
pub method: Option<String>,
pub path_contains: Option<String>,
pub status_min: Option<u16>,
pub status_max: Option<u16>,
pub from_timestamp: Option<u64>,
pub to_timestamp: Option<u64>,
pub tag: Option<(String, String)>,
pub limit: Option<usize>,
pub offset: Option<usize>,
pub newest_first: bool,
}
impl ReplayQuery {
pub fn new() -> Self {
Self {
newest_first: true,
..Default::default()
}
}
pub fn method(mut self, method: impl Into<String>) -> Self {
self.method = Some(method.into());
self
}
pub fn path_contains(mut self, path: impl Into<String>) -> Self {
self.path_contains = Some(path.into());
self
}
pub fn status_min(mut self, min: u16) -> Self {
self.status_min = Some(min);
self
}
pub fn status_max(mut self, max: u16) -> Self {
self.status_max = Some(max);
self
}
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn matches(&self, entry: &ReplayEntry) -> bool {
if let Some(ref method) = self.method {
if entry.request.method != *method {
return false;
}
}
if let Some(ref path) = self.path_contains {
if !entry.request.path.contains(path.as_str()) {
return false;
}
}
if let Some(min) = self.status_min {
if entry.response.status < min {
return false;
}
}
if let Some(max) = self.status_max {
if entry.response.status > max {
return false;
}
}
if let Some(from) = self.from_timestamp {
if entry.recorded_at < from {
return false;
}
}
if let Some(to) = self.to_timestamp {
if entry.recorded_at > to {
return false;
}
}
if let Some((ref key, ref value)) = self.tag {
match entry.meta.tags.get(key) {
Some(v) if v == value => {}
_ => return false,
}
}
true
}
}
#[async_trait]
pub trait ReplayStore: Send + Sync + 'static {
async fn store(&self, entry: ReplayEntry) -> ReplayStoreResult<()>;
async fn get(&self, id: &str) -> ReplayStoreResult<Option<ReplayEntry>>;
async fn list(&self, query: &ReplayQuery) -> ReplayStoreResult<Vec<ReplayEntry>>;
async fn delete(&self, id: &str) -> ReplayStoreResult<bool>;
async fn count(&self) -> ReplayStoreResult<usize>;
async fn clear(&self) -> ReplayStoreResult<()>;
async fn delete_before(&self, timestamp_ms: u64) -> ReplayStoreResult<usize>;
fn clone_store(&self) -> Box<dyn ReplayStore>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::replay::entry::{RecordedRequest, RecordedResponse};
use crate::replay::meta::ReplayMeta;
fn make_entry(method: &str, path: &str, status: u16) -> ReplayEntry {
ReplayEntry::new(
RecordedRequest::new(method, path, path),
RecordedResponse::new(status),
ReplayMeta::new(),
)
}
#[test]
fn test_query_matches_all() {
let query = ReplayQuery::new();
let entry = make_entry("GET", "/users", 200);
assert!(query.matches(&entry));
}
#[test]
fn test_query_method_filter() {
let query = ReplayQuery::new().method("POST");
assert!(!query.matches(&make_entry("GET", "/users", 200)));
assert!(query.matches(&make_entry("POST", "/users", 201)));
}
#[test]
fn test_query_path_filter() {
let query = ReplayQuery::new().path_contains("/users");
assert!(query.matches(&make_entry("GET", "/users/123", 200)));
assert!(!query.matches(&make_entry("GET", "/items", 200)));
}
#[test]
fn test_query_status_filter() {
let query = ReplayQuery::new().status_min(400).status_max(499);
assert!(!query.matches(&make_entry("GET", "/a", 200)));
assert!(query.matches(&make_entry("GET", "/a", 404)));
assert!(!query.matches(&make_entry("GET", "/a", 500)));
}
#[test]
fn test_query_limit() {
let query = ReplayQuery::new().limit(10);
assert_eq!(query.limit, Some(10));
}
}