alef 0.24.17

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Rendering for the generated Rust tests/mock_server.rs module.

use crate::core::hash::{self, CommentStyle};

/// Generate the complete `mock_server.rs` module source.
pub fn render_mock_server_module() -> String {
    // This is parameterized Axum mock server code identical in structure to
    // a generated mock_server.rs without any project-specific imports.
    //
    // The module is included via `mod mock_server;` in every integration-test
    // binary that needs MockRoute/MockServer, but only fixtures using
    // `MockServer::start` actually invoke `start()` / `handle_request()`. Each
    // integration test compiles as a separate binary, so the unused-in-some
    // helpers would otherwise trip `-D dead_code` under
    // `cargo test`/`cargo clippy`. The crate-level `#![allow(dead_code)]`
    // mirrors the pattern used by other generated helper modules
    // (e.g. `tests/common.rs`).
    hash::header(CommentStyle::DoubleSlash)
        + r#"//
// Minimal axum-based mock HTTP server for e2e tests.

#![allow(dead_code)]

use std::net::SocketAddr;
use std::sync::Arc;

use axum::Router;
use axum::body::Body;
use axum::extract::State;
use axum::http::{Request, StatusCode};
use axum::response::{IntoResponse, Response};
use tokio::net::TcpListener;

/// A single mock route: match by path + method, return a configured response.
#[derive(Clone, Debug)]
pub struct MockRoute {
    /// URL path to match, e.g. `"/v1/chat/completions"`.
    pub path: &'static str,
    /// HTTP method to match, e.g. `"POST"` or `"GET"`.
    pub method: &'static str,
    /// HTTP status code to return.
    pub status: u16,
    /// Response body JSON string (used when `is_streaming` is false).
    pub body: String,
    /// Whether this route serves a streaming SSE response.
    /// True even when `stream_chunks` is empty (e.g. an empty stream still sends
    /// `data: [DONE]\n\n` with the correct `content-type: text/event-stream` header).
    pub is_streaming: bool,
    /// Ordered SSE data payloads for streaming responses.
    /// Each entry becomes `data: <chunk>\n\n` in the response.
    /// A final `data: [DONE]\n\n` is always appended.
    pub stream_chunks: Vec<String>,
    /// Response headers to apply (name, value) pairs.
    /// Multiple entries with the same name produce multiple header lines.
    pub headers: Vec<(String, String)>,
    /// Optional artificial response delay in milliseconds. Used by timeout-error
    /// fixtures to force a client-side request timeout.
    pub delay_ms: Option<u64>,
}

struct ServerState {
    routes: Vec<MockRoute>,
}

pub struct MockServer {
    /// Base URL of the mock server, e.g. `"http://127.0.0.1:54321"`.
    pub url: String,
    handle: tokio::task::JoinHandle<()>,
}

impl MockServer {
    /// Start a mock server with the given routes.  Binds to a random port on
    /// localhost and returns immediately once the server is listening.
    pub async fn start(routes: Vec<MockRoute>) -> Self {
        let state = Arc::new(ServerState { routes });

        let app = Router::new().fallback(handle_request).with_state(state);

        let listener = TcpListener::bind("127.0.0.1:0")
            .await
            .expect("Failed to bind mock server port");
        let addr: SocketAddr = listener.local_addr().expect("Failed to get local addr");
        let url = format!("http://{addr}");

        let handle = tokio::spawn(async move {
            axum::serve(listener, app).await.expect("Mock server failed");
        });

        MockServer { url, handle }
    }

    /// Stop the mock server.
    pub fn shutdown(self) {
        self.handle.abort();
    }
}

impl Drop for MockServer {
    fn drop(&mut self) {
        self.handle.abort();
    }
}

async fn handle_request(State(state): State<Arc<ServerState>>, req: Request<Body>) -> Response {
    let path = req.uri().path().to_owned();
    let method = req.method().as_str().to_uppercase();

    for route in &state.routes {
        // Match on method and either exact path or path prefix (route.path is a prefix of the
        // request path, separated by a '/' boundary). This allows a single route registered at
        // "/v1/batches" to match requests to "/v1/batches/abc123" or
        // "/v1/batches/abc123/cancel".
        let path_matches = path == route.path
            || (path.starts_with(route.path)
                && path.as_bytes().get(route.path.len()) == Some(&b'/'));
        if path_matches && route.method.to_uppercase() == method {
            if let Some(delay_ms) = route.delay_ms {
                tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
            }
            let status =
                StatusCode::from_u16(route.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);

            if route.is_streaming {
                // Build SSE body: data: <chunk>\n\n ... data: [DONE]\n\n
                // Note: stream_chunks may be empty for an empty-stream fixture; we still
                // emit `data: [DONE]\n\n` with the correct SSE headers so clients do not hang.
                let mut sse = String::new();
                for chunk in &route.stream_chunks {
                    sse.push_str("data: ");
                    sse.push_str(chunk);
                    sse.push_str("\n\n");
                }
                sse.push_str("data: [DONE]\n\n");

                let mut builder = Response::builder()
                    .status(status)
                    .header("content-type", "text/event-stream")
                    .header("cache-control", "no-cache");
                for (name, value) in &route.headers {
                    builder = builder.header(name, value);
                }
                return builder.body(Body::from(sse)).unwrap().into_response();
            }

            let mut builder =
                Response::builder().status(status).header("content-type", "application/json");
            for (name, value) in &route.headers {
                builder = builder.header(name, value);
            }
            return builder.body(Body::from(route.body.clone())).unwrap().into_response();
        }
    }

    // No matching route → 404.
    Response::builder()
        .status(StatusCode::NOT_FOUND)
        .body(Body::from(format!("No mock route for {method} {path}")))
        .unwrap()
        .into_response()
}
"#
}