use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Recorded {
pub request: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<serde_json::Value>,
pub status: u16,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub link: Option<String>,
pub response: String,
}
pub fn key(method: &str, path: &str, body: Option<&serde_json::Value>) -> String {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
let body = body.map(serde_json::Value::to_string).unwrap_or_default();
for byte in [method, " ", path, " ", &body].concat().bytes() {
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x0100_0000_01b3);
}
format!("{hash:016x}.json")
}
#[derive(Debug, Clone)]
pub struct Recorder(pub PathBuf);
impl Recorder {
pub(crate) fn record(&self, recorded: &Recorded, body: Option<&serde_json::Value>) {
let mut recorded = recorded.clone();
if recorded.request.contains("/compare/")
&& let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&recorded.response)
{
for file in json
.get_mut("files")
.and_then(serde_json::Value::as_array_mut)
.into_iter()
.flatten()
{
if let Some(file) = file.as_object_mut() {
file.remove("patch");
}
}
recorded.response = json.to_string();
}
let recorded = &recorded;
let (method, path) = recorded
.request
.split_once(' ')
.unwrap_or(("GET", &recorded.request));
let file = self.0.join(key(method, path, body));
let written = serde_json::to_string_pretty(recorded)
.map_err(|e| e.to_string())
.and_then(|json| std::fs::write(&file, json).map_err(|e| e.to_string()));
if let Err(err) = written {
tracing::warn!(err, file = %file.display(), "couldn't record a response");
}
}
}
pub fn read(dir: &Path, file: &str) -> Option<Recorded> {
let text = std::fs::read_to_string(dir.join(file)).ok()?;
serde_json::from_str(&text).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keys_are_stable() {
assert_eq!(key("GET", "/user", None), "46fbc3d342fd9773.json");
let body = serde_json::json!({"query": "q", "variables": {"b": 1, "a": 2}});
assert_eq!(
key("POST", "/graphql", Some(&body)),
key("POST", "/graphql", Some(&body.clone()))
);
assert_ne!(key("GET", "/a", None), key("GET", "/b", None));
}
}