use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use flate2::read::GzDecoder;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct CapturedRequest {
#[allow(dead_code)]
pub method: String,
pub path: String,
pub headers: Vec<(String, String)>,
pub body: Option<Value>,
}
impl CapturedRequest {
pub fn header(&self, name: &str) -> Option<&str> {
let name = name.to_ascii_lowercase();
self.headers
.iter()
.find(|(k, _)| k.to_ascii_lowercase() == name)
.map(|(_, v)| v.as_str())
}
}
pub struct StubResponse {
pub status: u16,
pub body: String,
}
impl StubResponse {
pub fn envelope(data: Value) -> Self {
Self::envelope_status(data, 200)
}
pub fn envelope_status(data: Value, status: u16) -> Self {
let body = serde_json::json!({
"success": status < 400,
"data": data,
"meta": { "request_id": "stub" },
});
Self {
status,
body: body.to_string(),
}
}
pub fn raw(body: Value) -> Self {
Self::raw_status(body, 200)
}
pub fn raw_status(body: Value, status: u16) -> Self {
Self {
status,
body: body.to_string(),
}
}
}
pub struct StubServer {
pub url: String,
requests: Arc<Mutex<Vec<CapturedRequest>>>,
running: Arc<AtomicBool>,
handle: Option<thread::JoinHandle<()>>,
}
impl StubServer {
pub fn requests(&self) -> Vec<CapturedRequest> {
self.requests.lock().unwrap().clone()
}
pub fn requests_for(&self, path: &str) -> Vec<CapturedRequest> {
self.requests()
.into_iter()
.filter(|r| r.path == path)
.collect()
}
}
impl Drop for StubServer {
fn drop(&mut self) {
self.running.store(false, Ordering::SeqCst);
if let Ok(addr) = self.url.trim_start_matches("http://").parse::<std::net::SocketAddr>() {
let _ = TcpStream::connect(addr);
}
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
pub fn start_stub<F>(handler: F) -> StubServer
where
F: Fn(&CapturedRequest, usize) -> StubResponse + Send + Sync + 'static,
{
let listener = TcpListener::bind("127.0.0.1:0").expect("bind stub server");
let addr = listener.local_addr().unwrap();
let url = format!("http://127.0.0.1:{}", addr.port());
let requests = Arc::new(Mutex::new(Vec::new()));
let running = Arc::new(AtomicBool::new(true));
let hits = Arc::new(AtomicUsize::new(0));
let handler = Arc::new(handler);
let requests_t = requests.clone();
let running_t = running.clone();
let handle = thread::spawn(move || {
for stream in listener.incoming() {
if !running_t.load(Ordering::SeqCst) {
break;
}
let stream = match stream {
Ok(s) => s,
Err(_) => continue,
};
let hit = hits.fetch_add(1, Ordering::SeqCst);
if let Some(req) = handle_connection(&stream, &requests_t) {
let resp = handler(&req, hit);
write_response(&stream, &resp);
}
}
});
StubServer {
url,
requests,
running,
handle: Some(handle),
}
}
fn handle_connection(
stream: &TcpStream,
requests: &Arc<Mutex<Vec<CapturedRequest>>>,
) -> Option<CapturedRequest> {
let mut reader = BufReader::new(stream.try_clone().ok()?);
let mut line = String::new();
if reader.read_line(&mut line).ok()? == 0 {
return None;
}
let mut parts = line.split_whitespace();
let method = parts.next().unwrap_or("GET").to_string();
let path = parts.next().unwrap_or("/").to_string();
let mut headers: Vec<(String, String)> = Vec::new();
let mut content_length = 0usize;
let mut gzipped = false;
loop {
let mut h = String::new();
if reader.read_line(&mut h).ok()? == 0 {
break;
}
let trimmed = h.trim_end();
if trimmed.is_empty() {
break;
}
if let Some((k, v)) = trimmed.split_once(':') {
let k = k.trim().to_string();
let v = v.trim().to_string();
let kl = k.to_ascii_lowercase();
if kl == "content-length" {
content_length = v.parse().unwrap_or(0);
} else if kl == "content-encoding" && v.to_ascii_lowercase().contains("gzip") {
gzipped = true;
}
headers.push((k, v));
}
}
let mut raw = vec![0u8; content_length];
if content_length > 0 {
reader.read_exact(&mut raw).ok()?;
}
if gzipped && raw.len() > 1 && raw[0] == 0x1f && raw[1] == 0x8b {
let mut dec = GzDecoder::new(&raw[..]);
let mut out = Vec::new();
if dec.read_to_end(&mut out).is_ok() {
raw = out;
}
}
let body = if raw.is_empty() {
None
} else {
let text = String::from_utf8_lossy(&raw);
serde_json::from_str::<Value>(&text)
.ok()
.or_else(|| Some(Value::String(text.to_string())))
};
let captured = CapturedRequest {
method,
path,
headers,
body,
};
requests.lock().unwrap().push(captured.clone());
Some(captured)
}
fn write_response(mut stream: &TcpStream, resp: &StubResponse) {
let reason = match resp.status {
200 => "OK",
400 => "Bad Request",
404 => "Not Found",
500 => "Internal Server Error",
503 => "Service Unavailable",
_ => "Status",
};
let out = format!(
"HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
resp.status,
reason,
resp.body.as_bytes().len(),
resp.body
);
let _ = stream.write_all(out.as_bytes());
let _ = stream.flush();
}