use crate::insomnia_import::import_insomnia_export;
use clap::Subcommand;
use colored::Colorize;
use mockforge_import::import::asyncapi_import::{import_asyncapi_spec, AsyncApiImportResult};
use mockforge_import::import::openapi_import::{import_openapi_spec, OpenApiImportResult};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Subcommand)]
pub enum ImportCommands {
#[command(verbatim_doc_comment)]
Openapi {
spec_path: String,
#[arg(short, long)]
base_url: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short = 'V', long)]
verbose: bool,
#[arg(long)]
all_responses: bool,
},
#[command(verbatim_doc_comment)]
Asyncapi {
spec_path: String,
#[arg(short, long)]
base_url: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long)]
protocol: Option<String>,
#[arg(short = 'V', long)]
verbose: bool,
},
#[command(verbatim_doc_comment)]
Insomnia {
export_path: PathBuf,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(short, long)]
environment: Option<String>,
#[arg(short = 'V', long)]
verbose: bool,
},
#[command(verbatim_doc_comment)]
Coverage {
spec_path: String,
#[arg(short = 't', long, default_value = "auto")]
spec_type: String,
},
}
pub async fn handle_import_command(
command: ImportCommands,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match command {
ImportCommands::Openapi {
spec_path,
base_url,
output,
verbose,
all_responses: _all_responses,
} => {
handle_openapi_import(&spec_path, base_url.as_deref(), output.as_deref(), verbose).await
}
ImportCommands::Asyncapi {
spec_path,
base_url,
output,
protocol,
verbose,
} => {
handle_asyncapi_import(
&spec_path,
base_url.as_deref(),
output.as_deref(),
protocol.as_deref(),
verbose,
)
.await
}
ImportCommands::Insomnia {
export_path,
output,
environment,
verbose,
} => {
handle_insomnia_import(&export_path, output.as_deref(), environment.as_deref(), verbose)
.await
}
ImportCommands::Coverage {
spec_path,
spec_type,
} => handle_coverage_report(&spec_path, &spec_type).await,
}
}
async fn handle_openapi_import(
spec_path: &str,
base_url: Option<&str>,
output: Option<&Path>,
verbose: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("{}", "📋 Importing OpenAPI Specification...".cyan().bold());
println!();
let content = load_spec_content(spec_path).await?;
let json_content = if spec_path.ends_with(".yaml") || spec_path.ends_with(".yml") {
let yaml_value: serde_json::Value =
serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
serde_json::to_string(&yaml_value)?
} else {
content
};
let result = import_openapi_spec(&json_content, base_url)
.map_err(|e| format!("Failed to import OpenAPI spec: {}", e))?;
display_openapi_import_results(&result, verbose);
if let Some(output_path) = output {
save_openapi_routes(&result, output_path)?;
println!();
println!(
"{}",
format!("✅ Saved {} routes to {}", result.routes.len(), output_path.display())
.green()
.bold()
);
}
Ok(())
}
async fn handle_asyncapi_import(
spec_path: &str,
base_url: Option<&str>,
output: Option<&Path>,
protocol_filter: Option<&str>,
verbose: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("{}", "📋 Importing AsyncAPI Specification...".cyan().bold());
println!();
let content = load_spec_content(spec_path).await?;
let mut result = import_asyncapi_spec(&content, base_url)
.map_err(|e| format!("Failed to import AsyncAPI spec: {}", e))?;
if let Some(protocol) = protocol_filter {
result.channels.retain(|ch| {
format!("{:?}", ch.protocol).to_lowercase().contains(&protocol.to_lowercase())
});
}
display_asyncapi_import_results(&result, verbose);
if let Some(output_path) = output {
save_asyncapi_channels(&result.channels, output_path)?;
println!();
println!(
"{}",
format!("✅ Saved {} channels to {}", result.channels.len(), output_path.display())
.green()
.bold()
);
}
Ok(())
}
async fn handle_insomnia_import(
export_path: &Path,
output: Option<&Path>,
environment: Option<&str>,
verbose: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("{}", "📋 Importing Insomnia Export...".cyan().bold());
println!();
let content = fs::read_to_string(export_path)
.map_err(|e| format!("Failed to read Insomnia export: {}", e))?;
let result = import_insomnia_export(&content, environment)
.map_err(|e| format!("Failed to import Insomnia export: {}", e))?;
println!("{}", "📖 Import Results:".bold());
println!(" Routes imported: {}", result.routes.len().to_string().green());
println!(" Variables resolved: {}", result.variables.len().to_string().cyan());
if !result.warnings.is_empty() {
println!();
println!("{}", "⚠️ Warnings:".yellow().bold());
for warning in &result.warnings {
println!(" - {}", warning.yellow());
}
}
if verbose {
println!();
println!("{}", "📍 Imported Routes:".bold());
for route in &result.routes {
println!(" {} {} → {}", route.method.cyan(), route.path, route.response.status);
}
if !result.variables.is_empty() {
println!();
println!("{}", "🔑 Resolved Variables:".bold());
for (key, value) in &result.variables {
println!(" {} = {}", key.cyan(), value);
}
}
}
if let Some(output_path) = output {
let routes_json = serde_json::to_string_pretty(&result.routes)
.map_err(|e| format!("Failed to serialize routes: {}", e))?;
fs::write(output_path, routes_json)
.map_err(|e| format!("Failed to write output file: {}", e))?;
println!();
println!(
"{}",
format!("✅ Saved {} routes to {}", result.routes.len(), output_path.display())
.green()
.bold()
);
}
Ok(())
}
async fn handle_coverage_report(
spec_path: &str,
spec_type: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!("{}", "📊 Generating Coverage Report...".cyan().bold());
println!();
let content = load_spec_content(spec_path).await?;
let detected_type = if spec_type == "auto" {
detect_spec_type(&content)?
} else {
spec_type.to_string()
};
let json_content = if spec_path.ends_with(".yaml") || spec_path.ends_with(".yml") {
let yaml_value: serde_json::Value =
serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
serde_json::to_string(&yaml_value)?
} else {
content.clone()
};
match detected_type.as_str() {
"openapi" => {
let result = import_openapi_spec(&json_content, None)
.map_err(|e| format!("Failed to parse OpenAPI spec: {}", e))?;
display_openapi_coverage(&result);
}
"asyncapi" => {
let result = import_asyncapi_spec(&content, None)
.map_err(|e| format!("Failed to parse AsyncAPI spec: {}", e))?;
display_asyncapi_coverage(&result);
}
_ => {
return Err(format!("Unknown specification type: {}", detected_type).into());
}
}
Ok(())
}
async fn load_spec_content(
spec_path: &str,
) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
if spec_path.starts_with("http://") || spec_path.starts_with("https://") {
println!("📥 Fetching specification from URL: {}", spec_path);
let response = reqwest::get(spec_path).await?;
let content = response.text().await?;
Ok(content)
} else {
println!("📂 Loading specification from file: {}", spec_path);
let content = fs::read_to_string(spec_path)
.map_err(|e| format!("Failed to read file {}: {}", spec_path, e))?;
Ok(content)
}
}
fn detect_spec_type(content: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(content) {
if json.get("openapi").is_some() {
return Ok("openapi".to_string());
} else if json.get("asyncapi").is_some() {
return Ok("asyncapi".to_string());
}
}
if let Ok(yaml) = serde_yaml::from_str::<serde_json::Value>(content) {
if yaml.get("openapi").is_some() {
return Ok("openapi".to_string());
} else if yaml.get("asyncapi").is_some() {
return Ok("asyncapi".to_string());
}
}
Err("Unable to detect specification type. File must be valid OpenAPI or AsyncAPI spec.".into())
}
fn display_openapi_import_results(result: &OpenApiImportResult, verbose: bool) {
println!("{}", "📖 Specification Info:".bold());
println!(" Title: {}", result.spec_info.title.cyan());
println!(" Version: {}", result.spec_info.version.cyan());
if let Some(desc) = &result.spec_info.description {
println!(" Description: {}", desc);
}
println!(" OpenAPI Version: {}", result.spec_info.openapi_version);
if !result.spec_info.servers.is_empty() {
println!("\n{}", "🌐 Servers:".bold());
for server in &result.spec_info.servers {
println!(" • {}", server.green());
}
}
println!();
println!("{}", "✨ Generated Routes:".bold());
println!(" Total Routes: {}", result.routes.len().to_string().green().bold());
let mut method_counts = std::collections::HashMap::new();
for route in &result.routes {
*method_counts.entry(&route.method).or_insert(0) += 1;
}
println!("\n{}", " By Method:".bold());
for (method, count) in method_counts.iter() {
let method_colored = match method.as_str() {
"GET" => method.blue(),
"POST" => method.green(),
"PUT" => method.yellow(),
"DELETE" => method.red(),
"PATCH" => method.magenta(),
_ => method.normal(),
};
println!(" {}: {}", method_colored.bold(), count);
}
if !result.warnings.is_empty() {
println!();
println!("{}", format!("⚠️ {} Warnings:", result.warnings.len()).yellow().bold());
for warning in &result.warnings {
println!(" • {}", warning.yellow());
}
}
if verbose {
println!();
println!("{}", "📋 Route Details:".bold());
for (idx, route) in result.routes.iter().enumerate() {
let method_colored = match route.method.as_str() {
"GET" => route.method.as_str().blue(),
"POST" => route.method.as_str().green(),
"PUT" => route.method.as_str().yellow(),
"DELETE" => route.method.as_str().red(),
"PATCH" => route.method.as_str().magenta(),
_ => route.method.as_str().normal(),
};
println!(
" {}: {} {} → {} {}",
(idx + 1).to_string().dimmed(),
method_colored.bold(),
route.path.cyan(),
route.response.status.to_string().green(),
if route.body.is_some() {
"(with request body)".dimmed().to_string()
} else {
"".to_string()
}
);
}
}
}
fn display_asyncapi_import_results(result: &AsyncApiImportResult, verbose: bool) {
println!("{}", "📖 Specification Info:".bold());
println!(" Title: {}", result.spec_info.title.cyan());
println!(" Version: {}", result.spec_info.version.cyan());
if let Some(desc) = &result.spec_info.description {
println!(" Description: {}", desc);
}
println!(" AsyncAPI Version: {}", result.spec_info.asyncapi_version);
if !result.spec_info.servers.is_empty() {
println!("\n{}", "🌐 Servers:".bold());
for server in &result.spec_info.servers {
println!(" • {}", server.green());
}
}
println!();
println!("{}", "✨ Generated Channels:".bold());
println!(" Total Channels: {}", result.channels.len().to_string().green().bold());
let mut protocol_counts = std::collections::HashMap::new();
for channel in &result.channels {
*protocol_counts.entry(format!("{:?}", channel.protocol)).or_insert(0) += 1;
}
println!("\n{}", " By Protocol:".bold());
for (protocol, count) in protocol_counts.iter() {
println!(" {}: {}", protocol.cyan().bold(), count);
}
if !result.warnings.is_empty() {
println!();
println!("{}", format!("⚠️ {} Warnings:", result.warnings.len()).yellow().bold());
for warning in &result.warnings {
println!(" • {}", warning.yellow());
}
}
if verbose {
println!();
println!("{}", "📋 Channel Details:".bold());
for (idx, channel) in result.channels.iter().enumerate() {
println!(
" {}: {} {} ({})",
(idx + 1).to_string().dimmed(),
format!("{:?}", channel.protocol).cyan().bold(),
channel.path.green(),
channel.name.dimmed()
);
if let Some(desc) = &channel.description {
println!(" Description: {}", desc);
}
println!(" Operations: {}", channel.operations.len());
}
}
}
fn display_openapi_coverage(result: &OpenApiImportResult) {
println!("{}", "📊 Coverage Statistics:".bold());
println!();
let total_routes = result.routes.len();
let routes_with_bodies = result.routes.iter().filter(|r| r.body.is_some()).count();
let routes_with_responses = result.routes.len();
println!(" Total Endpoints: {}", total_routes.to_string().green().bold());
println!(
" Endpoints with Mock Responses: {} ({}%)",
routes_with_responses.to_string().green(),
calculate_percentage(routes_with_responses, total_routes)
);
println!(
" Endpoints with Request Bodies: {} ({}%)",
routes_with_bodies.to_string().green(),
calculate_percentage(routes_with_bodies, total_routes)
);
let mut method_coverage = std::collections::HashMap::new();
for route in &result.routes {
*method_coverage.entry(&route.method).or_insert(0) += 1;
}
println!();
println!("{}", " Coverage by HTTP Method:".bold());
for (method, count) in method_coverage.iter() {
let percentage = calculate_percentage(*count, total_routes);
println!(" {}: {} ({}%)", method.cyan().bold(), count, percentage);
}
let coverage_score = 100; println!();
println!("{}", format!("✅ Overall Coverage: {}%", coverage_score).green().bold());
}
fn display_asyncapi_coverage(result: &AsyncApiImportResult) {
println!("{}", "📊 Coverage Statistics:".bold());
println!();
let total_channels = result.channels.len();
let channels_with_schemas = result
.channels
.iter()
.filter(|ch| ch.operations.iter().any(|op| op.message_schema.is_some()))
.count();
let channels_with_examples = result
.channels
.iter()
.filter(|ch| ch.operations.iter().any(|op| op.example_message.is_some()))
.count();
println!(" Total Channels: {}", total_channels.to_string().green().bold());
println!(
" Channels with Message Schemas: {} ({}%)",
channels_with_schemas.to_string().green(),
calculate_percentage(channels_with_schemas, total_channels)
);
println!(
" Channels with Example Messages: {} ({}%)",
channels_with_examples.to_string().green(),
calculate_percentage(channels_with_examples, total_channels)
);
let mut protocol_coverage = std::collections::HashMap::new();
for channel in &result.channels {
*protocol_coverage.entry(format!("{:?}", channel.protocol)).or_insert(0) += 1;
}
println!();
println!("{}", " Coverage by Protocol:".bold());
for (protocol, count) in protocol_coverage.iter() {
let percentage = calculate_percentage(*count, total_channels);
println!(" {}: {} ({}%)", protocol.cyan().bold(), count, percentage);
}
let coverage_score = if total_channels > 0 {
((channels_with_schemas as f64 / total_channels as f64) * 100.0).round() as u32
} else {
0
};
println!();
println!("{}", format!("✅ Overall Coverage: {}%", coverage_score).green().bold());
}
fn calculate_percentage(count: usize, total: usize) -> u32 {
if total == 0 {
0
} else {
((count as f64 / total as f64) * 100.0).round() as u32
}
}
fn save_openapi_routes(
result: &OpenApiImportResult,
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let json = serde_json::to_string_pretty(&result.routes)?;
fs::write(output_path, json)?;
Ok(())
}
fn save_asyncapi_channels(
channels: &[mockforge_import::import::asyncapi_import::MockForgeChannel],
output_path: &Path,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let json = serde_json::to_string_pretty(&channels)?;
fs::write(output_path, json)?;
Ok(())
}