mockforge_bench/
request_gen.rs

1//! Request template generation from OpenAPI operations
2
3use crate::error::Result;
4use crate::spec_parser::ApiOperation;
5use openapiv3::{
6    MediaType, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, RequestBody,
7    Schema, SchemaKind, Type,
8};
9use serde_json::{json, Value};
10use std::collections::HashMap;
11
12/// A request template for load testing
13#[derive(Debug, Clone)]
14pub struct RequestTemplate {
15    pub operation: ApiOperation,
16    pub path_params: HashMap<String, String>,
17    pub query_params: HashMap<String, String>,
18    pub headers: HashMap<String, String>,
19    pub body: Option<Value>,
20}
21
22impl RequestTemplate {
23    /// Generate the full URL path with parameters substituted
24    pub fn generate_path(&self) -> String {
25        let mut path = self.operation.path.clone();
26
27        for (key, value) in &self.path_params {
28            path = path.replace(&format!("{{{}}}", key), value);
29        }
30
31        if !self.query_params.is_empty() {
32            let query_string: Vec<String> =
33                self.query_params.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
34            path = format!("{}?{}", path, query_string.join("&"));
35        }
36
37        path
38    }
39
40    /// Get all headers including content-type
41    pub fn get_headers(&self) -> HashMap<String, String> {
42        let mut headers = self.headers.clone();
43
44        if self.body.is_some() {
45            headers
46                .entry("Content-Type".to_string())
47                .or_insert_with(|| "application/json".to_string());
48        }
49
50        headers
51    }
52}
53
54/// Request template generator
55pub struct RequestGenerator;
56
57impl RequestGenerator {
58    /// Generate a request template from an API operation
59    pub fn generate_template(operation: &ApiOperation) -> Result<RequestTemplate> {
60        let mut template = RequestTemplate {
61            operation: operation.clone(),
62            path_params: HashMap::new(),
63            query_params: HashMap::new(),
64            headers: HashMap::new(),
65            body: None,
66        };
67
68        // Extract parameters
69        for param_ref in &operation.operation.parameters {
70            if let ReferenceOr::Item(param) = param_ref {
71                Self::process_parameter(param, &mut template)?;
72            }
73        }
74
75        // Extract request body
76        if let Some(ReferenceOr::Item(request_body)) = &operation.operation.request_body {
77            template.body = Self::generate_body(request_body)?;
78        }
79
80        Ok(template)
81    }
82
83    /// Process a parameter and add it to the template
84    fn process_parameter(param: &Parameter, template: &mut RequestTemplate) -> Result<()> {
85        let (name, param_data) = match param {
86            Parameter::Query { parameter_data, .. } => ("query", parameter_data),
87            Parameter::Path { parameter_data, .. } => ("path", parameter_data),
88            Parameter::Header { parameter_data, .. } => ("header", parameter_data),
89            Parameter::Cookie { .. } => return Ok(()), // Skip cookies for now
90        };
91
92        let value = Self::generate_param_value(param_data)?;
93
94        match name {
95            "query" => {
96                template.query_params.insert(param_data.name.clone(), value);
97            }
98            "path" => {
99                template.path_params.insert(param_data.name.clone(), value);
100            }
101            "header" => {
102                template.headers.insert(param_data.name.clone(), value);
103            }
104            _ => {}
105        }
106
107        Ok(())
108    }
109
110    /// Generate a value for a parameter
111    fn generate_param_value(param_data: &ParameterData) -> Result<String> {
112        // Try to use example first
113        if let Some(example) = &param_data.example {
114            return Ok(example.to_string().trim_matches('"').to_string());
115        }
116
117        // Generate from schema
118        if let ParameterSchemaOrContent::Schema(ReferenceOr::Item(schema)) = &param_data.format {
119            return Ok(Self::generate_value_from_schema(schema));
120        }
121
122        // Default value based on parameter name
123        Ok(Self::default_param_value(&param_data.name))
124    }
125
126    /// Generate a default value based on parameter name
127    fn default_param_value(name: &str) -> String {
128        match name.to_lowercase().as_str() {
129            "id" => "1".to_string(),
130            "limit" => "10".to_string(),
131            "offset" => "0".to_string(),
132            "page" => "1".to_string(),
133            "sort" => "name".to_string(),
134            _ => "test-value".to_string(),
135        }
136    }
137
138    /// Generate a request body from a RequestBody definition
139    fn generate_body(request_body: &RequestBody) -> Result<Option<Value>> {
140        // Look for application/json content
141        if let Some(content) = request_body.content.get("application/json") {
142            return Ok(Some(Self::generate_json_body(content)));
143        }
144
145        Ok(None)
146    }
147
148    /// Generate JSON body from media type
149    fn generate_json_body(media_type: &MediaType) -> Value {
150        // Try to use example first
151        if let Some(example) = &media_type.example {
152            return example.clone();
153        }
154
155        // Generate from schema
156        if let Some(ReferenceOr::Item(schema)) = &media_type.schema {
157            return Self::generate_json_from_schema(schema);
158        }
159
160        json!({})
161    }
162
163    /// Generate JSON from schema
164    fn generate_json_from_schema(schema: &Schema) -> Value {
165        match &schema.schema_kind {
166            SchemaKind::Type(Type::Object(obj)) => {
167                let mut map = serde_json::Map::new();
168
169                for (key, schema_ref) in &obj.properties {
170                    if let ReferenceOr::Item(prop_schema) = schema_ref {
171                        map.insert(key.clone(), Self::generate_json_from_schema(prop_schema));
172                    }
173                }
174
175                Value::Object(map)
176            }
177            SchemaKind::Type(Type::Array(arr)) => {
178                if let Some(ReferenceOr::Item(item_schema)) = &arr.items {
179                    return json!([Self::generate_json_from_schema(item_schema)]);
180                }
181                json!([])
182            }
183            SchemaKind::Type(Type::String(_)) => Self::generate_string_value(schema),
184            SchemaKind::Type(Type::Number(_)) => json!(42.0),
185            SchemaKind::Type(Type::Integer(_)) => json!(42),
186            SchemaKind::Type(Type::Boolean(_)) => json!(true),
187            _ => json!(null),
188        }
189    }
190
191    /// Generate a string value from schema
192    fn generate_string_value(schema: &Schema) -> Value {
193        // Use example if available
194        if let Some(example) = &schema.schema_data.example {
195            return example.clone();
196        }
197
198        json!("test-string")
199    }
200
201    /// Generate a value from schema (for parameters)
202    fn generate_value_from_schema(schema: &Schema) -> String {
203        match &schema.schema_kind {
204            SchemaKind::Type(Type::String(_)) => "test-value".to_string(),
205            SchemaKind::Type(Type::Number(_)) => "42.0".to_string(),
206            SchemaKind::Type(Type::Integer(_)) => "42".to_string(),
207            SchemaKind::Type(Type::Boolean(_)) => "true".to_string(),
208            _ => "test-value".to_string(),
209        }
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use openapiv3::Operation;
217
218    #[test]
219    fn test_generate_path() {
220        let op = ApiOperation {
221            method: "get".to_string(),
222            path: "/users/{id}".to_string(),
223            operation: Operation::default(),
224            operation_id: None,
225        };
226
227        let mut template = RequestTemplate {
228            operation: op,
229            path_params: HashMap::new(),
230            query_params: HashMap::new(),
231            headers: HashMap::new(),
232            body: None,
233        };
234
235        template.path_params.insert("id".to_string(), "123".to_string());
236        template.query_params.insert("limit".to_string(), "10".to_string());
237
238        let path = template.generate_path();
239        assert_eq!(path, "/users/123?limit=10");
240    }
241
242    #[test]
243    fn test_default_param_value() {
244        assert_eq!(RequestGenerator::default_param_value("id"), "1");
245        assert_eq!(RequestGenerator::default_param_value("limit"), "10");
246        assert_eq!(RequestGenerator::default_param_value("unknown"), "test-value");
247    }
248}