use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConformanceViolation {
pub timestamp: DateTime<Utc>,
pub method: String,
pub path: String,
pub client_ip: String,
pub status: u16,
pub reason: String,
pub category: String,
}
const DEFAULT_BUFFER_SIZE: usize = 256;
static VIOLATIONS: Lazy<Mutex<VecDeque<ServerConformanceViolation>>> =
Lazy::new(|| Mutex::new(VecDeque::with_capacity(DEFAULT_BUFFER_SIZE)));
pub fn record(violation: ServerConformanceViolation) {
let mut buf = VIOLATIONS.lock();
if buf.len() == DEFAULT_BUFFER_SIZE {
buf.pop_front();
}
buf.push_back(violation);
}
pub fn snapshot() -> Vec<ServerConformanceViolation> {
let buf = VIOLATIONS.lock();
buf.iter().rev().cloned().collect()
}
pub fn len() -> usize {
VIOLATIONS.lock().len()
}
pub fn clear() {
VIOLATIONS.lock().clear();
}
#[cfg(test)]
mod tests {
use super::*;
fn v(method: &str, status: u16) -> ServerConformanceViolation {
ServerConformanceViolation {
timestamp: Utc::now(),
method: method.to_string(),
path: "/test".into(),
client_ip: "127.0.0.1".into(),
status,
reason: "test".into(),
category: "parameters".into(),
}
}
#[test]
fn record_and_snapshot_in_lifo_order() {
clear();
record(v("GET", 400));
record(v("POST", 422));
let snap = snapshot();
assert_eq!(snap.len(), 2);
assert_eq!(snap[0].method, "POST");
assert_eq!(snap[1].method, "GET");
}
#[test]
fn buffer_drops_oldest_at_capacity() {
clear();
for i in 0..(DEFAULT_BUFFER_SIZE + 50) {
let mut entry = v("GET", 400);
entry.reason = format!("{i}");
record(entry);
}
assert_eq!(len(), DEFAULT_BUFFER_SIZE);
let snap = snapshot();
assert_eq!(snap[0].reason, format!("{}", DEFAULT_BUFFER_SIZE + 50 - 1));
assert_eq!(snap[DEFAULT_BUFFER_SIZE - 1].reason, format!("{}", 50));
}
}