use axum::extract::{Json as AxumJson, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::routing::{get, post};
use axum::{Json, Router};
use mockforge_core::intelligent_behavior::{
IntelligentBehaviorConfig, MockAI, Request as MockAiRequest, StatefulAiContext,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use uuid::Uuid;
#[derive(Clone)]
pub struct MockAiApiState {
pub mockai: Option<Arc<RwLock<MockAI>>>,
}
impl MockAiApiState {
pub fn new(mockai: Option<Arc<RwLock<MockAI>>>) -> Self {
Self { mockai }
}
}
#[derive(Debug, Deserialize)]
pub struct GenerateRequest {
#[serde(default = "default_method")]
pub method: String,
pub path: String,
#[serde(default)]
pub body: Option<serde_json::Value>,
#[serde(default)]
pub query_params: HashMap<String, String>,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub session_id: Option<String>,
}
fn default_method() -> String {
"GET".to_string()
}
#[derive(Debug, Serialize)]
pub struct GenerateResponseBody {
pub status_code: u16,
pub body: serde_json::Value,
pub headers: HashMap<String, String>,
pub session_id: String,
}
async fn status_handler(State(state): State<MockAiApiState>) -> Response {
let available = state.mockai.is_some();
Json(serde_json::json!({
"available": available,
"reason": if available {
"MockAI is configured and ready"
} else {
"MockAI is not configured (missing API key or no model attached)"
},
}))
.into_response()
}
async fn generate_handler(
State(state): State<MockAiApiState>,
AxumJson(req): AxumJson<GenerateRequest>,
) -> Response {
let Some(mockai) = state.mockai.clone() else {
return (
StatusCode::SERVICE_UNAVAILABLE,
Json(serde_json::json!({
"error": "mockai_unavailable",
"message": "MockAI is not configured. Set a provider API key (OPENAI_API_KEY) and redeploy.",
})),
)
.into_response();
};
if req.path.is_empty() {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": "missing_path",
"message": "`path` is required",
})),
)
.into_response();
}
let session_id = req.session_id.unwrap_or_else(|| Uuid::new_v4().to_string());
let context = StatefulAiContext::new(session_id.clone(), IntelligentBehaviorConfig::default());
let mockai_request = MockAiRequest {
method: req.method,
path: req.path,
body: req.body,
query_params: req.query_params,
headers: req.headers,
};
let guard = mockai.read().await;
let result = guard.generate_response(&mockai_request, &context).await;
drop(guard);
match result {
Ok(resp) => Json(GenerateResponseBody {
status_code: resp.status_code,
body: resp.body,
headers: resp.headers,
session_id,
})
.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "mockai_generate_failed",
"message": e.to_string(),
})),
)
.into_response(),
}
}
pub fn mockai_api_router(state: MockAiApiState) -> Router {
Router::new()
.route("/status", get(status_handler))
.route("/generate", post(generate_handler))
.with_state(state)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn status_reports_unavailable_when_no_mockai() {
let state = MockAiApiState::new(None);
let resp = status_handler(State(state)).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
let s = std::str::from_utf8(&body).unwrap();
assert!(s.contains("\"available\":false"));
}
#[tokio::test]
async fn generate_returns_503_when_no_mockai() {
let state = MockAiApiState::new(None);
let req = GenerateRequest {
method: "GET".into(),
path: "/users/42".into(),
body: None,
query_params: HashMap::new(),
headers: HashMap::new(),
session_id: None,
};
let resp = generate_handler(State(state), AxumJson(req)).await;
assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[test]
fn default_method_is_get() {
assert_eq!(default_method(), "GET");
}
}