use crate::error::Result;
use crate::param_overrides::OperationOverrides;
use crate::spec_parser::ApiOperation;
use openapiv3::{
MediaType, Parameter, ParameterData, ParameterSchemaOrContent, ReferenceOr, RequestBody,
Schema, SchemaKind, Type,
};
use serde_json::{json, Value};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct RequestTemplate {
pub operation: ApiOperation,
pub path_params: HashMap<String, String>,
pub query_params: HashMap<String, String>,
pub headers: HashMap<String, String>,
pub body: Option<Value>,
}
impl RequestTemplate {
pub fn generate_path(&self) -> String {
let mut path = self.operation.path.clone();
for (key, value) in &self.path_params {
path = path.replace(&format!("{{{}}}", key), value);
}
if !self.query_params.is_empty() {
let query_string: Vec<String> =
self.query_params.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
path = format!("{}?{}", path, query_string.join("&"));
}
path
}
pub fn get_headers(&self) -> HashMap<String, String> {
let mut headers = self.headers.clone();
if self.body.is_some() {
headers
.entry("Content-Type".to_string())
.or_insert_with(|| "application/json".to_string());
}
headers
}
}
pub struct RequestGenerator;
impl RequestGenerator {
pub fn generate_template(operation: &ApiOperation) -> Result<RequestTemplate> {
Self::generate_template_with_overrides(operation, None)
}
pub fn generate_template_with_overrides(
operation: &ApiOperation,
overrides: Option<&OperationOverrides>,
) -> Result<RequestTemplate> {
let mut template = RequestTemplate {
operation: operation.clone(),
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
for param_ref in &operation.operation.parameters {
if let ReferenceOr::Item(param) = param_ref {
Self::process_parameter_with_overrides(param, &mut template, overrides)?;
}
}
if let Some(ovr) = overrides {
for (name, value) in &ovr.path_params {
template.path_params.entry(name.clone()).or_insert_with(|| value.clone());
}
for (name, value) in &ovr.query_params {
template.query_params.entry(name.clone()).or_insert_with(|| value.clone());
}
for (name, value) in &ovr.headers {
template.headers.entry(name.clone()).or_insert_with(|| value.clone());
}
}
if let Some(ovr) = overrides {
if let Some(body) = ovr.get_body() {
template.body = Some(body.clone());
} else if let Some(ReferenceOr::Item(request_body)) = &operation.operation.request_body
{
template.body = Self::generate_body(request_body)?;
}
} else if let Some(ReferenceOr::Item(request_body)) = &operation.operation.request_body {
template.body = Self::generate_body(request_body)?;
}
Ok(template)
}
fn process_parameter_with_overrides(
param: &Parameter,
template: &mut RequestTemplate,
overrides: Option<&OperationOverrides>,
) -> Result<()> {
let (param_type, param_data) = match param {
Parameter::Query { parameter_data, .. } => ("query", parameter_data),
Parameter::Path { parameter_data, .. } => ("path", parameter_data),
Parameter::Header { parameter_data, .. } => ("header", parameter_data),
Parameter::Cookie { parameter_data, .. } => ("cookie", parameter_data),
};
let value = if let Some(ovr) = overrides {
match param_type {
"path" => ovr.get_path_param(¶m_data.name).cloned(),
"query" => ovr.get_query_param(¶m_data.name).cloned(),
"header" => ovr.get_header(¶m_data.name).cloned(),
_ => None,
}
} else {
None
}
.unwrap_or_else(|| Self::generate_param_value(param_data).unwrap_or_default());
match param_type {
"query" => {
template.query_params.insert(param_data.name.clone(), value);
}
"path" => {
template.path_params.insert(param_data.name.clone(), value);
}
"header" => {
template.headers.insert(param_data.name.clone(), value);
}
"cookie" => {
let cookie_pair = format!("{}={}", param_data.name, value);
template
.headers
.entry("Cookie".to_string())
.and_modify(|existing| {
existing.push_str("; ");
existing.push_str(&cookie_pair);
})
.or_insert(cookie_pair);
}
_ => {}
}
Ok(())
}
fn generate_param_value(param_data: &ParameterData) -> Result<String> {
if let Some(example) = ¶m_data.example {
return Ok(example.to_string().trim_matches('"').to_string());
}
if let ParameterSchemaOrContent::Schema(ReferenceOr::Item(schema)) = ¶m_data.format {
return Ok(Self::generate_value_from_schema(schema));
}
Ok(Self::default_param_value(¶m_data.name))
}
fn default_param_value(name: &str) -> String {
match name.to_lowercase().as_str() {
"id" => "1".to_string(),
"limit" => "10".to_string(),
"offset" => "0".to_string(),
"page" => "1".to_string(),
"sort" => "name".to_string(),
_ => "test-value".to_string(),
}
}
fn generate_body(request_body: &RequestBody) -> Result<Option<Value>> {
if let Some(content) = request_body.content.get("application/json") {
return Ok(Some(Self::generate_json_body(content)));
}
Ok(None)
}
fn generate_json_body(media_type: &MediaType) -> Value {
if let Some(example) = &media_type.example {
return example.clone();
}
if let Some(ReferenceOr::Item(schema)) = &media_type.schema {
return Self::generate_json_from_schema(schema);
}
json!({})
}
fn generate_json_from_schema(schema: &Schema) -> Value {
match &schema.schema_kind {
SchemaKind::Type(Type::Object(obj)) => {
let mut map = serde_json::Map::new();
for (key, schema_ref) in &obj.properties {
if let ReferenceOr::Item(prop_schema) = schema_ref {
map.insert(key.clone(), Self::generate_json_from_schema(prop_schema));
}
}
Value::Object(map)
}
SchemaKind::Type(Type::Array(arr)) => {
if let Some(ReferenceOr::Item(item_schema)) = &arr.items {
return json!([Self::generate_json_from_schema(item_schema)]);
}
json!([])
}
SchemaKind::Type(Type::String(_)) => Self::generate_string_value(schema),
SchemaKind::Type(Type::Number(_)) => json!(42.0),
SchemaKind::Type(Type::Integer(_)) => json!(42),
SchemaKind::Type(Type::Boolean(_)) => json!(true),
_ => json!(null),
}
}
fn generate_string_value(schema: &Schema) -> Value {
if let Some(example) = &schema.schema_data.example {
return example.clone();
}
json!("test-string")
}
fn generate_value_from_schema(schema: &Schema) -> String {
match &schema.schema_kind {
SchemaKind::Type(Type::String(_)) => "test-value".to_string(),
SchemaKind::Type(Type::Number(_)) => "42.0".to_string(),
SchemaKind::Type(Type::Integer(_)) => "42".to_string(),
SchemaKind::Type(Type::Boolean(_)) => "true".to_string(),
_ => "test-value".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use openapiv3::Operation;
#[test]
fn test_generate_path() {
let op = ApiOperation {
method: "get".to_string(),
path: "/users/{id}".to_string(),
operation: Operation::default(),
operation_id: None,
};
let mut template = RequestTemplate {
operation: op,
path_params: HashMap::new(),
query_params: HashMap::new(),
headers: HashMap::new(),
body: None,
};
template.path_params.insert("id".to_string(), "123".to_string());
template.query_params.insert("limit".to_string(), "10".to_string());
let path = template.generate_path();
assert_eq!(path, "/users/123?limit=10");
}
#[test]
fn test_default_param_value() {
assert_eq!(RequestGenerator::default_param_value("id"), "1");
assert_eq!(RequestGenerator::default_param_value("limit"), "10");
assert_eq!(RequestGenerator::default_param_value("unknown"), "test-value");
}
}