use std::collections::VecDeque;
use std::fmt;
use std::sync::{Arc, Mutex};
use bytes::Bytes;
use futures_util::StreamExt;
use crate::{
Body, Headers, HttpRequest, HttpResponse, HttpTransport, SendFuture, TransportError,
TransportErrorKind,
};
#[derive(Clone, Default)]
pub struct ReplayTransport {
state: Arc<Mutex<State>>,
}
#[derive(Default)]
struct State {
responses: VecDeque<ReplayResponse>,
requests: Vec<HttpRequest>,
}
impl fmt::Debug for ReplayTransport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = self.lock();
f.debug_struct("ReplayTransport")
.field("remaining", &state.responses.len())
.field("requests", &state.requests.len())
.finish()
}
}
impl ReplayTransport {
#[must_use]
pub fn new(responses: impl IntoIterator<Item = ReplayResponse>) -> Self {
Self {
state: Arc::new(Mutex::new(State {
responses: responses.into_iter().collect(),
requests: Vec::new(),
})),
}
}
pub fn push(&self, response: ReplayResponse) {
self.lock().responses.push_back(response);
}
#[must_use]
pub fn requests(&self) -> Vec<HttpRequest> {
self.lock().requests.clone()
}
#[must_use]
pub fn remaining(&self) -> usize {
self.lock().responses.len()
}
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
}
impl HttpTransport for ReplayTransport {
fn send(&self, request: HttpRequest) -> SendFuture<'_> {
let next = {
let mut state = self.lock();
state.requests.push(request);
state.responses.pop_front()
};
Box::pin(async move {
let response = next.ok_or_else(|| {
TransportError::new(TransportErrorKind::Connect, "no replayed response left")
})?;
if let Some((kind, message)) = response.failure {
return Err(TransportError::new(kind, message));
}
let chunks = futures_util::stream::iter(response.chunks.into_iter().map(Ok));
let body: Body = if response.stalls {
Box::pin(chunks.chain(futures_util::stream::pending()))
} else {
Box::pin(chunks)
};
Ok(HttpResponse::new(response.status, response.headers, body))
})
}
}
#[derive(Debug, Clone)]
pub struct ReplayResponse {
status: u16,
headers: Headers,
chunks: Vec<Bytes>,
failure: Option<(TransportErrorKind, String)>,
stalls: bool,
}
impl ReplayResponse {
#[must_use]
pub fn new(status: u16, body: impl Into<Bytes>) -> Self {
Self::chunks(status, [body])
}
#[must_use]
pub fn chunks(status: u16, chunks: impl IntoIterator<Item = impl Into<Bytes>>) -> Self {
Self {
status,
headers: Headers::new(),
chunks: chunks.into_iter().map(Into::into).collect(),
failure: None,
stalls: false,
}
}
#[must_use]
pub fn json(status: u16, body: impl Into<Bytes>) -> Self {
Self::new(status, body).header("content-type", "application/json")
}
#[must_use]
pub fn json_value(status: u16, value: &impl serde::Serialize) -> Self {
let body = serde_json::to_vec(value).expect("serialize replayed JSON response");
Self::json(status, body)
}
#[must_use]
pub fn sse(body: impl AsRef<str>) -> Self {
let chunks = body
.as_ref()
.split_inclusive("\n\n")
.map(|event| Bytes::from(event.to_owned()));
Self::chunks(200, chunks).header("content-type", "text/event-stream")
}
#[must_use]
pub fn ndjson(body: impl AsRef<str>) -> Self {
let chunks = body
.as_ref()
.split_inclusive('\n')
.map(|line| Bytes::from(line.to_owned()));
Self::chunks(200, chunks).header("content-type", "application/x-ndjson")
}
#[must_use]
pub fn failure(kind: TransportErrorKind, message: impl Into<String>) -> Self {
Self {
status: 0,
headers: Headers::new(),
chunks: Vec::new(),
failure: Some((kind, message.into())),
stalls: false,
}
}
#[must_use]
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(name, value);
self
}
#[must_use]
pub fn split_every(mut self, size: usize) -> Self {
assert!(
size > 0,
"a replayed body cannot be split into empty chunks"
);
let body: Vec<u8> = self.chunks.concat();
self.chunks = body.chunks(size).map(Bytes::copy_from_slice).collect();
self
}
#[must_use]
pub fn then_stall(mut self) -> Self {
self.stalls = true;
self
}
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use futures_util::StreamExt;
use super::*;
#[tokio::test]
async fn responses_are_served_in_order_and_requests_recorded() {
let transport = ReplayTransport::new([
ReplayResponse::json(200, r#"{"n":1}"#).header("request-id", "req_1"),
ReplayResponse::failure(TransportErrorKind::Timeout, "slow"),
]);
let first = transport
.send(HttpRequest::post("https://x/a", "1"))
.await
.unwrap();
assert_eq!(first.headers.get("Request-Id"), Some("req_1"));
assert_eq!(first.text().await.unwrap(), r#"{"n":1}"#);
let second = transport
.send(HttpRequest::get("https://x/b"))
.await
.unwrap_err();
assert_eq!(second.kind(), TransportErrorKind::Timeout);
let exhausted = transport
.send(HttpRequest::get("https://x/c"))
.await
.unwrap_err();
assert_eq!(exhausted.kind(), TransportErrorKind::Connect);
let urls: Vec<_> = transport.requests().into_iter().map(|r| r.url).collect();
assert_eq!(urls, ["https://x/a", "https://x/b", "https://x/c"]);
assert_eq!(transport.remaining(), 0);
}
#[tokio::test]
async fn sse_is_chunked_per_event() {
let transport = ReplayTransport::default();
transport.push(ReplayResponse::sse("data: a\n\ndata: b\n\n"));
let response = transport.send(HttpRequest::get("https://x")).await.unwrap();
assert_eq!(
response.headers.get("content-type"),
Some("text/event-stream")
);
let chunks: Vec<_> = response.into_body().collect().await;
assert_eq!(chunks.len(), 2);
}
#[tokio::test]
async fn bodies_arrive_in_the_chunks_they_were_built_with_or_split() {
let body = |response: ReplayResponse| async {
let transport = ReplayTransport::new([response]);
let response = transport.send(HttpRequest::get("https://x")).await.unwrap();
let chunks: Vec<Bytes> = response.into_body().map(Result::unwrap).collect().await;
chunks
};
assert_eq!(
body(ReplayResponse::chunks(200, ["ab", "", "cde"])).await,
["ab", "", "cde"]
);
assert_eq!(
body(ReplayResponse::chunks(200, ["ab", "", "cde"]).split_every(2)).await,
["ab", "cd", "e"]
);
assert_eq!(
body(ReplayResponse::new(200, "héllo").split_every(1))
.await
.len(),
"héllo".len()
);
assert!(
body(ReplayResponse::new(200, "").split_every(3))
.await
.is_empty()
);
}
#[test]
fn json_takes_the_body_as_written_and_json_value_serializes() {
let written = ReplayResponse::json(400, "{not json");
assert_eq!(written.chunks, ["{not json"]);
let value = ReplayResponse::json_value(200, &serde_json::json!({"ok": true}));
assert_eq!(value.chunks, [r#"{"ok":true}"#]);
assert_eq!(value.headers.get("content-type"), Some("application/json"));
}
#[tokio::test]
async fn stalled_bodies_never_end() {
let transport = ReplayTransport::new([ReplayResponse::sse("data: a\n\n").then_stall()]);
let mut body = transport
.send(HttpRequest::get("https://x"))
.await
.unwrap()
.into_body();
assert!(body.next().await.is_some());
let next = futures_util::future::select(
body.next(),
futures_timer::Delay::new(std::time::Duration::from_millis(20)),
)
.await;
assert!(matches!(next, futures_util::future::Either::Right(_)));
}
}