use mockforge_openapi::OpenApiSpec;
use openapiv3::Operation;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedCommand {
pub operation_id: String,
pub method: String,
pub url: String,
pub path_template: String,
pub headers: HashMap<String, String>,
pub body: Option<String>,
pub curl_command: String,
pub httpie_command: String,
pub description: Option<String>,
pub parameter_examples: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct CommandGenerationOptions {
pub base_url: Option<String>,
pub format: CommandFormat,
pub include_auth: bool,
pub all_operations: bool,
pub include_examples: bool,
pub custom_headers: HashMap<String, String>,
pub max_examples_per_operation: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum CommandFormat {
Curl,
Httpie,
Both,
}
#[derive(Debug)]
pub struct CommandGenerationResult {
pub commands: Vec<GeneratedCommand>,
pub warnings: Vec<String>,
pub spec_info: OpenApiSpecInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenApiSpecInfo {
pub title: String,
pub version: String,
pub description: Option<String>,
pub openapi_version: String,
pub servers: Vec<String>,
}
pub fn generate_commands_from_openapi(
spec_content: &str,
options: CommandGenerationOptions,
) -> Result<CommandGenerationResult, String> {
let json_value: serde_json::Value =
serde_json::from_str(spec_content).map_err(|e| format!("Failed to parse JSON: {}", e))?;
let spec = OpenApiSpec::from_json(json_value)
.map_err(|e| format!("Failed to load OpenAPI spec: {}", e))?;
spec.validate().map_err(|e| format!("Invalid OpenAPI specification: {}", e))?;
let spec_info = OpenApiSpecInfo {
title: spec.title().to_string(),
version: spec.version().to_string(),
description: spec.description().map(|s| s.to_string()),
openapi_version: spec.spec.openapi.clone(),
servers: spec
.spec
.servers
.iter()
.filter_map(|server| server.url.parse::<url::Url>().ok())
.map(|url| url.to_string())
.collect(),
};
let base_url = options
.base_url
.clone()
.or_else(|| spec_info.servers.first().cloned())
.unwrap_or_else(|| "http://localhost:3000".to_string());
let mut commands = Vec::new();
let mut warnings = Vec::new();
let path_operations = spec.all_paths_and_operations();
for (path, operations) in path_operations {
for (method, operation) in operations {
match generate_commands_for_operation(
&spec, &method, &path, &operation, &base_url, &options,
) {
Ok(mut op_commands) => commands.append(&mut op_commands),
Err(e) => warnings
.push(format!("Failed to generate commands for {} {}: {}", method, path, e)),
}
}
}
Ok(CommandGenerationResult {
commands,
warnings,
spec_info,
})
}
fn generate_commands_for_operation(
spec: &OpenApiSpec,
method: &str,
path: &str,
operation: &Operation,
base_url: &str,
options: &CommandGenerationOptions,
) -> Result<Vec<GeneratedCommand>, String> {
let operation_id = operation.operation_id.clone().unwrap_or_else(|| {
format!("{}_{}", method.to_lowercase(), path.replace("/", "_").trim_matches('_'))
});
let description = operation
.summary
.as_ref()
.or(operation.description.as_ref())
.map(|s| s.to_string());
let parameter_combinations =
generate_parameter_combinations(spec, operation, options.max_examples_per_operation)?;
if parameter_combinations.is_empty() && !options.all_operations {
return Ok(Vec::new());
}
let mut commands = Vec::new();
let combinations = if parameter_combinations.is_empty() {
vec![HashMap::new()]
} else {
parameter_combinations
};
for params in combinations {
let url = build_url_with_params(base_url, path, ¶ms)?;
let headers = build_headers(spec, operation, ¶ms, options)?;
let body = build_request_body(operation, ¶ms, options)?;
let curl_command = generate_curl_command(method, &url, &headers, &body, ¶ms);
let httpie_command = generate_httpie_command(method, &url, &headers, &body, ¶ms);
commands.push(GeneratedCommand {
operation_id: operation_id.to_string(),
method: method.to_uppercase(),
url: url.clone(),
path_template: path.to_string(),
headers: headers.clone(),
body: body.clone(),
curl_command,
httpie_command,
description: description.clone(),
parameter_examples: params,
});
}
Ok(commands)
}
fn generate_parameter_combinations(
spec: &OpenApiSpec,
operation: &Operation,
_max_combinations: usize,
) -> Result<Vec<HashMap<String, String>>, String> {
let mut params = HashMap::new();
for param_ref in &operation.parameters {
let param = match param_ref {
openapiv3::ReferenceOr::Item(param) => Some(param.clone()),
openapiv3::ReferenceOr::Reference { reference } => {
if let Some(param_name) = reference.strip_prefix("#/components/parameters/") {
if let Some(components) = &spec.spec.components {
if let Some(param_ref) = components.parameters.get(param_name) {
param_ref.as_item().cloned()
} else {
None
}
} else {
None
}
} else {
None
}
}
};
if let Some(param) = param {
let (name, example_value) = match param {
openapiv3::Parameter::Path { parameter_data, .. }
| openapiv3::Parameter::Query { parameter_data, .. }
| openapiv3::Parameter::Header { parameter_data, .. }
| openapiv3::Parameter::Cookie { parameter_data, .. } => {
let name = parameter_data.name.clone();
let example = generate_parameter_example(¶meter_data);
(name, example)
}
};
params.insert(name, example_value);
}
}
if params.is_empty() {
Ok(vec![HashMap::new()])
} else {
Ok(vec![params])
}
}
fn generate_parameter_example(parameter_data: &openapiv3::ParameterData) -> String {
if let Some(example) = ¶meter_data.example {
return serde_json::to_string(example).unwrap_or_else(|_| "example".to_string());
}
match ¶meter_data.format {
openapiv3::ParameterSchemaOrContent::Schema(schema_ref) => {
match schema_ref {
openapiv3::ReferenceOr::Item(schema) => generate_example_from_schema(schema),
openapiv3::ReferenceOr::Reference { .. } => {
"example".to_string()
}
}
}
openapiv3::ParameterSchemaOrContent::Content(_) => {
"example".to_string()
}
}
}
fn generate_example_from_schema(schema: &openapiv3::Schema) -> String {
match &schema.schema_kind {
openapiv3::SchemaKind::Type(openapiv3::Type::String(_)) => "example_string".to_string(),
openapiv3::SchemaKind::Type(openapiv3::Type::Integer(_)) => "42".to_string(),
openapiv3::SchemaKind::Type(openapiv3::Type::Number(_)) => "3.14".to_string(),
openapiv3::SchemaKind::Type(openapiv3::Type::Boolean(_)) => "true".to_string(),
openapiv3::SchemaKind::Type(openapiv3::Type::Object(_)) => "{}".to_string(),
openapiv3::SchemaKind::Type(openapiv3::Type::Array(_)) => "[]".to_string(),
_ => "example".to_string(),
}
}
fn build_url_with_params(
base_url: &str,
path_template: &str,
params: &HashMap<String, String>,
) -> Result<String, String> {
let mut url = base_url.trim_end_matches('/').to_string();
let mut path = path_template.to_string();
for (key, value) in params {
let placeholder = format!("{{{}}}", key);
path = path.replace(&placeholder, value);
}
url.push_str(&path);
let query_params: Vec<String> = params
.iter()
.filter(|(key, _)| !path_template.contains(&format!("{{{}}}", key)))
.map(|(key, value)| format!("{}={}", key, urlencoding::encode(value)))
.collect();
if !query_params.is_empty() {
url.push('?');
url.push_str(&query_params.join("&"));
}
Ok(url)
}
fn build_headers(
spec: &OpenApiSpec,
operation: &Operation,
_params: &HashMap<String, String>,
options: &CommandGenerationOptions,
) -> Result<HashMap<String, String>, String> {
let mut headers = HashMap::new();
for (key, value) in &options.custom_headers {
headers.insert(key.clone(), value.clone());
}
if let Some(request_body) = &operation.request_body {
if let Some(_content) =
request_body.as_item().and_then(|rb| rb.content.get("application/json"))
{
headers.insert("Content-Type".to_string(), "application/json".to_string());
}
}
if options.include_auth {
add_security_headers(spec, operation, &mut headers)?;
}
Ok(headers)
}
fn add_security_headers(
spec: &OpenApiSpec,
operation: &Operation,
headers: &mut HashMap<String, String>,
) -> Result<(), String> {
let security_requirements = if let Some(ref security_reqs) = operation.security {
if !security_reqs.is_empty() {
security_reqs.clone()
} else {
spec.get_global_security_requirements()
}
} else {
spec.get_global_security_requirements()
};
if security_requirements.is_empty() {
return Ok(());
}
let security_schemes = match spec.security_schemes() {
Some(schemes) => schemes,
None => return Ok(()), };
for requirement in &security_requirements {
for (scheme_name, _scopes) in requirement {
if let Some(scheme_ref) = security_schemes.get(scheme_name) {
if let Some(scheme) = scheme_ref.as_item() {
match scheme {
openapiv3::SecurityScheme::HTTP { scheme, .. } => {
match scheme.as_str() {
"bearer" => {
headers.insert(
"Authorization".to_string(),
"Bearer YOUR_TOKEN_HERE".to_string(),
);
}
"basic" => {
headers.insert(
"Authorization".to_string(),
"Basic YOUR_CREDENTIALS_HERE".to_string(),
);
}
_ => {
headers.insert(
"Authorization".to_string(),
format!("{} YOUR_CREDENTIALS_HERE", scheme.to_uppercase()),
);
}
}
}
openapiv3::SecurityScheme::APIKey { location, name, .. } => {
match location {
openapiv3::APIKeyLocation::Header => {
headers.insert(name.clone(), "YOUR_API_KEY_HERE".to_string());
}
openapiv3::APIKeyLocation::Query => {
}
openapiv3::APIKeyLocation::Cookie => {
}
}
}
openapiv3::SecurityScheme::OpenIDConnect { .. } => {
headers.insert(
"Authorization".to_string(),
"Bearer YOUR_OIDC_TOKEN_HERE".to_string(),
);
}
openapiv3::SecurityScheme::OAuth2 { .. } => {
headers.insert(
"Authorization".to_string(),
"Bearer YOUR_OAUTH_TOKEN_HERE".to_string(),
);
}
}
}
}
}
}
Ok(())
}
fn build_request_body(
operation: &Operation,
_params: &HashMap<String, String>,
options: &CommandGenerationOptions,
) -> Result<Option<String>, String> {
if !options.include_examples {
return Ok(None);
}
if let Some(request_body) = &operation.request_body {
if let Some(content) =
request_body.as_item().and_then(|rb| rb.content.get("application/json"))
{
if let Some(example) = &content.example {
return serde_json::to_string_pretty(example)
.map(Some)
.map_err(|e| format!("Failed to serialize example: {}", e));
}
}
}
Ok(None)
}
fn generate_curl_command(
method: &str,
url: &str,
headers: &HashMap<String, String>,
body: &Option<String>,
_params: &HashMap<String, String>,
) -> String {
let mut cmd = format!("curl -X {} '{}'", method.to_uppercase(), url);
for (key, value) in headers {
cmd.push_str(&format!(" \\\n -H '{}: {}'", key, value));
}
if let Some(body_content) = body {
cmd.push_str(&format!(" \\\n -d '{}'", body_content.replace("'", "\\'")));
}
cmd
}
fn generate_httpie_command(
method: &str,
url: &str,
headers: &HashMap<String, String>,
body: &Option<String>,
_params: &HashMap<String, String>,
) -> String {
let mut cmd = format!("http {} '{}'", method.to_uppercase(), url);
for (key, value) in headers {
cmd.push_str(&format!(" \\\n {}:'{}'", key, value));
}
if let Some(body_content) = body {
cmd.push_str(&format!(" \\\n <<< '{}'", body_content.replace("'", "\\'")));
}
cmd
}