mockforge_sdk/
stub.rs

1//! Response stub configuration
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8
9/// Type alias for a dynamic response function
10pub type DynamicResponseFn = Arc<dyn Fn(&RequestContext) -> Value + Send + Sync>;
11
12/// Request context passed to dynamic response functions
13#[derive(Debug, Clone)]
14pub struct RequestContext {
15    /// HTTP method
16    pub method: String,
17    /// Request path
18    pub path: String,
19    /// Path parameters extracted from the URL
20    pub path_params: HashMap<String, String>,
21    /// Query parameters
22    pub query_params: HashMap<String, String>,
23    /// Request headers
24    pub headers: HashMap<String, String>,
25    /// Request body
26    pub body: Option<Value>,
27}
28
29/// A response stub for mocking API endpoints
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct ResponseStub {
32    /// HTTP method (GET, POST, PUT, DELETE, etc.)
33    pub method: String,
34    /// Path pattern (supports {{path_params}})
35    pub path: String,
36    /// HTTP status code
37    pub status: u16,
38    /// Response headers
39    pub headers: HashMap<String, String>,
40    /// Response body (supports templates like {{uuid}}, {{faker.name}})
41    pub body: Value,
42    /// Optional latency in milliseconds
43    pub latency_ms: Option<u64>,
44}
45
46impl ResponseStub {
47    /// Create a new response stub
48    pub fn new(method: impl Into<String>, path: impl Into<String>, body: Value) -> Self {
49        Self {
50            method: method.into(),
51            path: path.into(),
52            status: 200,
53            headers: HashMap::new(),
54            body,
55            latency_ms: None,
56        }
57    }
58
59    /// Set the HTTP status code
60    pub fn status(mut self, status: u16) -> Self {
61        self.status = status;
62        self
63    }
64
65    /// Add a response header
66    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
67        self.headers.insert(key.into(), value.into());
68        self
69    }
70
71    /// Set response latency in milliseconds
72    pub fn latency(mut self, ms: u64) -> Self {
73        self.latency_ms = Some(ms);
74        self
75    }
76}
77
78/// Dynamic stub with runtime response generation
79pub struct DynamicStub {
80    /// HTTP method
81    pub method: String,
82    /// Path pattern
83    pub path: String,
84    /// HTTP status code (can be dynamic)
85    pub status: Arc<RwLock<u16>>,
86    /// Response headers (can be dynamic)
87    pub headers: Arc<RwLock<HashMap<String, String>>>,
88    /// Dynamic response function
89    pub response_fn: DynamicResponseFn,
90    /// Optional latency in milliseconds
91    pub latency_ms: Option<u64>,
92}
93
94impl DynamicStub {
95    /// Create a new dynamic stub
96    pub fn new<F>(method: impl Into<String>, path: impl Into<String>, response_fn: F) -> Self
97    where
98        F: Fn(&RequestContext) -> Value + Send + Sync + 'static,
99    {
100        Self {
101            method: method.into(),
102            path: path.into(),
103            status: Arc::new(RwLock::new(200)),
104            headers: Arc::new(RwLock::new(HashMap::new())),
105            response_fn: Arc::new(response_fn),
106            latency_ms: None,
107        }
108    }
109
110    /// Set the HTTP status code
111    pub async fn set_status(&self, status: u16) {
112        *self.status.write().await = status;
113    }
114
115    /// Get the current status code
116    pub async fn get_status(&self) -> u16 {
117        *self.status.read().await
118    }
119
120    /// Add a response header
121    pub async fn add_header(&self, key: String, value: String) {
122        self.headers.write().await.insert(key, value);
123    }
124
125    /// Remove a response header
126    pub async fn remove_header(&self, key: &str) {
127        self.headers.write().await.remove(key);
128    }
129
130    /// Get all headers (returns a clone)
131    ///
132    /// For more efficient read-only access, consider using `with_headers()` instead.
133    pub async fn get_headers(&self) -> HashMap<String, String> {
134        self.headers.read().await.clone()
135    }
136
137    /// Access headers without cloning via a callback
138    ///
139    /// This is more efficient than `get_headers()` when you only need to
140    /// read header values without modifying them.
141    ///
142    /// # Examples
143    ///
144    /// ```rust
145    /// # use mockforge_sdk::DynamicStub;
146    /// # use serde_json::json;
147    /// # async fn example() {
148    /// let stub = DynamicStub::new("GET", "/test", |_| json!({}));
149    /// stub.add_header("X-Custom".to_string(), "value".to_string()).await;
150    ///
151    /// // Efficient read-only access
152    /// let has_custom = stub.with_headers(|headers| {
153    ///     headers.contains_key("X-Custom")
154    /// }).await;
155    /// # }
156    /// ```
157    pub async fn with_headers<F, R>(&self, f: F) -> R
158    where
159        F: FnOnce(&HashMap<String, String>) -> R,
160    {
161        let headers = self.headers.read().await;
162        f(&headers)
163    }
164
165    /// Generate a response for a given request context
166    pub fn generate_response(&self, ctx: &RequestContext) -> Value {
167        (self.response_fn)(ctx)
168    }
169
170    /// Set latency
171    pub fn with_latency(mut self, ms: u64) -> Self {
172        self.latency_ms = Some(ms);
173        self
174    }
175}
176
177/// Builder for creating response stubs
178pub struct StubBuilder {
179    method: String,
180    path: String,
181    status: u16,
182    headers: HashMap<String, String>,
183    body: Value,
184    latency_ms: Option<u64>,
185}
186
187impl StubBuilder {
188    /// Create a new stub builder
189    pub fn new(method: impl Into<String>, path: impl Into<String>) -> Self {
190        Self {
191            method: method.into(),
192            path: path.into(),
193            status: 200,
194            headers: HashMap::new(),
195            body: Value::Null,
196            latency_ms: None,
197        }
198    }
199
200    /// Set the HTTP status code
201    pub fn status(mut self, status: u16) -> Self {
202        self.status = status;
203        self
204    }
205
206    /// Add a response header
207    pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
208        self.headers.insert(key.into(), value.into());
209        self
210    }
211
212    /// Set the response body
213    pub fn body(mut self, body: Value) -> Self {
214        self.body = body;
215        self
216    }
217
218    /// Set response latency in milliseconds
219    pub fn latency(mut self, ms: u64) -> Self {
220        self.latency_ms = Some(ms);
221        self
222    }
223
224    /// Build the response stub
225    pub fn build(self) -> ResponseStub {
226        ResponseStub {
227            method: self.method,
228            path: self.path,
229            status: self.status,
230            headers: self.headers,
231            body: self.body,
232            latency_ms: self.latency_ms,
233        }
234    }
235}