Skip to main content

mockforge_bench/
request_gen.rs

1//! Request template generation from OpenAPI operations
2
3use crate::error::Result;
4use crate::param_overrides::OperationOverrides;
5use crate::spec_parser::ApiOperation;
6use openapiv3::{
7    MediaType, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, RequestBody,
8    Schema, SchemaKind, Type,
9};
10use serde_json::{json, Value};
11use std::collections::HashMap;
12
13/// A request template for load testing
14#[derive(Debug, Clone)]
15pub struct RequestTemplate {
16    pub operation: ApiOperation,
17    pub path_params: HashMap<String, String>,
18    pub query_params: HashMap<String, String>,
19    pub headers: HashMap<String, String>,
20    pub body: Option<Value>,
21}
22
23impl RequestTemplate {
24    /// Generate the full URL path with parameters substituted
25    pub fn generate_path(&self) -> String {
26        let mut path = self.operation.path.clone();
27
28        for (key, value) in &self.path_params {
29            path = path.replace(&format!("{{{}}}", key), value);
30        }
31
32        if !self.query_params.is_empty() {
33            let query_string: Vec<String> =
34                self.query_params.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
35            path = format!("{}?{}", path, query_string.join("&"));
36        }
37
38        path
39    }
40
41    /// Get all headers including content-type
42    pub fn get_headers(&self) -> HashMap<String, String> {
43        let mut headers = self.headers.clone();
44
45        if self.body.is_some() {
46            headers
47                .entry("Content-Type".to_string())
48                .or_insert_with(|| "application/json".to_string());
49        }
50
51        headers
52    }
53}
54
55/// Request template generator
56pub struct RequestGenerator;
57
58impl RequestGenerator {
59    /// Generate a request template from an API operation
60    pub fn generate_template(operation: &ApiOperation) -> Result<RequestTemplate> {
61        Self::generate_template_with_overrides(operation, None)
62    }
63
64    /// Generate a request template with optional parameter overrides
65    ///
66    /// When overrides are provided, they take precedence over auto-generated values.
67    /// This allows users to provide realistic test data instead of placeholder values.
68    pub fn generate_template_with_overrides(
69        operation: &ApiOperation,
70        overrides: Option<&OperationOverrides>,
71    ) -> Result<RequestTemplate> {
72        let mut template = RequestTemplate {
73            operation: operation.clone(),
74            path_params: HashMap::new(),
75            query_params: HashMap::new(),
76            headers: HashMap::new(),
77            body: None,
78        };
79
80        // Extract parameters from OpenAPI spec
81        for param_ref in &operation.operation.parameters {
82            if let ReferenceOr::Item(param) = param_ref {
83                Self::process_parameter_with_overrides(param, &mut template, overrides)?;
84            }
85        }
86
87        // Apply any additional overridden parameters not in the spec
88        if let Some(ovr) = overrides {
89            // Add overridden path params that weren't in the spec
90            for (name, value) in &ovr.path_params {
91                template.path_params.entry(name.clone()).or_insert_with(|| value.clone());
92            }
93            // Add overridden query params that weren't in the spec
94            for (name, value) in &ovr.query_params {
95                template.query_params.entry(name.clone()).or_insert_with(|| value.clone());
96            }
97            // Add overridden headers that weren't in the spec
98            for (name, value) in &ovr.headers {
99                template.headers.entry(name.clone()).or_insert_with(|| value.clone());
100            }
101        }
102
103        // Extract request body (override takes precedence)
104        if let Some(ovr) = overrides {
105            if let Some(body) = ovr.get_body() {
106                template.body = Some(body.clone());
107            } else if let Some(ReferenceOr::Item(request_body)) = &operation.operation.request_body
108            {
109                template.body = Self::generate_body(request_body)?;
110            }
111        } else if let Some(ReferenceOr::Item(request_body)) = &operation.operation.request_body {
112            template.body = Self::generate_body(request_body)?;
113        }
114
115        Ok(template)
116    }
117
118    /// Process a parameter and add it to the template
119    fn process_parameter(param: &Parameter, template: &mut RequestTemplate) -> Result<()> {
120        Self::process_parameter_with_overrides(param, template, None)
121    }
122
123    /// Process a parameter with optional overrides
124    fn process_parameter_with_overrides(
125        param: &Parameter,
126        template: &mut RequestTemplate,
127        overrides: Option<&OperationOverrides>,
128    ) -> Result<()> {
129        let (param_type, param_data) = match param {
130            Parameter::Query { parameter_data, .. } => ("query", parameter_data),
131            Parameter::Path { parameter_data, .. } => ("path", parameter_data),
132            Parameter::Header { parameter_data, .. } => ("header", parameter_data),
133            Parameter::Cookie { parameter_data, .. } => ("cookie", parameter_data),
134        };
135
136        // Check for override first, then fall back to generated value
137        let value = if let Some(ovr) = overrides {
138            match param_type {
139                "path" => ovr.get_path_param(&param_data.name).cloned(),
140                "query" => ovr.get_query_param(&param_data.name).cloned(),
141                "header" => ovr.get_header(&param_data.name).cloned(),
142                _ => None,
143            }
144        } else {
145            None
146        }
147        .unwrap_or_else(|| Self::generate_param_value(param_data).unwrap_or_default());
148
149        match param_type {
150            "query" => {
151                template.query_params.insert(param_data.name.clone(), value);
152            }
153            "path" => {
154                template.path_params.insert(param_data.name.clone(), value);
155            }
156            "header" => {
157                template.headers.insert(param_data.name.clone(), value);
158            }
159            "cookie" => {
160                // Append cookie to existing Cookie header or create new one
161                let cookie_pair = format!("{}={}", param_data.name, value);
162                template
163                    .headers
164                    .entry("Cookie".to_string())
165                    .and_modify(|existing| {
166                        existing.push_str("; ");
167                        existing.push_str(&cookie_pair);
168                    })
169                    .or_insert(cookie_pair);
170            }
171            _ => {}
172        }
173
174        Ok(())
175    }
176
177    /// Generate a value for a parameter
178    fn generate_param_value(param_data: &ParameterData) -> Result<String> {
179        // Try to use example first
180        if let Some(example) = &param_data.example {
181            return Ok(example.to_string().trim_matches('"').to_string());
182        }
183
184        // Generate from schema
185        if let ParameterSchemaOrContent::Schema(ReferenceOr::Item(schema)) = &param_data.format {
186            return Ok(Self::generate_value_from_schema(schema));
187        }
188
189        // Default value based on parameter name
190        Ok(Self::default_param_value(&param_data.name))
191    }
192
193    /// Generate a default value based on parameter name
194    fn default_param_value(name: &str) -> String {
195        match name.to_lowercase().as_str() {
196            "id" => "1".to_string(),
197            "limit" => "10".to_string(),
198            "offset" => "0".to_string(),
199            "page" => "1".to_string(),
200            "sort" => "name".to_string(),
201            _ => "test-value".to_string(),
202        }
203    }
204
205    /// Generate a request body from a RequestBody definition
206    fn generate_body(request_body: &RequestBody) -> Result<Option<Value>> {
207        // Look for application/json content
208        if let Some(content) = request_body.content.get("application/json") {
209            return Ok(Some(Self::generate_json_body(content)));
210        }
211
212        Ok(None)
213    }
214
215    /// Generate JSON body from media type
216    fn generate_json_body(media_type: &MediaType) -> Value {
217        // Try to use example first
218        if let Some(example) = &media_type.example {
219            return example.clone();
220        }
221
222        // Generate from schema
223        if let Some(ReferenceOr::Item(schema)) = &media_type.schema {
224            return Self::generate_json_from_schema(schema);
225        }
226
227        json!({})
228    }
229
230    /// Generate JSON from schema
231    fn generate_json_from_schema(schema: &Schema) -> Value {
232        match &schema.schema_kind {
233            SchemaKind::Type(Type::Object(obj)) => {
234                let mut map = serde_json::Map::new();
235
236                for (key, schema_ref) in &obj.properties {
237                    if let ReferenceOr::Item(prop_schema) = schema_ref {
238                        map.insert(key.clone(), Self::generate_json_from_schema(prop_schema));
239                    }
240                }
241
242                Value::Object(map)
243            }
244            SchemaKind::Type(Type::Array(arr)) => {
245                if let Some(ReferenceOr::Item(item_schema)) = &arr.items {
246                    return json!([Self::generate_json_from_schema(item_schema)]);
247                }
248                json!([])
249            }
250            SchemaKind::Type(Type::String(_)) => Self::generate_string_value(schema),
251            SchemaKind::Type(Type::Number(_)) => json!(42.0),
252            SchemaKind::Type(Type::Integer(_)) => json!(42),
253            SchemaKind::Type(Type::Boolean(_)) => json!(true),
254            _ => json!(null),
255        }
256    }
257
258    /// Generate a string value from schema
259    fn generate_string_value(schema: &Schema) -> Value {
260        // Use example if available
261        if let Some(example) = &schema.schema_data.example {
262            return example.clone();
263        }
264
265        json!("test-string")
266    }
267
268    /// Generate a value from schema (for parameters)
269    fn generate_value_from_schema(schema: &Schema) -> String {
270        match &schema.schema_kind {
271            SchemaKind::Type(Type::String(_)) => "test-value".to_string(),
272            SchemaKind::Type(Type::Number(_)) => "42.0".to_string(),
273            SchemaKind::Type(Type::Integer(_)) => "42".to_string(),
274            SchemaKind::Type(Type::Boolean(_)) => "true".to_string(),
275            _ => "test-value".to_string(),
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use openapiv3::Operation;
284
285    #[test]
286    fn test_generate_path() {
287        let op = ApiOperation {
288            method: "get".to_string(),
289            path: "/users/{id}".to_string(),
290            operation: Operation::default(),
291            operation_id: None,
292        };
293
294        let mut template = RequestTemplate {
295            operation: op,
296            path_params: HashMap::new(),
297            query_params: HashMap::new(),
298            headers: HashMap::new(),
299            body: None,
300        };
301
302        template.path_params.insert("id".to_string(), "123".to_string());
303        template.query_params.insert("limit".to_string(), "10".to_string());
304
305        let path = template.generate_path();
306        assert_eq!(path, "/users/123?limit=10");
307    }
308
309    #[test]
310    fn test_default_param_value() {
311        assert_eq!(RequestGenerator::default_param_value("id"), "1");
312        assert_eq!(RequestGenerator::default_param_value("limit"), "10");
313        assert_eq!(RequestGenerator::default_param_value("unknown"), "test-value");
314    }
315}