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, Default)]
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 StubFaultInjectionConfig {
116 #[must_use]
118 pub fn http_error(codes: Vec<u16>) -> Self {
119 Self {
120 enabled: true,
121 http_errors: Some(codes),
122 http_error_probability: Some(1.0),
123 ..Default::default()
124 }
125 }
126
127 #[must_use]
129 pub fn timeout(ms: u64) -> Self {
130 Self {
131 enabled: true,
132 timeout_error: true,
133 timeout_ms: Some(ms),
134 timeout_probability: Some(1.0),
135 ..Default::default()
136 }
137 }
138
139 #[must_use]
141 pub fn connection_error() -> Self {
142 Self {
143 enabled: true,
144 connection_error: true,
145 connection_error_probability: Some(1.0),
146 ..Default::default()
147 }
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct ResponseStub {
154 pub method: String,
156 pub path: String,
158 pub status: u16,
160 pub headers: HashMap<String, String>,
162 pub body: Value,
164 pub latency_ms: Option<u64>,
166 #[serde(skip_serializing_if = "Option::is_none")]
169 pub state_machine: Option<StateMachineConfig>,
170 #[serde(skip_serializing_if = "Option::is_none")]
173 pub fault_injection: Option<StubFaultInjectionConfig>,
174}
175
176impl ResponseStub {
177 pub fn new(method: impl Into<String>, path: impl Into<String>, body: Value) -> Self {
179 Self {
180 method: method.into(),
181 path: path.into(),
182 status: 200,
183 headers: HashMap::new(),
184 body,
185 latency_ms: None,
186 state_machine: None,
187 fault_injection: None,
188 }
189 }
190
191 #[must_use]
193 pub const fn status(mut self, status: u16) -> Self {
194 self.status = status;
195 self
196 }
197
198 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
200 self.headers.insert(key.into(), value.into());
201 self
202 }
203
204 #[must_use]
206 pub const fn latency(mut self, ms: u64) -> Self {
207 self.latency_ms = Some(ms);
208 self
209 }
210
211 #[must_use]
213 pub fn with_state_machine(mut self, config: StateMachineConfig) -> Self {
214 self.state_machine = Some(config);
215 self
216 }
217
218 #[must_use]
220 pub const fn has_state_machine(&self) -> bool {
221 self.state_machine.is_some()
222 }
223
224 #[must_use]
226 pub const fn state_machine(&self) -> Option<&StateMachineConfig> {
227 self.state_machine.as_ref()
228 }
229
230 #[must_use]
238 pub fn apply_state_override(&self, current_state: &str) -> Self {
239 let mut stub = self.clone();
240
241 if let Some(ref state_machine) = self.state_machine {
242 if let Some(ref state_responses) = state_machine.state_responses {
243 if let Some(override_config) = state_responses.get(current_state) {
244 if let Some(status) = override_config.status {
246 stub.status = status;
247 }
248
249 if let Some(ref body) = override_config.body {
251 stub.body = body.clone();
252 }
253
254 if let Some(ref headers) = override_config.headers {
256 for (key, value) in headers {
257 stub.headers.insert(key.clone(), value.clone());
258 }
259 }
260 }
261 }
262 }
263
264 stub
265 }
266
267 #[must_use]
269 pub fn with_fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
270 self.fault_injection = Some(config);
271 self
272 }
273
274 #[must_use]
276 pub fn has_fault_injection(&self) -> bool {
277 self.fault_injection.as_ref().is_some_and(|f| f.enabled)
278 }
279
280 #[must_use]
282 pub const fn fault_injection(&self) -> Option<&StubFaultInjectionConfig> {
283 self.fault_injection.as_ref()
284 }
285}
286
287impl ResourceIdExtractConfig {
288 #[must_use]
290 pub fn to_core(&self) -> CoreResourceIdExtract {
291 match self {
292 Self::PathParam { param } => CoreResourceIdExtract::PathParam {
293 param: param.clone(),
294 },
295 Self::JsonPath { path } => CoreResourceIdExtract::JsonPath { path: path.clone() },
296 Self::Header { name } => CoreResourceIdExtract::Header { name: name.clone() },
297 Self::QueryParam { param } => CoreResourceIdExtract::QueryParam {
298 param: param.clone(),
299 },
300 }
301 }
302}
303
304pub struct DynamicStub {
306 pub method: String,
308 pub path: String,
310 pub status: Arc<RwLock<u16>>,
312 pub headers: Arc<RwLock<HashMap<String, String>>>,
314 pub response_fn: DynamicResponseFn,
316 pub latency_ms: Option<u64>,
318}
319
320impl DynamicStub {
321 pub fn new<F>(method: impl Into<String>, path: impl Into<String>, response_fn: F) -> Self
323 where
324 F: Fn(&RequestContext) -> Value + Send + Sync + 'static,
325 {
326 Self {
327 method: method.into(),
328 path: path.into(),
329 status: Arc::new(RwLock::new(200)),
330 headers: Arc::new(RwLock::new(HashMap::new())),
331 response_fn: Arc::new(response_fn),
332 latency_ms: None,
333 }
334 }
335
336 pub async fn set_status(&self, status: u16) {
338 *self.status.write().await = status;
339 }
340
341 pub async fn get_status(&self) -> u16 {
343 *self.status.read().await
344 }
345
346 pub async fn add_header(&self, key: String, value: String) {
348 self.headers.write().await.insert(key, value);
349 }
350
351 pub async fn remove_header(&self, key: &str) {
353 self.headers.write().await.remove(key);
354 }
355
356 pub async fn get_headers(&self) -> HashMap<String, String> {
360 self.headers.read().await.clone()
361 }
362
363 pub async fn with_headers<F, R>(&self, f: F) -> R
384 where
385 F: FnOnce(&HashMap<String, String>) -> R,
386 {
387 let headers = self.headers.read().await;
388 f(&headers)
389 }
390
391 #[must_use]
393 pub fn generate_response(&self, ctx: &RequestContext) -> Value {
394 (self.response_fn)(ctx)
395 }
396
397 #[must_use]
399 pub const fn with_latency(mut self, ms: u64) -> Self {
400 self.latency_ms = Some(ms);
401 self
402 }
403}
404
405pub struct StubBuilder {
407 method: String,
408 path: String,
409 status: u16,
410 headers: HashMap<String, String>,
411 body: Value,
412 latency_ms: Option<u64>,
413 state_machine: Option<StateMachineConfig>,
414 fault_injection: Option<StubFaultInjectionConfig>,
415}
416
417impl StubBuilder {
418 pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
420 Self {
421 method: method.into(),
422 path: path.into(),
423 status: 200,
424 headers: HashMap::new(),
425 body: Value::Null,
426 latency_ms: None,
427 state_machine: None,
428 fault_injection: None,
429 }
430 }
431
432 #[must_use]
434 pub const fn status(mut self, status: u16) -> Self {
435 self.status = status;
436 self
437 }
438
439 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
441 self.headers.insert(key.into(), value.into());
442 self
443 }
444
445 #[must_use]
447 pub fn body(mut self, body: Value) -> Self {
448 self.body = body;
449 self
450 }
451
452 #[must_use]
454 pub const fn latency(mut self, ms: u64) -> Self {
455 self.latency_ms = Some(ms);
456 self
457 }
458
459 #[must_use]
461 pub fn state_machine(mut self, config: StateMachineConfig) -> Self {
462 self.state_machine = Some(config);
463 self
464 }
465
466 #[must_use]
468 pub fn fault_injection(mut self, config: StubFaultInjectionConfig) -> Self {
469 self.fault_injection = Some(config);
470 self
471 }
472
473 #[must_use]
475 pub fn build(self) -> ResponseStub {
476 ResponseStub {
477 method: self.method,
478 path: self.path,
479 status: self.status,
480 headers: self.headers,
481 body: self.body,
482 latency_ms: self.latency_ms,
483 state_machine: self.state_machine,
484 fault_injection: self.fault_injection,
485 }
486 }
487}