dstest 0.1.6

Deterministic Simulation Testing for containerised services
use std::sync::Arc;
use std::time::Duration;

use mlua::{Lua, Result, Table};

use crate::engine::context::BindingContext;
use crate::substrate::Substrate;

pub fn register<S: Substrate>(lua: &Lua, dstest: &Table, ctx: &BindingContext<S>) -> Result<()> {
    let state = Arc::clone(&ctx.state);
    let config = Arc::clone(&ctx.config);

    let http_fn =
        lua.create_async_function(move |lua, (id, method, path): (String, String, String)| {
            let state = Arc::clone(&state);
            let config = Arc::clone(&config);

            async move {
                let (timeout, retries, delay) = {
                    let cfg = config.lock().expect("poisoned config lock");
                    (
                        cfg.http_timeout_secs,
                        cfg.http_retries,
                        cfg.http_retry_delay_ms,
                    )
                };

                let host = {
                    let state = state.lock().expect("poisoned engine state lock");
                    state.subject_hosts.get(&id).cloned().ok_or_else(|| {
                        mlua::Error::RuntimeError(format!("unknown subject {}", id))
                    })?
                };

                let url = format!("http://{host}{path}");
                let client = reqwest::Client::builder()
                    .timeout(Duration::from_secs(timeout))
                    .build()
                    .map_err(|e| {
                        mlua::Error::RuntimeError(format!("failed to create HTTP client: {}", e))
                    })?;

                let req_method: reqwest::Method = method
                    .parse()
                    .map_err(|e| mlua::Error::RuntimeError(format!("invalid method: {}", e)))?;

                let mut last_err = None;

                for attempt in 0..retries {
                    match client.request(req_method.clone(), &url).send().await {
                        Ok(resp) => {
                            let status = resp.status().as_u16();
                            let body = resp.text().await.map_err(|e| {
                                mlua::Error::RuntimeError(format!("body read failed: {}", e))
                            })?;
                            let t = lua.create_table()?;
                            t.set("status", status)?;
                            t.set("body", body)?;
                            return Ok(t);
                        }
                        Err(e) => {
                            last_err = Some(e);
                            if attempt < retries - 1 {
                                tokio::time::sleep(Duration::from_millis(delay)).await;
                            }
                        }
                    }
                }

                let e = last_err.unwrap();
                Err(mlua::Error::RuntimeError(format!(
                    "HTTP failed after {} retries: {}",
                    retries, e
                )))
            }
        })?;

    dstest.set("http", http_fn)?;
    Ok(())
}