1use 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#[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 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 pub fn has_connection_header(&self) -> bool {
47 header_map_has_connection(&self.headers)
48 }
49
50 pub fn get_headers(&self) -> HashMap<String, String> {
52 let mut headers = self.headers.clone();
53
54 if self.body.is_some() {
55 headers
56 .entry("Content-Type".to_string())
57 .or_insert_with(|| "application/json".to_string());
58 }
59
60 headers
61 }
62}
63
64pub fn header_map_has_connection(headers: &HashMap<String, String>) -> bool {
67 headers.keys().any(|k| k.eq_ignore_ascii_case("connection"))
68}
69
70pub fn should_force_k6_http1(
74 wafbench_verbatim: bool,
75 templates: &[RequestTemplate],
76 extra_headers: &HashMap<String, String>,
77) -> bool {
78 wafbench_verbatim
79 || header_map_has_connection(extra_headers)
80 || templates.iter().any(RequestTemplate::has_connection_header)
81}
82
83pub struct RequestGenerator;
85
86impl RequestGenerator {
87 pub fn generate_template(operation: &ApiOperation) -> Result<RequestTemplate> {
89 Self::generate_template_with_overrides(operation, None)
90 }
91
92 pub fn generate_template_with_overrides(
97 operation: &ApiOperation,
98 overrides: Option<&OperationOverrides>,
99 ) -> Result<RequestTemplate> {
100 let mut template = RequestTemplate {
101 operation: operation.clone(),
102 path_params: HashMap::new(),
103 query_params: HashMap::new(),
104 headers: HashMap::new(),
105 body: None,
106 };
107
108 for param_ref in &operation.operation.parameters {
110 if let ReferenceOr::Item(param) = param_ref {
111 Self::process_parameter_with_overrides(param, &mut template, overrides)?;
112 }
113 }
114
115 if let Some(ovr) = overrides {
117 for (name, value) in &ovr.path_params {
119 template.path_params.entry(name.clone()).or_insert_with(|| value.clone());
120 }
121 for (name, value) in &ovr.query_params {
123 template.query_params.entry(name.clone()).or_insert_with(|| value.clone());
124 }
125 for (name, value) in &ovr.headers {
127 template.headers.entry(name.clone()).or_insert_with(|| value.clone());
128 }
129 }
130
131 if let Some(ovr) = overrides {
133 if let Some(body) = ovr.get_body() {
134 template.body = Some(body.clone());
135 } else if let Some(ReferenceOr::Item(request_body)) = &operation.operation.request_body
136 {
137 template.body = Self::generate_body(request_body)?;
138 }
139 } else if let Some(ReferenceOr::Item(request_body)) = &operation.operation.request_body {
140 template.body = Self::generate_body(request_body)?;
141 }
142
143 Ok(template)
144 }
145
146 fn process_parameter_with_overrides(
148 param: &Parameter,
149 template: &mut RequestTemplate,
150 overrides: Option<&OperationOverrides>,
151 ) -> Result<()> {
152 let (param_type, param_data) = match param {
153 Parameter::Query { parameter_data, .. } => ("query", parameter_data),
154 Parameter::Path { parameter_data, .. } => ("path", parameter_data),
155 Parameter::Header { parameter_data, .. } => ("header", parameter_data),
156 Parameter::Cookie { parameter_data, .. } => ("cookie", parameter_data),
157 };
158
159 let value = if let Some(ovr) = overrides {
161 match param_type {
162 "path" => ovr.get_path_param(¶m_data.name).cloned(),
163 "query" => ovr.get_query_param(¶m_data.name).cloned(),
164 "header" => ovr.get_header(¶m_data.name).cloned(),
165 _ => None,
166 }
167 } else {
168 None
169 }
170 .unwrap_or_else(|| Self::generate_param_value(param_data).unwrap_or_default());
171
172 match param_type {
173 "query" => {
174 template.query_params.insert(param_data.name.clone(), value);
175 }
176 "path" => {
177 template.path_params.insert(param_data.name.clone(), value);
178 }
179 "header" => {
180 let from_override =
187 overrides.and_then(|o| o.get_header(¶m_data.name)).is_some();
188 if Self::is_transport_owned_header(¶m_data.name) && !from_override {
189 } else {
191 template.headers.insert(param_data.name.clone(), value);
192 }
193 }
194 "cookie" => {
195 let cookie_pair = format!("{}={}", param_data.name, value);
197 template
198 .headers
199 .entry("Cookie".to_string())
200 .and_modify(|existing| {
201 existing.push_str("; ");
202 existing.push_str(&cookie_pair);
203 })
204 .or_insert(cookie_pair);
205 }
206 _ => {}
207 }
208
209 Ok(())
210 }
211
212 pub(crate) fn is_transport_owned_header(name: &str) -> bool {
216 matches!(
217 name.to_ascii_lowercase().as_str(),
218 "content-length"
219 | "transfer-encoding"
220 | "host"
221 | "connection"
222 | "keep-alive"
223 | "te"
224 | "trailer"
225 | "upgrade"
226 | "proxy-connection"
227 )
228 }
229
230 fn generate_param_value(param_data: &ParameterData) -> Result<String> {
232 if let Some(example) = ¶m_data.example {
234 return Ok(example.to_string().trim_matches('"').to_string());
235 }
236
237 if let ParameterSchemaOrContent::Schema(ReferenceOr::Item(schema)) = ¶m_data.format {
239 return Ok(Self::generate_value_from_schema(schema));
240 }
241
242 Ok(Self::default_param_value(¶m_data.name))
244 }
245
246 fn default_param_value(name: &str) -> String {
248 match name.to_lowercase().as_str() {
249 "id" => "1".to_string(),
250 "limit" => "10".to_string(),
251 "offset" => "0".to_string(),
252 "page" => "1".to_string(),
253 "sort" => "name".to_string(),
254 _ => "test-value".to_string(),
255 }
256 }
257
258 fn generate_body(request_body: &RequestBody) -> Result<Option<Value>> {
260 if let Some(content) = request_body.content.get("application/json") {
262 return Ok(Some(Self::generate_json_body(content)));
263 }
264
265 Ok(None)
266 }
267
268 fn generate_json_body(media_type: &MediaType) -> Value {
270 if let Some(example) = &media_type.example {
272 return example.clone();
273 }
274
275 if let Some(ReferenceOr::Item(schema)) = &media_type.schema {
277 return Self::generate_json_from_schema(schema);
278 }
279
280 json!({})
281 }
282
283 fn generate_json_from_schema(schema: &Schema) -> Value {
285 match &schema.schema_kind {
286 SchemaKind::Type(Type::Object(obj)) => {
287 let mut map = serde_json::Map::new();
288
289 for (key, schema_ref) in &obj.properties {
290 if let ReferenceOr::Item(prop_schema) = schema_ref {
291 map.insert(key.clone(), Self::generate_json_from_schema(prop_schema));
292 }
293 }
294
295 Value::Object(map)
296 }
297 SchemaKind::Type(Type::Array(arr)) => {
298 if let Some(ReferenceOr::Item(item_schema)) = &arr.items {
299 return json!([Self::generate_json_from_schema(item_schema)]);
300 }
301 json!([])
302 }
303 SchemaKind::Type(Type::String(_)) => Self::generate_string_value(schema),
304 SchemaKind::Type(Type::Number(n)) => n
305 .enumeration
306 .iter()
307 .flatten()
308 .next()
309 .map(|v| json!(v))
310 .unwrap_or_else(|| json!(42.0)),
311 SchemaKind::Type(Type::Integer(i)) => i
312 .enumeration
313 .iter()
314 .flatten()
315 .next()
316 .map(|v| json!(v))
317 .unwrap_or_else(|| json!(42)),
318 SchemaKind::Type(Type::Boolean(_)) => json!(true),
319 _ => json!(null),
320 }
321 }
322
323 fn generate_string_value(schema: &Schema) -> Value {
325 if let Some(example) = &schema.schema_data.example {
327 return example.clone();
328 }
329 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
334 if let Some(first) = s.enumeration.iter().flatten().next() {
335 return json!(first);
336 }
337 }
338
339 json!("test-string")
340 }
341
342 fn generate_value_from_schema(schema: &Schema) -> String {
344 match &schema.schema_kind {
345 SchemaKind::Type(Type::String(_)) => "test-value".to_string(),
346 SchemaKind::Type(Type::Number(_)) => "42.0".to_string(),
347 SchemaKind::Type(Type::Integer(_)) => "42".to_string(),
348 SchemaKind::Type(Type::Boolean(_)) => "true".to_string(),
349 _ => "test-value".to_string(),
350 }
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357 use openapiv3::Operation;
358
359 #[test]
362 fn generate_string_value_uses_enum_member() {
363 use openapiv3::{Schema, SchemaData, SchemaKind, StringType, Type};
364 let st = StringType {
365 enumeration: vec![Some("EVALUATION".into()), Some("PAYG".into())],
366 ..Default::default()
367 };
368 let schema = Schema {
369 schema_data: SchemaData::default(),
370 schema_kind: SchemaKind::Type(Type::String(st)),
371 };
372 assert_eq!(RequestGenerator::generate_string_value(&schema), json!("EVALUATION"));
373
374 let plain = Schema {
376 schema_data: SchemaData::default(),
377 schema_kind: SchemaKind::Type(Type::String(StringType::default())),
378 };
379 assert_eq!(RequestGenerator::generate_string_value(&plain), json!("test-string"));
380 }
381
382 #[test]
383 fn test_generate_path() {
384 let op = ApiOperation {
385 method: "get".to_string(),
386 path: "/users/{id}".to_string(),
387 operation: Operation::default(),
388 operation_id: None,
389 };
390
391 let mut template = RequestTemplate {
392 operation: op,
393 path_params: HashMap::new(),
394 query_params: HashMap::new(),
395 headers: HashMap::new(),
396 body: None,
397 };
398
399 template.path_params.insert("id".to_string(), "123".to_string());
400 template.query_params.insert("limit".to_string(), "10".to_string());
401
402 let path = template.generate_path();
403 assert_eq!(path, "/users/123?limit=10");
404 }
405
406 #[test]
407 fn test_default_param_value() {
408 assert_eq!(RequestGenerator::default_param_value("id"), "1");
409 assert_eq!(RequestGenerator::default_param_value("limit"), "10");
410 assert_eq!(RequestGenerator::default_param_value("unknown"), "test-value");
411 }
412
413 #[test]
416 fn spec_content_length_header_is_not_invented() {
417 use openapiv3::{HeaderStyle, IntegerType, ParameterData, Schema, SchemaData, SchemaKind};
418
419 let mut operation = Operation::default();
420 operation.parameters.push(ReferenceOr::Item(Parameter::Header {
421 parameter_data: ParameterData {
422 name: "Content-Length".to_string(),
423 description: None,
424 required: false,
425 deprecated: None,
426 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
427 schema_data: SchemaData::default(),
428 schema_kind: SchemaKind::Type(Type::Integer(IntegerType::default())),
429 })),
430 example: None,
431 examples: Default::default(),
432 explode: None,
433 extensions: Default::default(),
434 },
435 style: HeaderStyle::Simple,
436 }));
437 operation.parameters.push(ReferenceOr::Item(Parameter::Header {
439 parameter_data: ParameterData {
440 name: "x-amz-request-route".to_string(),
441 description: None,
442 required: false,
443 deprecated: None,
444 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
445 schema_data: SchemaData::default(),
446 schema_kind: SchemaKind::Type(Type::String(openapiv3::StringType::default())),
447 })),
448 example: None,
449 examples: Default::default(),
450 explode: None,
451 extensions: Default::default(),
452 },
453 style: HeaderStyle::Simple,
454 }));
455
456 let api_op = ApiOperation {
457 method: "post".to_string(),
458 path: "/WriteGetObjectResponse".to_string(),
459 operation,
460 operation_id: Some("WriteGetObjectResponse".to_string()),
461 };
462 let template = RequestGenerator::generate_template(&api_op).expect("template");
463 let headers = template.get_headers();
464 assert!(
465 !headers.keys().any(|k| k.eq_ignore_ascii_case("content-length")),
466 "invented Content-Length={:?} would make k6 drop empty-body POSTs",
467 headers.get("Content-Length")
468 );
469 assert_eq!(headers.get("x-amz-request-route").map(String::as_str), Some("test-value"));
470 assert!(RequestGenerator::is_transport_owned_header("Content-Length"));
471 assert!(RequestGenerator::is_transport_owned_header("HOST"));
472 assert!(!RequestGenerator::is_transport_owned_header("x-amz-request-route"));
473 }
474
475 #[test]
476 fn connection_header_detection_is_case_insensitive() {
477 let mut headers = HashMap::new();
478 headers.insert("connection".to_string(), "x-real-ip".to_string());
479 assert!(header_map_has_connection(&headers));
480 headers.clear();
481 headers.insert("X-Custom".to_string(), "1".to_string());
482 assert!(!header_map_has_connection(&headers));
483 assert!(should_force_k6_http1(true, &[], &HashMap::new()));
484 assert!(!should_force_k6_http1(false, &[], &HashMap::new()));
485 }
486}