mockforge_http/
mockai_api.rs1use axum::extract::{Json as AxumJson, State};
27use axum::http::StatusCode;
28use axum::response::{IntoResponse, Response};
29use axum::routing::{get, post};
30use axum::{Json, Router};
31use mockforge_core::intelligent_behavior::{
32 IntelligentBehaviorConfig, MockAI, Request as MockAiRequest, StatefulAiContext,
33};
34use serde::{Deserialize, Serialize};
35use std::collections::HashMap;
36use std::sync::Arc;
37use tokio::sync::RwLock;
38use uuid::Uuid;
39
40#[derive(Clone)]
45pub struct MockAiApiState {
46 pub mockai: Option<Arc<RwLock<MockAI>>>,
49}
50
51impl MockAiApiState {
52 pub fn new(mockai: Option<Arc<RwLock<MockAI>>>) -> Self {
54 Self { mockai }
55 }
56}
57
58#[derive(Debug, Deserialize)]
60pub struct GenerateRequest {
61 #[serde(default = "default_method")]
65 pub method: String,
66 pub path: String,
68 #[serde(default)]
70 pub body: Option<serde_json::Value>,
71 #[serde(default)]
73 pub query_params: HashMap<String, String>,
74 #[serde(default)]
76 pub headers: HashMap<String, String>,
77 #[serde(default)]
80 pub session_id: Option<String>,
81}
82
83fn default_method() -> String {
84 "GET".to_string()
85}
86
87#[derive(Debug, Serialize)]
89pub struct GenerateResponseBody {
90 pub status_code: u16,
92 pub body: serde_json::Value,
94 pub headers: HashMap<String, String>,
96 pub session_id: String,
98}
99
100async fn status_handler(State(state): State<MockAiApiState>) -> Response {
101 let available = state.mockai.is_some();
102 Json(serde_json::json!({
103 "available": available,
104 "reason": if available {
105 "MockAI is configured and ready"
106 } else {
107 "MockAI is not configured (missing API key or no model attached)"
108 },
109 }))
110 .into_response()
111}
112
113async fn generate_handler(
114 State(state): State<MockAiApiState>,
115 AxumJson(req): AxumJson<GenerateRequest>,
116) -> Response {
117 let Some(mockai) = state.mockai.clone() else {
118 return (
119 StatusCode::SERVICE_UNAVAILABLE,
120 Json(serde_json::json!({
121 "error": "mockai_unavailable",
122 "message": "MockAI is not configured. Set a provider API key (OPENAI_API_KEY) and redeploy.",
123 })),
124 )
125 .into_response();
126 };
127
128 if req.path.is_empty() {
129 return (
130 StatusCode::BAD_REQUEST,
131 Json(serde_json::json!({
132 "error": "missing_path",
133 "message": "`path` is required",
134 })),
135 )
136 .into_response();
137 }
138
139 let session_id = req.session_id.unwrap_or_else(|| Uuid::new_v4().to_string());
140 let context = StatefulAiContext::new(session_id.clone(), IntelligentBehaviorConfig::default());
143
144 let mockai_request = MockAiRequest {
145 method: req.method,
146 path: req.path,
147 body: req.body,
148 query_params: req.query_params,
149 headers: req.headers,
150 };
151
152 let guard = mockai.read().await;
153 let result = guard.generate_response(&mockai_request, &context).await;
154 drop(guard);
155
156 match result {
157 Ok(resp) => Json(GenerateResponseBody {
158 status_code: resp.status_code,
159 body: resp.body,
160 headers: resp.headers,
161 session_id,
162 })
163 .into_response(),
164 Err(e) => (
165 StatusCode::INTERNAL_SERVER_ERROR,
166 Json(serde_json::json!({
167 "error": "mockai_generate_failed",
168 "message": e.to_string(),
169 })),
170 )
171 .into_response(),
172 }
173}
174
175pub fn mockai_api_router(state: MockAiApiState) -> Router {
178 Router::new()
179 .route("/status", get(status_handler))
180 .route("/generate", post(generate_handler))
181 .with_state(state)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[tokio::test]
189 async fn status_reports_unavailable_when_no_mockai() {
190 let state = MockAiApiState::new(None);
191 let resp = status_handler(State(state)).await;
192 assert_eq!(resp.status(), StatusCode::OK);
193 let body = axum::body::to_bytes(resp.into_body(), 1024).await.unwrap();
194 let s = std::str::from_utf8(&body).unwrap();
195 assert!(s.contains("\"available\":false"));
196 }
197
198 #[tokio::test]
199 async fn generate_returns_503_when_no_mockai() {
200 let state = MockAiApiState::new(None);
201 let req = GenerateRequest {
202 method: "GET".into(),
203 path: "/users/42".into(),
204 body: None,
205 query_params: HashMap::new(),
206 headers: HashMap::new(),
207 session_id: None,
208 };
209 let resp = generate_handler(State(state), AxumJson(req)).await;
210 assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
211 }
212
213 #[test]
214 fn default_method_is_get() {
215 assert_eq!(default_method(), "GET");
216 }
217}