use std::path::Path;
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::provider::{CompletionRequest, CompletionResponse, Provider};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct Exchange {
pub(crate) request: CompletionRequest,
pub(crate) response: CompletionResponse,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Recording {
pub(crate) harness: String,
pub(crate) exchanges: Vec<Exchange>,
}
fn series(version: &str) -> String {
version.split('.').take(2).collect::<Vec<_>>().join(".")
}
impl Recording {
fn new(exchanges: Vec<Exchange>) -> Self {
Self {
harness: env!("CARGO_PKG_VERSION").to_string(),
exchanges,
}
}
fn save(&self, path: &Path) -> Result<()> {
let bytes = serde_json::to_vec_pretty(self)
.map_err(|e| Error::Config(format!("cannot serialise the recording: {e}")))?;
std::fs::write(path, bytes)?;
Ok(())
}
pub(crate) fn load(path: &Path) -> Result<Self> {
let bytes = std::fs::read(path)?;
let recording: Self = serde_json::from_slice(&bytes).map_err(|e| {
Error::Config(format!(
"{} is not a readable io-harness recording: {e}",
path.display()
))
})?;
let current = env!("CARGO_PKG_VERSION");
if series(&recording.harness) != series(current) {
return Err(Error::Config(format!(
"{} was recorded by io-harness {} and this build is {current}: refusing to \
replay across a series, because a recording read by a build whose request or \
response shape changed replays something other than what was recorded",
path.display(),
recording.harness,
)));
}
Ok(recording)
}
}
#[derive(Debug)]
pub struct Record<P> {
inner: P,
seen: Mutex<Vec<Exchange>>,
}
impl<P: Provider> Record<P> {
pub fn new(inner: P) -> Self {
Self {
inner,
seen: Mutex::new(Vec::new()),
}
}
pub fn save(&self, path: impl AsRef<Path>) -> Result<()> {
Recording::new(self.exchanges()).save(path.as_ref())
}
fn exchanges(&self) -> Vec<Exchange> {
self.seen.lock().unwrap_or_else(|e| e.into_inner()).clone()
}
}
impl<P: Provider + Sync> Provider for Record<P> {
async fn complete(&self, request: CompletionRequest) -> Result<CompletionResponse> {
let response = self.inner.complete(request.clone()).await;
if let Ok(response) = &response {
self.seen
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(Exchange {
request,
response: response.clone(),
});
}
response
}
fn name(&self) -> &str {
self.inner.name()
}
fn endpoint(&self) -> Option<&str> {
self.inner.endpoint()
}
fn endpoints(&self) -> Vec<&str> {
self.inner.endpoints()
}
fn last_served(&self) -> Option<String> {
self.inner.last_served()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_recording_is_refused_across_a_series_and_accepted_within_one() {
assert_eq!(series("0.12.0"), series("0.12.7"));
assert_ne!(series("0.11.0"), series("0.12.0"));
assert_eq!(series("nightly"), "nightly");
}
}