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 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
55pub struct RequestGenerator;
57
58impl RequestGenerator {
59 pub fn generate_template(operation: &ApiOperation) -> Result<RequestTemplate> {
61 Self::generate_template_with_overrides(operation, None)
62 }
63
64 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 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 if let Some(ovr) = overrides {
89 for (name, value) in &ovr.path_params {
91 template.path_params.entry(name.clone()).or_insert_with(|| value.clone());
92 }
93 for (name, value) in &ovr.query_params {
95 template.query_params.entry(name.clone()).or_insert_with(|| value.clone());
96 }
97 for (name, value) in &ovr.headers {
99 template.headers.entry(name.clone()).or_insert_with(|| value.clone());
100 }
101 }
102
103 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 fn process_parameter_with_overrides(
120 param: &Parameter,
121 template: &mut RequestTemplate,
122 overrides: Option<&OperationOverrides>,
123 ) -> Result<()> {
124 let (param_type, param_data) = match param {
125 Parameter::Query { parameter_data, .. } => ("query", parameter_data),
126 Parameter::Path { parameter_data, .. } => ("path", parameter_data),
127 Parameter::Header { parameter_data, .. } => ("header", parameter_data),
128 Parameter::Cookie { parameter_data, .. } => ("cookie", parameter_data),
129 };
130
131 let value = if let Some(ovr) = overrides {
133 match param_type {
134 "path" => ovr.get_path_param(¶m_data.name).cloned(),
135 "query" => ovr.get_query_param(¶m_data.name).cloned(),
136 "header" => ovr.get_header(¶m_data.name).cloned(),
137 _ => None,
138 }
139 } else {
140 None
141 }
142 .unwrap_or_else(|| Self::generate_param_value(param_data).unwrap_or_default());
143
144 match param_type {
145 "query" => {
146 template.query_params.insert(param_data.name.clone(), value);
147 }
148 "path" => {
149 template.path_params.insert(param_data.name.clone(), value);
150 }
151 "header" => {
152 let from_override =
159 overrides.and_then(|o| o.get_header(¶m_data.name)).is_some();
160 if Self::is_transport_owned_header(¶m_data.name) && !from_override {
161 } else {
163 template.headers.insert(param_data.name.clone(), value);
164 }
165 }
166 "cookie" => {
167 let cookie_pair = format!("{}={}", param_data.name, value);
169 template
170 .headers
171 .entry("Cookie".to_string())
172 .and_modify(|existing| {
173 existing.push_str("; ");
174 existing.push_str(&cookie_pair);
175 })
176 .or_insert(cookie_pair);
177 }
178 _ => {}
179 }
180
181 Ok(())
182 }
183
184 pub(crate) fn is_transport_owned_header(name: &str) -> bool {
188 matches!(
189 name.to_ascii_lowercase().as_str(),
190 "content-length"
191 | "transfer-encoding"
192 | "host"
193 | "connection"
194 | "keep-alive"
195 | "te"
196 | "trailer"
197 | "upgrade"
198 | "proxy-connection"
199 )
200 }
201
202 fn generate_param_value(param_data: &ParameterData) -> Result<String> {
204 if let Some(example) = ¶m_data.example {
206 return Ok(example.to_string().trim_matches('"').to_string());
207 }
208
209 if let ParameterSchemaOrContent::Schema(ReferenceOr::Item(schema)) = ¶m_data.format {
211 return Ok(Self::generate_value_from_schema(schema));
212 }
213
214 Ok(Self::default_param_value(¶m_data.name))
216 }
217
218 fn default_param_value(name: &str) -> String {
220 match name.to_lowercase().as_str() {
221 "id" => "1".to_string(),
222 "limit" => "10".to_string(),
223 "offset" => "0".to_string(),
224 "page" => "1".to_string(),
225 "sort" => "name".to_string(),
226 _ => "test-value".to_string(),
227 }
228 }
229
230 fn generate_body(request_body: &RequestBody) -> Result<Option<Value>> {
232 if let Some(content) = request_body.content.get("application/json") {
234 return Ok(Some(Self::generate_json_body(content)));
235 }
236
237 Ok(None)
238 }
239
240 fn generate_json_body(media_type: &MediaType) -> Value {
242 if let Some(example) = &media_type.example {
244 return example.clone();
245 }
246
247 if let Some(ReferenceOr::Item(schema)) = &media_type.schema {
249 return Self::generate_json_from_schema(schema);
250 }
251
252 json!({})
253 }
254
255 fn generate_json_from_schema(schema: &Schema) -> Value {
257 match &schema.schema_kind {
258 SchemaKind::Type(Type::Object(obj)) => {
259 let mut map = serde_json::Map::new();
260
261 for (key, schema_ref) in &obj.properties {
262 if let ReferenceOr::Item(prop_schema) = schema_ref {
263 map.insert(key.clone(), Self::generate_json_from_schema(prop_schema));
264 }
265 }
266
267 Value::Object(map)
268 }
269 SchemaKind::Type(Type::Array(arr)) => {
270 if let Some(ReferenceOr::Item(item_schema)) = &arr.items {
271 return json!([Self::generate_json_from_schema(item_schema)]);
272 }
273 json!([])
274 }
275 SchemaKind::Type(Type::String(_)) => Self::generate_string_value(schema),
276 SchemaKind::Type(Type::Number(n)) => n
277 .enumeration
278 .iter()
279 .flatten()
280 .next()
281 .map(|v| json!(v))
282 .unwrap_or_else(|| json!(42.0)),
283 SchemaKind::Type(Type::Integer(i)) => i
284 .enumeration
285 .iter()
286 .flatten()
287 .next()
288 .map(|v| json!(v))
289 .unwrap_or_else(|| json!(42)),
290 SchemaKind::Type(Type::Boolean(_)) => json!(true),
291 _ => json!(null),
292 }
293 }
294
295 fn generate_string_value(schema: &Schema) -> Value {
297 if let Some(example) = &schema.schema_data.example {
299 return example.clone();
300 }
301 if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
306 if let Some(first) = s.enumeration.iter().flatten().next() {
307 return json!(first);
308 }
309 }
310
311 json!("test-string")
312 }
313
314 fn generate_value_from_schema(schema: &Schema) -> String {
316 match &schema.schema_kind {
317 SchemaKind::Type(Type::String(_)) => "test-value".to_string(),
318 SchemaKind::Type(Type::Number(_)) => "42.0".to_string(),
319 SchemaKind::Type(Type::Integer(_)) => "42".to_string(),
320 SchemaKind::Type(Type::Boolean(_)) => "true".to_string(),
321 _ => "test-value".to_string(),
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use openapiv3::Operation;
330
331 #[test]
334 fn generate_string_value_uses_enum_member() {
335 use openapiv3::{Schema, SchemaData, SchemaKind, StringType, Type};
336 let st = StringType {
337 enumeration: vec![Some("EVALUATION".into()), Some("PAYG".into())],
338 ..Default::default()
339 };
340 let schema = Schema {
341 schema_data: SchemaData::default(),
342 schema_kind: SchemaKind::Type(Type::String(st)),
343 };
344 assert_eq!(RequestGenerator::generate_string_value(&schema), json!("EVALUATION"));
345
346 let plain = Schema {
348 schema_data: SchemaData::default(),
349 schema_kind: SchemaKind::Type(Type::String(StringType::default())),
350 };
351 assert_eq!(RequestGenerator::generate_string_value(&plain), json!("test-string"));
352 }
353
354 #[test]
355 fn test_generate_path() {
356 let op = ApiOperation {
357 method: "get".to_string(),
358 path: "/users/{id}".to_string(),
359 operation: Operation::default(),
360 operation_id: None,
361 };
362
363 let mut template = RequestTemplate {
364 operation: op,
365 path_params: HashMap::new(),
366 query_params: HashMap::new(),
367 headers: HashMap::new(),
368 body: None,
369 };
370
371 template.path_params.insert("id".to_string(), "123".to_string());
372 template.query_params.insert("limit".to_string(), "10".to_string());
373
374 let path = template.generate_path();
375 assert_eq!(path, "/users/123?limit=10");
376 }
377
378 #[test]
379 fn test_default_param_value() {
380 assert_eq!(RequestGenerator::default_param_value("id"), "1");
381 assert_eq!(RequestGenerator::default_param_value("limit"), "10");
382 assert_eq!(RequestGenerator::default_param_value("unknown"), "test-value");
383 }
384
385 #[test]
388 fn spec_content_length_header_is_not_invented() {
389 use openapiv3::{HeaderStyle, IntegerType, ParameterData, Schema, SchemaData, SchemaKind};
390
391 let mut operation = Operation::default();
392 operation.parameters.push(ReferenceOr::Item(Parameter::Header {
393 parameter_data: ParameterData {
394 name: "Content-Length".to_string(),
395 description: None,
396 required: false,
397 deprecated: None,
398 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
399 schema_data: SchemaData::default(),
400 schema_kind: SchemaKind::Type(Type::Integer(IntegerType::default())),
401 })),
402 example: None,
403 examples: Default::default(),
404 explode: None,
405 extensions: Default::default(),
406 },
407 style: HeaderStyle::Simple,
408 }));
409 operation.parameters.push(ReferenceOr::Item(Parameter::Header {
411 parameter_data: ParameterData {
412 name: "x-amz-request-route".to_string(),
413 description: None,
414 required: false,
415 deprecated: None,
416 format: ParameterSchemaOrContent::Schema(ReferenceOr::Item(Schema {
417 schema_data: SchemaData::default(),
418 schema_kind: SchemaKind::Type(Type::String(openapiv3::StringType::default())),
419 })),
420 example: None,
421 examples: Default::default(),
422 explode: None,
423 extensions: Default::default(),
424 },
425 style: HeaderStyle::Simple,
426 }));
427
428 let api_op = ApiOperation {
429 method: "post".to_string(),
430 path: "/WriteGetObjectResponse".to_string(),
431 operation,
432 operation_id: Some("WriteGetObjectResponse".to_string()),
433 };
434 let template = RequestGenerator::generate_template(&api_op).expect("template");
435 let headers = template.get_headers();
436 assert!(
437 !headers.keys().any(|k| k.eq_ignore_ascii_case("content-length")),
438 "invented Content-Length={:?} would make k6 drop empty-body POSTs",
439 headers.get("Content-Length")
440 );
441 assert_eq!(headers.get("x-amz-request-route").map(String::as_str), Some("test-value"));
442 assert!(RequestGenerator::is_transport_owned_header("Content-Length"));
443 assert!(RequestGenerator::is_transport_owned_header("HOST"));
444 assert!(!RequestGenerator::is_transport_owned_header("x-amz-request-route"));
445 }
446}