1use mockforge_core::ResourceIdExtract as CoreResourceIdExtract;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use std::collections::HashMap;
7use std::sync::Arc;
8use tokio::sync::RwLock;
9
10pub type DynamicResponseFn = Arc<dyn Fn(&RequestContext) -> Value + Send + Sync>;
12
13#[derive(Debug, Clone)]
15pub struct RequestContext {
16 pub method: String,
18 pub path: String,
20 pub path_params: HashMap<String, String>,
22 pub query_params: HashMap<String, String>,
24 pub headers: HashMap<String, String>,
26 pub body: Option<Value>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct StateMachineConfig {
33 pub resource_type: String,
35 #[serde(flatten)]
37 pub resource_id_extract: ResourceIdExtractConfig,
38 pub initial_state: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
43 pub state_responses: Option<HashMap<String, StateResponseOverride>>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(tag = "extract_type", rename_all = "snake_case")]
49pub enum ResourceIdExtractConfig {
50 PathParam {
52 param: String,
54 },
55 JsonPath {
57 path: String,
59 },
60 Header {
62 name: String,
64 },
65 QueryParam {
67 param: String,
69 },
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct StateResponseOverride {
75 #[serde(skip_serializing_if = "Option::is_none")]
77 pub status: Option<u16>,
78 #[serde(skip_serializing_if = "Option::is_none")]
80 pub body: Option<Value>,
81 #[serde(skip_serializing_if = "Option::is_none")]
83 pub headers: Option<HashMap<String, String>>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct StubFaultInjectionConfig {
89 #[serde(default)]
91 pub enabled: bool,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub http_errors: Option<Vec<u16>>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub http_error_probability: Option<f64>,
98 #[serde(default)]
100 pub timeout_error: bool,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub timeout_ms: Option<u64>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub timeout_probability: Option<f64>,
107 #[serde(default)]
109 pub connection_error: bool,
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub connection_error_probability: Option<f64>,
113}
114
115impl Default for StubFaultInjectionConfig {
116 fn default() -> Self {
117 Self {
118 enabled: false,
119 http_errors: None,
120 http_error_probability: None,
121 timeout_error: false,
122 timeout_ms: None,
123 timeout_probability: None,
124 connection_error: false,
125 connection_error_probability: None,
126 }
127 }
128}
129
130impl StubFaultInjectionConfig {
131 pub fn http_error(codes: Vec<u16>) -> Self {
133 Self {
134 enabled: true,
135 http_errors: Some(codes),
136 http_error_probability: Some(1.0),
137 ..Default::default()
138 }
139 }
140
141 pub fn timeout(ms: u64) -> Self {
143 Self {
144 enabled: true,
145 timeout_error: true,
146 timeout_ms: Some(ms),
147 timeout_probability: Some(1.0),
148 ..Default::default()
149 }
150 }
151
152 pub fn connection_error() -> Self {
154 Self {
155 enabled: true,
156 connection_error: true,
157 connection_error_probability: Some(1.0),
158 ..Default::default()
159 }
160 }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct ResponseStub {
166 pub method: String,
168 pub path: String,
170 pub status: u16,
172 pub headers: HashMap<String, String>,
174 pub body: Value,
176 pub latency_ms: Option<u64>,
178 #[serde(skip_serializing_if = "Option::is_none")]
181 pub state_machine: Option<StateMachineConfig>,
182 #[serde(skip_serializing_if = "Option::is_none")]
185 pub fault_injection: Option<StubFaultInjectionConfig>,
186}
187
188impl ResponseStub {
189 pub fn new(method: impl Into<String>, path: impl Into<String>, body: Value) -> Self {
191 Self {
192 method: method.into(),
193 path: path.into(),
194 status: 200,
195 headers: HashMap::new(),
196 body,
197 latency_ms: None,
198 state_machine: None,
199 fault_injection: None,
200 }
201 }
202
203 pub fn status(mut self, status: u16) -> Self {
205 self.status = status;
206 self
207 }
208
209 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
211 self.headers.insert(key.into(), value.into());
212 self
213 }
214
215 pub fn latency(mut self, ms: u64) -> Self {
217 self.latency_ms = Some(ms);
218 self
219 }
220
221 pub fn with_state_machine(mut self, config: StateMachineConfig) -> Self {
223 self.state_machine = Some(config);
224 self
225 }
226
227 pub fn has_state_machine(&self) -> bool {
229 self.state_machine.is_some()
230 }
231
232 pub fn state_machine(&self) -> Option<&StateMachineConfig> {
234 self.state_machine.as_ref()
235 }
236
237 pub fn apply_state_override(&self, current_state: &str) -> ResponseStub {
245 let mut stub = self.clone();
246
247 if let Some(ref state_machine) = self.state_machine {
248 if let Some(ref state_responses) = state_machine.state_responses {
249 if let Some(ref override_config) = state_responses.get(current_state) {
250 if let Some(status) = override_config.status {
252 stub.status = status;
253 }
254
255 if let Some(ref body) = override_config.body {
257 stub.body = body.clone();
258 }
259
260 if let Some(ref headers) = override_config.headers {
262 for (key, value) in headers {
263 stub.headers.insert(key.clone(), value.clone());
264 }
265 }
266 }
267 }
268 }
269
270 stub
271 }
272
273 pub fn with_fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
275 self.fault_injection = Some(config);
276 self
277 }
278
279 pub fn has_fault_injection(&self) -> bool {
281 self.fault_injection.as_ref().map(|f| f.enabled).unwrap_or(false)
282 }
283
284 pub fn fault_injection(&self) -> Option<&StubFaultInjectionConfig> {
286 self.fault_injection.as_ref()
287 }
288}
289
290impl ResourceIdExtractConfig {
291 pub fn to_core(&self) -> CoreResourceIdExtract {
293 match self {
294 ResourceIdExtractConfig::PathParam { param } => CoreResourceIdExtract::PathParam {
295 param: param.clone(),
296 },
297 ResourceIdExtractConfig::JsonPath { path } => {
298 CoreResourceIdExtract::JsonPath { path: path.clone() }
299 }
300 ResourceIdExtractConfig::Header { name } => {
301 CoreResourceIdExtract::Header { name: name.clone() }
302 }
303 ResourceIdExtractConfig::QueryParam { param } => CoreResourceIdExtract::QueryParam {
304 param: param.clone(),
305 },
306 }
307 }
308}
309
310pub struct DynamicStub {
312 pub method: String,
314 pub path: String,
316 pub status: Arc<RwLock<u16>>,
318 pub headers: Arc<RwLock<HashMap<String, String>>>,
320 pub response_fn: DynamicResponseFn,
322 pub latency_ms: Option<u64>,
324}
325
326impl DynamicStub {
327 pub fn new<F>(method: impl Into<String>, path: impl Into<String>, response_fn: F) -> Self
329 where
330 F: Fn(&RequestContext) -> Value + Send + Sync + 'static,
331 {
332 Self {
333 method: method.into(),
334 path: path.into(),
335 status: Arc::new(RwLock::new(200)),
336 headers: Arc::new(RwLock::new(HashMap::new())),
337 response_fn: Arc::new(response_fn),
338 latency_ms: None,
339 }
340 }
341
342 pub async fn set_status(&self, status: u16) {
344 *self.status.write().await = status;
345 }
346
347 pub async fn get_status(&self) -> u16 {
349 *self.status.read().await
350 }
351
352 pub async fn add_header(&self, key: String, value: String) {
354 self.headers.write().await.insert(key, value);
355 }
356
357 pub async fn remove_header(&self, key: &str) {
359 self.headers.write().await.remove(key);
360 }
361
362 pub async fn get_headers(&self) -> HashMap<String, String> {
366 self.headers.read().await.clone()
367 }
368
369 pub async fn with_headers<F, R>(&self, f: F) -> R
390 where
391 F: FnOnce(&HashMap<String, String>) -> R,
392 {
393 let headers = self.headers.read().await;
394 f(&headers)
395 }
396
397 pub fn generate_response(&self, ctx: &RequestContext) -> Value {
399 (self.response_fn)(ctx)
400 }
401
402 pub fn with_latency(mut self, ms: u64) -> Self {
404 self.latency_ms = Some(ms);
405 self
406 }
407}
408
409pub struct StubBuilder {
411 method: String,
412 path: String,
413 status: u16,
414 headers: HashMap<String, String>,
415 body: Value,
416 latency_ms: Option<u64>,
417 state_machine: Option<StateMachineConfig>,
418 fault_injection: Option<StubFaultInjectionConfig>,
419}
420
421impl StubBuilder {
422 pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
424 Self {
425 method: method.into(),
426 path: path.into(),
427 status: 200,
428 headers: HashMap::new(),
429 body: Value::Null,
430 latency_ms: None,
431 state_machine: None,
432 fault_injection: None,
433 }
434 }
435
436 pub fn status(mut self, status: u16) -> Self {
438 self.status = status;
439 self
440 }
441
442 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
444 self.headers.insert(key.into(), value.into());
445 self
446 }
447
448 pub fn body(mut self, body: Value) -> Self {
450 self.body = body;
451 self
452 }
453
454 pub fn latency(mut self, ms: u64) -> Self {
456 self.latency_ms = Some(ms);
457 self
458 }
459
460 pub fn state_machine(mut self, config: StateMachineConfig) -> Self {
462 self.state_machine = Some(config);
463 self
464 }
465
466 pub fn fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
468 self.fault_injection = Some(config);
469 self
470 }
471
472 pub fn build(self) -> ResponseStub {
474 ResponseStub {
475 method: self.method,
476 path: self.path,
477 status: self.status,
478 headers: self.headers,
479 body: self.body,
480 latency_ms: self.latency_ms,
481 state_machine: self.state_machine,
482 fault_injection: self.fault_injection,
483 }
484 }
485}