Skip to main content

mockforge_bench/
param_overrides.rs

1//! Parameter overrides for customizing request values in load tests
2//!
3//! This module allows users to provide custom parameter values instead of
4//! the auto-generated placeholder values like "test-value".
5//!
6//! # Example Configuration File (JSON)
7//!
8//! ```json
9//! {
10//!   "defaults": {
11//!     "path_params": {
12//!       "id": "12345",
13//!       "uuid": "550e8400-e29b-41d4-a716-446655440000"
14//!     },
15//!     "query_params": {
16//!       "limit": "100",
17//!       "page": "1"
18//!     }
19//!   },
20//!   "operations": {
21//!     "createUser": {
22//!       "body": {
23//!         "name": "Test User",
24//!         "email": "test@example.com"
25//!       }
26//!     },
27//!     "getUser": {
28//!       "path_params": {
29//!         "id": "user-123"
30//!       }
31//!     }
32//!   }
33//! }
34//! ```
35
36use crate::error::{BenchError, Result};
37use serde::{Deserialize, Serialize};
38use serde_json::Value;
39use std::collections::HashMap;
40use std::path::Path;
41
42/// Parameter overrides configuration
43#[derive(Debug, Clone, Default, Serialize, Deserialize)]
44pub struct ParameterOverrides {
45    /// Default values applied to all operations
46    #[serde(default)]
47    pub defaults: OperationOverrides,
48
49    /// Per-operation overrides (keyed by operation ID or "METHOD /path")
50    #[serde(default)]
51    pub operations: HashMap<String, OperationOverrides>,
52}
53
54/// Overrides for a specific operation or defaults
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct OperationOverrides {
57    /// Path parameter overrides (e.g., {"id": "123"})
58    #[serde(default)]
59    pub path_params: HashMap<String, String>,
60
61    /// Query parameter overrides (e.g., {"limit": "50"})
62    #[serde(default)]
63    pub query_params: HashMap<String, String>,
64
65    /// Header overrides (e.g., {"X-Custom": "value"})
66    #[serde(default)]
67    pub headers: HashMap<String, String>,
68
69    /// Request body override (JSON value)
70    #[serde(default)]
71    pub body: Option<Value>,
72}
73
74impl ParameterOverrides {
75    /// Load parameter overrides from a JSON or YAML file
76    pub fn from_file(path: &Path) -> Result<Self> {
77        let content = std::fs::read_to_string(path).map_err(|e| {
78            BenchError::Other(format!("Failed to read params file '{}': {}", path.display(), e))
79        })?;
80
81        let extension = path.extension().and_then(|e| e.to_str()).unwrap_or("");
82
83        match extension.to_lowercase().as_str() {
84            "json" => serde_json::from_str(&content)
85                .map_err(|e| BenchError::Other(format!("Failed to parse JSON params file: {}", e))),
86            "yaml" | "yml" => serde_yaml::from_str(&content)
87                .map_err(|e| BenchError::Other(format!("Failed to parse YAML params file: {}", e))),
88            _ => {
89                // Try JSON first, then YAML
90                serde_json::from_str(&content)
91                    .or_else(|_| serde_yaml::from_str(&content))
92                    .map_err(|e| {
93                        BenchError::Other(format!(
94                            "Failed to parse params file (tried JSON and YAML): {}",
95                            e
96                        ))
97                    })
98            }
99        }
100    }
101
102    /// Get the effective overrides for an operation
103    ///
104    /// Merges default overrides with operation-specific overrides.
105    /// Operation-specific values take precedence over defaults.
106    pub fn get_for_operation(
107        &self,
108        operation_id: Option<&str>,
109        method: &str,
110        path: &str,
111    ) -> OperationOverrides {
112        let mut result = self.defaults.clone();
113
114        // Try to find operation-specific overrides
115        let op_overrides = operation_id
116            .and_then(|id| self.operations.get(id))
117            .or_else(|| {
118                // Try "METHOD /path" format
119                let key = format!("{} {}", method.to_uppercase(), path);
120                self.operations.get(&key)
121            })
122            .or_else(|| {
123                // Try just the path
124                self.operations.get(path)
125            });
126
127        if let Some(overrides) = op_overrides {
128            // Merge operation overrides into result (operation takes precedence)
129            for (k, v) in &overrides.path_params {
130                result.path_params.insert(k.clone(), v.clone());
131            }
132            for (k, v) in &overrides.query_params {
133                result.query_params.insert(k.clone(), v.clone());
134            }
135            for (k, v) in &overrides.headers {
136                result.headers.insert(k.clone(), v.clone());
137            }
138            if overrides.body.is_some() {
139                result.body = overrides.body.clone();
140            }
141        }
142
143        result
144    }
145
146    /// Check if this configuration is empty (no overrides defined)
147    pub fn is_empty(&self) -> bool {
148        self.defaults.is_empty() && self.operations.is_empty()
149    }
150}
151
152impl OperationOverrides {
153    /// Check if this override set is empty
154    pub fn is_empty(&self) -> bool {
155        self.path_params.is_empty()
156            && self.query_params.is_empty()
157            && self.headers.is_empty()
158            && self.body.is_none()
159    }
160
161    /// Get a path parameter value if overridden
162    pub fn get_path_param(&self, name: &str) -> Option<&String> {
163        self.path_params.get(name)
164    }
165
166    /// Get a query parameter value if overridden
167    pub fn get_query_param(&self, name: &str) -> Option<&String> {
168        self.query_params.get(name)
169    }
170
171    /// Get a header value if overridden
172    pub fn get_header(&self, name: &str) -> Option<&String> {
173        self.headers.get(name)
174    }
175
176    /// Get the body override if present
177    pub fn get_body(&self) -> Option<&Value> {
178        self.body.as_ref()
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use serde_json::json;
186
187    #[test]
188    fn test_parse_json_overrides() {
189        let json = r#"{
190            "defaults": {
191                "path_params": {
192                    "id": "default-id"
193                },
194                "query_params": {
195                    "limit": "50"
196                }
197            },
198            "operations": {
199                "getUser": {
200                    "path_params": {
201                        "id": "user-123"
202                    }
203                },
204                "POST /users": {
205                    "body": {
206                        "name": "Test User"
207                    }
208                }
209            }
210        }"#;
211
212        let overrides: ParameterOverrides = serde_json::from_str(json).unwrap();
213
214        // Check defaults
215        assert_eq!(overrides.defaults.path_params.get("id"), Some(&"default-id".to_string()));
216        assert_eq!(overrides.defaults.query_params.get("limit"), Some(&"50".to_string()));
217
218        // Check operation-specific
219        let get_user = overrides.operations.get("getUser").unwrap();
220        assert_eq!(get_user.path_params.get("id"), Some(&"user-123".to_string()));
221
222        let post_users = overrides.operations.get("POST /users").unwrap();
223        assert!(post_users.body.is_some());
224    }
225
226    #[test]
227    fn test_get_for_operation_with_defaults() {
228        let overrides = ParameterOverrides {
229            defaults: OperationOverrides {
230                path_params: [("id".to_string(), "default-id".to_string())].into_iter().collect(),
231                query_params: [("limit".to_string(), "10".to_string())].into_iter().collect(),
232                ..Default::default()
233            },
234            operations: HashMap::new(),
235        };
236
237        let result = overrides.get_for_operation(Some("unknownOp"), "GET", "/users");
238        assert_eq!(result.path_params.get("id"), Some(&"default-id".to_string()));
239        assert_eq!(result.query_params.get("limit"), Some(&"10".to_string()));
240    }
241
242    #[test]
243    fn test_get_for_operation_with_override() {
244        let mut operations = HashMap::new();
245        operations.insert(
246            "getUser".to_string(),
247            OperationOverrides {
248                path_params: [("id".to_string(), "user-456".to_string())].into_iter().collect(),
249                ..Default::default()
250            },
251        );
252
253        let overrides = ParameterOverrides {
254            defaults: OperationOverrides {
255                path_params: [("id".to_string(), "default-id".to_string())].into_iter().collect(),
256                ..Default::default()
257            },
258            operations,
259        };
260
261        // Operation-specific should override default
262        let result = overrides.get_for_operation(Some("getUser"), "GET", "/users/{id}");
263        assert_eq!(result.path_params.get("id"), Some(&"user-456".to_string()));
264    }
265
266    #[test]
267    fn test_get_for_operation_by_method_path() {
268        let mut operations = HashMap::new();
269        operations.insert(
270            "POST /virtualservice".to_string(),
271            OperationOverrides {
272                body: Some(json!({"name": "my-service"})),
273                ..Default::default()
274            },
275        );
276
277        let overrides = ParameterOverrides {
278            defaults: OperationOverrides::default(),
279            operations,
280        };
281
282        let result = overrides.get_for_operation(None, "POST", "/virtualservice");
283        assert!(result.body.is_some());
284        assert_eq!(result.body.unwrap()["name"], "my-service");
285    }
286
287    #[test]
288    fn test_empty_overrides() {
289        let overrides = ParameterOverrides::default();
290        assert!(overrides.is_empty());
291
292        let result = overrides.get_for_operation(Some("anyOp"), "GET", "/any");
293        assert!(result.is_empty());
294    }
295}