use crate::redact::Redactor;
use parking_lot::Mutex;
use serde_json::Value;
use similar::TextDiff;
use std::collections::BTreeMap;
use std::sync::Arc;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct TappedCall {
pub timestamp: SystemTime,
pub method: String,
pub url: String,
pub request_headers: BTreeMap<String, String>,
pub request_body: Option<Value>,
pub response_status: u16,
pub response_headers: BTreeMap<String, String>,
pub response_body: Option<Value>,
pub elapsed_ms: u64,
}
impl TappedCall {
pub fn pretty_request(&self) -> String {
let body = self
.request_body
.as_ref()
.map(|v| serde_json::to_string_pretty(v).unwrap_or_default())
.unwrap_or_default();
format!("{} {}\n\n{}", self.method, self.url, body)
}
}
#[derive(Clone)]
pub struct Tap {
redactor: Arc<Redactor>,
history_size: usize,
history: Arc<Mutex<Vec<TappedCall>>>,
}
impl Tap {
pub fn new() -> Self {
Self::with_capacity(1024)
}
pub fn with_capacity(history_size: usize) -> Self {
Self {
redactor: Arc::new(Redactor::default()),
history_size,
history: Arc::new(Mutex::new(Vec::with_capacity(history_size.min(64)))),
}
}
pub fn with_redactor(mut self, r: Redactor) -> Self {
self.redactor = Arc::new(r);
self
}
pub fn redactor(&self) -> &Redactor {
&self.redactor
}
pub fn last(&self) -> Option<TappedCall> {
self.history.lock().last().cloned()
}
pub fn all(&self) -> Vec<TappedCall> {
self.history.lock().clone()
}
pub fn reset(&self) {
self.history.lock().clear();
}
#[allow(clippy::too_many_arguments)]
pub fn record<HR, HS>(
&self,
method: impl Into<String>,
url: impl Into<String>,
request_headers: HR,
request_body: Option<Value>,
response_status: u16,
response_headers: HS,
response_body: Option<Value>,
elapsed_ms: u64,
) -> TappedCall
where
HR: IntoIterator<Item = (String, String)>,
HS: IntoIterator<Item = (String, String)>,
{
let call = TappedCall {
timestamp: SystemTime::now(),
method: method.into(),
url: url.into(),
request_headers: self.redactor.headers(request_headers),
request_body: request_body.map(|b| self.redactor.body(b)),
response_status,
response_headers: self.redactor.headers(response_headers),
response_body: response_body.map(|b| self.redactor.body(b)),
elapsed_ms,
};
let mut history = self.history.lock();
history.push(call.clone());
let excess = history.len().saturating_sub(self.history_size);
if excess > 0 {
history.drain(0..excess);
}
call
}
}
impl Default for Tap {
fn default() -> Self {
Self::new()
}
}
pub fn diff(a: &TappedCall, b: &TappedCall) -> String {
let pretty = |c: &TappedCall| -> String {
c.request_body
.as_ref()
.map(|v| serde_json::to_string_pretty(v).unwrap_or_default())
.unwrap_or_default()
};
let a_text = pretty(a);
let b_text = pretty(b);
TextDiff::from_lines(&a_text, &b_text)
.unified_diff()
.header("a", "b")
.to_string()
}