use crate::cli::RunCommand;
use crate::core::analytics_engine::AnalyticsEngine;
use crate::core::api::ApiError;
use crate::core::config::{AuthScheme, ConfigLoader};
use crate::core::examples::generate_smart_example;
use crate::core::output::{
error_response, exit_codes, is_json_mode, RequestDetails, ResponseEnvelope, RunResponse,
};
use crate::core::query_intelligence::{
generate_parameter_example, save_query_to_history, show_query_help, QueryBuilder,
};
use crate::core::simple_query_builder;
use anyhow::Result;
use colored::*;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
pub fn execute(cmd: RunCommand) -> Result<()> {
if cmd.help_query {
show_query_help();
return Ok(());
}
if cmd.list_queries {
return crate::core::saved_queries::display_saved_queries();
}
if let Some(query_name) = &cmd.load_query {
return load_saved_query(query_name);
}
let operation = cmd.operation.as_ref().ok_or_else(|| {
ApiError::ValidationError(
"Operation is required. Use --list-queries or --load-query for saved queries."
.to_string(),
)
})?;
if cmd.build_query {
if cmd.spec.is_some() {
let spec_path = get_spec_path(&cmd)?; let spec_content = fs::read_to_string(&spec_path)?;
let spec = crate::core::parser::parse_spec(&spec_content)?;
match simple_query_builder::run_query_builder(&spec, operation) {
Ok(_) => return Ok(()),
Err(e) => {
return Err(ApiError::OperationNotFound(format!(
"Operation '{}' not found in spec: {}\n\nUse 'mrapids list operations --spec {}' to see available operations.",
operation,
e,
spec_path.display()
)).into());
}
}
}
if let Ok(spec_path) = get_spec_path(&cmd) {
if let Ok(spec_content) = fs::read_to_string(&spec_path) {
if let Ok(spec) = crate::core::parser::parse_spec(&spec_content) {
match simple_query_builder::run_query_builder(&spec, operation) {
Ok(_) => return Ok(()),
Err(_) => {
println!(
"⚠️ Operation '{}' not found in spec, using basic query builder",
operation
);
}
}
}
}
}
let mut builder = QueryBuilder::new(operation.clone());
builder.interactive_build()?;
return Ok(());
}
if cmd.replay_last {
return replay_last_query(&cmd);
}
if let Some(query_file) = &cmd.query_file {
return load_query_from_file(&cmd, query_file);
}
if cmd.interactive {
return generate_template_interactive(&cmd);
}
if let Some(query_name) = &cmd.save_query {
return save_current_query(&cmd, query_name);
}
let operation_path = PathBuf::from(operation);
if operation_path.exists() {
execute_from_file(&operation_path, &cmd)
} else if cmd.template.is_some() {
execute_from_template(&cmd)
} else {
execute_direct_operation(&cmd)
}
}
fn execute_direct_operation(cmd: &RunCommand) -> Result<()> {
let op_name = cmd
.operation
.as_ref()
.ok_or_else(|| ApiError::ValidationError("Operation is required".to_string()))?;
let quiet = cmd.json_output || is_json_mode();
if !quiet {
println!("⚡ Executing operation: {}", op_name.bright_cyan());
}
let spec_path = get_spec_path(cmd)?;
if !quiet {
println!(
"📋 Using spec: {}",
spec_path.display().to_string().dimmed()
);
}
let spec_content = fs::read_to_string(&spec_path)?;
let spec = crate::core::parser::parse_spec(&spec_content)?;
let operation = crate::core::show::find_operation_with_spec(&spec, op_name)?;
let mut request = build_request_from_operation(operation, cmd, &spec.base_url)?;
apply_environment(&mut request, cmd.env.as_deref(), Some(&spec_path))?;
if request.base_url.is_empty() || request.base_url == "http://localhost" {
if let Ok(url) =
std::env::var("API_BASE_URL").or_else(|_| std::env::var("MRAPIDS_BASE_URL"))
{
if !url.is_empty() {
request.base_url = url;
}
}
}
if request.body.is_some() {
if let Some(spec_ct) = &request.spec_content_type {
request
.headers
.insert("Content-Type".to_string(), spec_ct.clone());
if cmd.verbose {
println!(" Using Content-Type from spec: {}", spec_ct);
}
} else if !request.headers.contains_key("Content-Type") {
request
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
if cmd.verbose {
println!(" Using default Content-Type: application/json");
}
}
}
apply_command_line_overrides(&mut request, cmd)?;
execute_request(&mut request, cmd, op_name)
}
fn execute_from_file(path: &Path, cmd: &RunCommand) -> Result<()> {
let quiet = cmd.json_output || is_json_mode();
if crate::core::request_runner::is_request_config(path) {
if !quiet {
println!(
"📄 Loading request config from: {}",
path.display().to_string().cyan()
);
}
let mut config = crate::core::request_runner::load_request_config(path)?;
if let Some(url) = &cmd.url {
config.base_url = Some(url.clone());
}
let data = if let Some(file) = &cmd.file {
Some(format!("@{}", file.display()))
} else {
cmd.data.clone()
};
return crate::core::request_runner::execute_request_config_with_options(
config,
cmd.url.clone(),
data,
&cmd.output,
cmd.allow_localhost,
);
}
if !quiet {
println!(
"📄 Loading spec from: {}",
path.display().to_string().cyan()
);
}
Err(ApiError::ValidationError(
"Direct spec execution is deprecated. Use:\n\
mrapids run <operation-name> [options]\n\
Example: mrapids run get-user --id 123"
.to_string(),
)
.into())
}
fn execute_from_template(cmd: &RunCommand) -> Result<()> {
let template_name = cmd.template.as_ref().unwrap();
if !cmd.json_output && !is_json_mode() {
println!("📋 Loading template: {}", template_name.bright_cyan());
}
let template_path = find_template(template_name)?;
let template_content = fs::read_to_string(&template_path)?;
let mut vars = HashMap::new();
for var in &cmd.template_vars {
if let Some((key, value)) = var.split_once('=') {
vars.insert(key.to_string(), value.to_string());
}
}
if let Some(id) = &cmd.id {
vars.insert("ID".to_string(), id.clone());
}
if let Some(name) = &cmd.name {
vars.insert("NAME".to_string(), name.clone());
}
let processed = substitute_variables(&template_content, &vars)?;
let config: crate::core::request_runner::RequestConfig = serde_yaml::from_str(&processed)?;
crate::core::request_runner::execute_request_config_with_options(
config,
cmd.url.clone(),
cmd.data.clone(),
&cmd.output,
cmd.allow_localhost,
)
}
fn build_request_from_operation(
operation: &crate::core::parser::UnifiedOperation,
cmd: &RunCommand,
base_url: &str,
) -> Result<Request> {
if cmd.verbose {
println!(
"\n Building request for operation: {}",
operation.operation_id
);
println!(" Operation path: {}", operation.path);
println!(" Command params: {:?}", cmd.params);
}
let mut request = Request {
method: operation.method.clone(),
path: operation.path.clone(),
base_url: base_url.to_string(),
headers: HashMap::new(),
query_params: HashMap::new(),
path_params: HashMap::new(),
body: None,
spec_content_type: None,
};
if let Some(id) = &cmd.id {
if operation.path.contains("{id}") {
request.path_params.insert("id".to_string(), json!(id));
} else if operation.path.contains("{petId}") {
request.path_params.insert("petId".to_string(), json!(id));
} else if operation.path.contains("{userId}") {
request.path_params.insert("userId".to_string(), json!(id));
} else if operation.path.contains("{productId}") {
request
.path_params
.insert("productId".to_string(), json!(id));
} else if operation.path.contains("{orderId}") {
request.path_params.insert("orderId".to_string(), json!(id));
} else if operation.path.contains("{customerId}") {
request
.path_params
.insert("customerId".to_string(), json!(id));
} else {
let id_pattern = regex::Regex::new(r"\{(\w*[iI]d)\}").unwrap();
if let Some(captures) = id_pattern.captures(&operation.path) {
if let Some(param_name) = captures.get(1) {
request
.path_params
.insert(param_name.as_str().to_string(), json!(id));
}
} else {
request.query_params.insert("id".to_string(), id.clone());
}
}
}
if let Some(name) = &cmd.name {
request
.query_params
.insert("name".to_string(), name.clone());
}
if let Some(status) = &cmd.status {
request
.query_params
.insert("status".to_string(), status.clone());
}
if let Some(limit) = &cmd.limit {
request
.query_params
.insert("limit".to_string(), limit.to_string());
}
if let Some(offset) = &cmd.offset {
request
.query_params
.insert("offset".to_string(), offset.to_string());
}
if let Some(sort) = &cmd.sort {
request
.query_params
.insert("sort".to_string(), sort.clone());
}
if cmd.verbose {
println!(" Processing {} generic parameters", cmd.params.len());
}
for param in &cmd.params {
if let Some((key, value)) = param.split_once('=') {
let path_param_pattern = format!("{{{}}}", key);
if cmd.verbose {
println!(
" Checking if '{}' is in path '{}' (pattern: '{}')",
key, operation.path, path_param_pattern
);
}
if operation.path.contains(&path_param_pattern) {
request.path_params.insert(key.to_string(), json!(value));
if cmd.verbose {
println!(" Adding path parameter: {} = {}", key, value);
}
} else {
let is_path_param = operation.parameters.iter().any(|p| {
p.name == key && p.location == crate::core::parser::ParameterLocation::Path
});
if is_path_param {
request.path_params.insert(key.to_string(), json!(value));
if cmd.verbose {
println!(" Adding path parameter (from spec): {} = {}", key, value);
}
} else {
let decoded_value = smart_decode_param(value);
request
.query_params
.insert(key.to_string(), decoded_value.clone());
if cmd.verbose {
println!(" Adding query parameter: {} = {}", key, decoded_value);
if decoded_value != value {
println!(" (decoded from: {})", value);
}
}
}
}
}
}
for param in &cmd.query_params {
if let Some((key, value)) = param.split_once('=') {
let decoded_value = smart_decode_param(value);
request.query_params.insert(key.to_string(), decoded_value);
}
}
request
.headers
.insert("Accept".to_string(), "application/json".to_string());
request
.headers
.insert("User-Agent".to_string(), "MicroRapid/0.1.0".to_string());
if needs_body(&operation.method) {
request.body = if let Some(file) = &cmd.file {
let content = fs::read_to_string(file)?;
Some(content)
} else if let Some(data) = &cmd.data {
if data.starts_with('@') {
let file_path = &data[1..];
let content = fs::read_to_string(file_path)?;
Some(content)
} else {
Some(data.clone())
}
} else if cmd.stdin {
use std::io::Read;
let mut buffer = String::new();
std::io::stdin().read_to_string(&mut buffer)?;
Some(buffer)
} else if cmd.required_only {
generate_required_only_body(operation)?
} else {
load_default_example(&operation.operation_id).ok()
};
if let Some(request_body) = &operation.request_body {
let content_type = if request_body
.content
.contains_key("application/x-www-form-urlencoded")
{
Some("application/x-www-form-urlencoded")
} else if request_body.content.contains_key("application/json") {
Some("application/json")
} else if request_body.content.contains_key("multipart/form-data") {
Some("multipart/form-data")
} else {
request_body.content.keys().next().map(|s| s.as_str())
};
if let Some(ct) = content_type {
request.spec_content_type = Some(ct.to_string());
if cmd.verbose {
println!(" Found Content-Type in spec: {}", ct);
}
}
}
}
Ok(request)
}
struct Request {
method: String,
path: String,
base_url: String,
headers: HashMap<String, String>,
query_params: HashMap<String, String>,
path_params: HashMap<String, Value>,
body: Option<String>,
spec_content_type: Option<String>, }
fn execute_request(request: &mut Request, cmd: &RunCommand, operation_id: &str) -> Result<()> {
let run_id = AnalyticsEngine::generate_run_id();
let request_id = AnalyticsEngine::generate_request_id();
let run_start = std::time::Instant::now();
let json_mode = cmd.json_output || is_json_mode();
let db = AnalyticsEngine::open().ok();
if let Some(ref engine) = db {
let spec_path = get_spec_path(cmd).ok().map(|p| p.display().to_string());
let env_json = cmd.env.as_ref().map(|e| json!({"environment": e}));
let _ = engine.create_run(&run_id, spec_path.as_deref(), env_json.as_ref());
}
if !json_mode {
println!("🔖 Run ID: {}", run_id.bright_magenta());
}
if request.base_url.is_empty() || request.base_url == "http://localhost" {
return create_no_url_error(cmd, request);
}
if request.body.is_some() && !request.headers.contains_key("Content-Type") {
request
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
if cmd.verbose {
println!(" No Content-Type specified, defaulting to application/json");
}
}
use crate::utils::request_warnings::RequestAnalyzer;
use crate::utils::security::enforce_https_with_options;
let full_url = format!("{}{}", request.base_url, request.path);
enforce_https_with_options(&full_url, cmd.allow_insecure, cmd.allow_localhost)?;
let mut analyzer = RequestAnalyzer::new(cmd.no_warnings);
let headers_vec: Vec<(String, String)> = request
.headers
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
analyzer.analyze_headers(&headers_vec);
let query_params_vec: Vec<(String, String)> = request
.query_params
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
analyzer.analyze_url_params(&query_params_vec);
let path_params_vec: Vec<(String, String)> = request
.path_params
.iter()
.filter_map(|(k, v)| {
if let Value::String(s) = v {
Some((k.clone(), s.clone()))
} else {
Some((k.clone(), v.to_string()))
}
})
.collect();
analyzer.analyze_url_params(&path_params_vec);
if let Some(body) = &request.body {
analyzer.analyze_json_body(body);
}
if !cmd.no_warnings && analyzer.has_high_severity_warnings() {
let high_warnings = analyzer.get_high_severity_warnings();
let warning_details: Vec<String> = high_warnings
.iter()
.map(|w| format!("{}: {}", w.location, w.message))
.collect();
if json_mode {
let error_response = serde_json::json!({
"success": false,
"error": "Security check failed: potential injection detected",
"security_warnings": warning_details,
"hint": "Use --no-warnings to bypass (not recommended)"
});
println!("{}", serde_json::to_string_pretty(&error_response)?);
return Ok(());
} else {
return Err(ApiError::ValidationError(format!(
"Security check failed: potential injection detected\n {}\n\nUse --no-warnings to bypass (not recommended)",
warning_details.join("\n ")
)).into());
}
}
if !json_mode {
analyzer.display_warnings();
if cmd.allow_localhost {
use colored::*;
println!(
"\n{} {}",
"⚠️".yellow(),
"LOCALHOST ACCESS ENABLED".yellow().bold()
);
println!(
" {} Allowing connections to localhost and private IPs",
"•".dimmed()
);
println!(
" {} This should only be used for local development",
"•".dimmed()
);
println!(
" {} Do not use in production or CI/CD pipelines\n",
"•".dimmed()
);
}
}
if cmd.verbose && !json_mode {
println!("\n{} Request Details:", "📋".bright_blue());
println!(" Method: {}", request.method.bright_green());
println!(" Path: {}", request.path.bright_cyan());
println!(" Base URL: {}", request.base_url.dimmed());
if !request.headers.is_empty() {
println!(" Headers:");
let headers_json = serde_json::json!(&request.headers);
let redacted = crate::utils::redaction::sanitize_headers_for_storage(&headers_json);
if let Some(map) = redacted.as_object() {
for (key, val) in map {
let display_value = val.as_str().unwrap_or_default();
let styled = if display_value == "[REDACTED]" {
display_value.bright_black()
} else {
display_value.normal()
};
println!(" {}: {}", key.yellow(), styled);
}
}
}
if !request.query_params.is_empty() {
println!(" Query Parameters:");
for (key, value) in &request.query_params {
println!(" {}: {}", key.yellow(), value);
}
}
if let Some(body) = &request.body {
println!(" Body: {}", body.dimmed());
}
println!(); }
if cmd.as_curl {
print_as_curl(request)?;
if cmd.dry_run {
return Ok(());
}
}
if cmd.dry_run {
println!("\n{} Dry run complete (request not sent)", "✅".green());
return Ok(());
}
let path_placeholders: Vec<String> = request
.path
.split('/')
.filter(|s| s.starts_with('{') && s.ends_with('}'))
.map(|s| s[1..s.len() - 1].to_string())
.collect();
let missing_path_params: Vec<&String> = path_placeholders
.iter()
.filter(|p| !request.path_params.contains_key(p.as_str()))
.collect();
if !missing_path_params.is_empty() {
let missing_names: Vec<&str> = missing_path_params.iter().map(|s| s.as_str()).collect();
if json_mode {
let error_json = serde_json::json!({
"success": false,
"command": "run",
"error": "Missing required path parameter(s)",
"missing_params": missing_names,
"message": format!("Required path parameter(s) not provided: {}. Use --param {}=<value>",
missing_names.join(", "),
missing_names[0]),
});
println!("{}", serde_json::to_string_pretty(&error_json)?);
return Ok(());
} else {
return Err(ApiError::ValidationError(format!(
"Missing required path parameter(s): {}.\nProvide with: --param {}=<value>",
missing_names.join(", "),
missing_names[0],
))
.into());
}
}
let mut url_path = request.path.clone();
for (param_name, param_value) in &request.path_params {
let placeholder = format!("{{{}}}", param_name);
let value_str = match param_value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => param_value.to_string(),
};
url_path = url_path.replace(&placeholder, &value_str);
}
let full_url = format!("{}{}", request.base_url.trim_end_matches('/'), url_path);
if !json_mode {
println!("🌐 Request URL: {}", full_url.bright_blue());
println!("🚀 Sending request...");
}
if let Some(ref engine) = db {
let headers_json = json!(request.headers);
let query_json = json!(request.query_params);
let path_json = json!(request.path_params);
let _ = engine.log_request_v2(
&run_id,
&request_id,
Some(operation_id),
&request.path,
&request.method,
Some(&full_url),
Some(&headers_json),
Some(&query_json),
Some(&path_json),
request.body.as_deref(),
);
}
let mut attempts = 0;
let max_attempts = cmd.retry + 1;
let final_success;
loop {
attempts += 1;
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(cmd.timeout as u64))
.build()?;
let mut http_request = match request.method.to_uppercase().as_str() {
"GET" => client.get(&full_url),
"POST" => client.post(&full_url),
"PUT" => client.put(&full_url),
"DELETE" => client.delete(&full_url),
"PATCH" => client.patch(&full_url),
_ => {
return Err(ApiError::ValidationError(format!(
"Unsupported method: {}",
request.method
))
.into())
}
};
for (key, value) in &request.headers {
http_request = http_request.header(key, value);
}
if !request.query_params.is_empty() {
http_request = http_request.query(&request.query_params);
}
if let Some(body) = &request.body {
http_request = http_request.body(body.clone());
}
let start_time = std::time::Instant::now();
match http_request.send() {
Ok(response) => {
let status = response.status().as_u16();
let response_time = start_time.elapsed();
final_success = (200..300).contains(&status);
let response_headers = response.headers().clone();
let status_text = response.status().canonical_reason().map(|s| s.to_string());
let body_text = response.text()?;
let body_size = body_text.len();
if let Some(ref engine) = db {
let headers_map: HashMap<String, String> = response_headers
.iter()
.filter_map(|(k, v)| {
v.to_str()
.ok()
.map(|v| (k.as_str().to_string(), v.to_string()))
})
.collect();
let headers_json = json!(headers_map);
let _ = engine.log_response(
&request_id,
status as i32,
status_text.as_deref(),
Some(&headers_json),
Some(&body_text),
response_time.as_secs_f64() * 1000.0,
final_success,
None,
);
}
if json_mode {
let response_body_json: Option<serde_json::Value> =
serde_json::from_str(&body_text).ok();
let response_body_json = if cmd.redact {
response_body_json.map(|v| {
use crate::utils::redaction::redact_response;
redact_response(&v, true)
})
} else {
response_body_json
};
let response_headers_map: HashMap<String, String> = response_headers
.iter()
.filter_map(|(k, v)| {
v.to_str()
.ok()
.map(|v| (k.as_str().to_string(), v.to_string()))
})
.collect();
let run_response = RunResponse {
operation: operation_id.to_string(),
method: request.method.clone(),
url: full_url.clone(),
status_code: status,
status_text: status_text.clone(),
headers: Some(json!(response_headers_map)),
body: response_body_json.clone(),
body_raw: if response_body_json.is_none() {
Some(body_text.clone())
} else {
None
},
body_size_bytes: Some(body_size),
request: Some(RequestDetails {
headers: Some(json!(request.headers)),
query_params: Some(json!(request.query_params)),
path_params: Some(json!(request.path_params)),
body: request
.body
.as_ref()
.and_then(|b| serde_json::from_str(b).ok()),
}),
};
let duration_ms = response_time.as_secs_f64() * 1000.0;
let envelope = ResponseEnvelope::success_with_run(
"run",
run_response,
run_id.clone(),
Some(request_id.clone()),
duration_ms,
);
println!("{}", envelope.to_json());
} else {
let display_body = if cmd.redact {
if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(&body_text)
{
use crate::utils::redaction::redact_response;
let redacted = redact_response(&json_val, true);
serde_json::to_string_pretty(&redacted).unwrap_or(body_text.clone())
} else {
body_text.clone() }
} else {
body_text.clone()
};
display_response_from_parts(
status,
&response_headers,
&display_body,
&cmd.output,
cmd.save.as_deref(),
)?;
}
if cmd.save_to_collection && final_success {
if let Err(e) = save_to_collection(&request, status, cmd, operation_id) {
if !json_mode {
eprintln!("⚠️ Failed to save to collection: {}", e);
}
}
}
if final_success {
let mut params = HashMap::new();
for (k, v) in &request.query_params {
params.insert(k.clone(), v.clone());
}
if let Err(e) = save_query_to_history(
operation_id.to_string(),
params,
Some(response_time.as_millis() as u64),
Some(status),
true,
) {
if cmd.verbose && !json_mode {
eprintln!("⚠️ Failed to save query history: {}", e);
}
}
}
if !json_mode {
println!(
"📊 Run {} completed: {} {}",
run_id.bright_magenta(),
if final_success {
"✅".green()
} else {
"❌".red()
},
format!("{} {}", status, status_text.unwrap_or_default()).dimmed()
);
}
break;
}
Err(e) => {
if attempts < max_attempts {
if !json_mode {
println!(
"⚠️ Request failed: {}. Retrying ({}/{})...",
e, attempts, max_attempts
);
}
std::thread::sleep(std::time::Duration::from_secs(2));
} else {
if let Some(ref engine) = db {
let _ = engine.log_response(
&request_id,
0,
None,
None,
None,
0.0,
false,
Some(&e.to_string()),
);
}
let run_duration = run_start.elapsed().as_secs_f64() * 1000.0;
if let Some(ref engine) = db {
let _ = engine.complete_run(&run_id, 1, 0, 1, run_duration, "failed");
}
if json_mode {
let error_envelope = error_response(
"run",
"NETWORK_ERROR",
&format!("Request failed after {} attempts: {}", max_attempts, e),
exit_codes::NETWORK_ERROR,
);
println!("{}", error_envelope.to_json());
std::process::exit(exit_codes::NETWORK_ERROR);
}
return Err(ApiError::NetworkError(format!(
"Request failed after {} attempts: {}",
max_attempts, e
))
.into());
}
}
}
}
let run_duration = run_start.elapsed().as_secs_f64() * 1000.0;
if let Some(ref engine) = db {
let status = if final_success { "completed" } else { "failed" };
let successful = if final_success { 1 } else { 0 };
let failed = if final_success { 0 } else { 1 };
let _ = engine.complete_run(&run_id, 1, successful, failed, run_duration, status);
}
Ok(())
}
fn display_response_from_parts(
status_code: u16,
headers: &reqwest::header::HeaderMap,
body: &str,
format: &str,
save_path: Option<&Path>,
) -> Result<()> {
let status_text = reqwest::StatusCode::from_u16(status_code)
.map(|s| s.canonical_reason().unwrap_or(""))
.unwrap_or("");
if (200..300).contains(&status_code) {
println!(
"✅ Status: {} {}",
status_code.to_string().green(),
status_text
);
} else {
println!(
"❌ Status: {} {}",
status_code.to_string().red(),
status_text
);
}
if let Some(path) = save_path {
fs::write(path, body)?;
println!(
"💾 Response saved to: {}",
path.display().to_string().green()
);
}
let content_type = headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("text/plain");
if content_type.contains("json") {
if let Ok(json) = serde_json::from_str::<Value>(body) {
match format {
"json" => println!("{}", serde_json::to_string_pretty(&json)?),
"yaml" => println!("{}", serde_yaml::to_string(&json)?),
"table" => print_as_table(&json),
_ => crate::core::request_runner::print_json_pretty(&json, 0),
}
} else {
println!("{}", body);
}
} else {
println!("{}", body);
}
Ok(())
}
fn get_spec_path(cmd: &RunCommand) -> Result<PathBuf> {
if let Some(spec_path) = &cmd.spec {
if spec_path.exists() {
return Ok(spec_path.canonicalize()?);
} else {
return Err(ApiError::ValidationError(format!(
"Spec file not found: {}\n\nProvide a valid path or omit --spec for auto-detection.",
spec_path.display()
)).into());
}
}
find_api_spec()
}
fn find_api_spec() -> Result<PathBuf> {
let spec_locations = [
"specs/api.yaml",
"specs/api.json",
"specs/openapi.yaml",
"specs/openapi.json",
"specs/swagger.yaml",
"specs/swagger.json",
"api.yaml",
"api.json",
"openapi.yaml",
"openapi.json",
];
for location in &spec_locations {
let path = PathBuf::from(location);
if path.exists() {
return Ok(path.canonicalize()?);
}
}
if let Ok(entries) = fs::read_dir("specs") {
for entry in entries {
let entry = entry?;
let path = entry.path();
if let Some(ext) = path.extension() {
if ext == "yaml" || ext == "yml" || ext == "json" {
return Ok(path.canonicalize()?);
}
}
}
}
Err(ApiError::ValidationError(
"No API specification found. Please run 'mrapids init' or place your spec in specs/api.yaml".to_string()
).into())
}
fn find_template(name: &str) -> Result<PathBuf> {
let template_paths = [
format!("templates/{}.yaml", name),
format!("templates/{}.yml", name),
format!("templates/{}.json", name),
format!(".mrapids/templates/{}.yaml", name),
format!("{}.yaml", name),
format!("{}.yml", name),
];
for path in &template_paths {
let path = PathBuf::from(path);
if path.exists() {
return Ok(path);
}
}
Err(ApiError::ValidationError(format!("Template '{}' not found", name)).into())
}
fn save_to_collection(
request: &Request,
_response_status: u16,
cmd: &RunCommand,
operation_id: &str,
) -> Result<()> {
use crate::collections::models::{Collection, CollectionRequest};
use chrono::Local;
use std::fs::OpenOptions;
use std::io::Write;
let collection_name = if let Some(name) = &cmd.collection {
name.clone()
} else {
format!("daily-{}", Local::now().format("%Y-%m-%d"))
};
let collections_dir = PathBuf::from("collections");
fs::create_dir_all(&collections_dir)?;
let collection_file = collections_dir.join(format!("{}.yaml", collection_name));
if let Some(parent) = collection_file.parent() {
fs::create_dir_all(parent)?;
}
let mut collection = if collection_file.exists() {
let content = fs::read_to_string(&collection_file)?;
match serde_yaml::from_str::<Collection>(&content) {
Ok(col) => col,
Err(e) => {
eprintln!(
"⚠️ Warning: Failed to parse existing collection '{}': {}",
collection_name, e
);
eprintln!(" Creating backup at {}.backup", collection_file.display());
let backup_path = collection_file.with_extension("yaml.backup");
fs::copy(&collection_file, &backup_path)?;
Collection {
name: collection_name.clone(),
description: Some(format!(
"Auto-saved requests from {}",
Local::now().format("%Y-%m-%d")
)),
requests: Vec::new(),
variables: HashMap::new(),
auth_profile: None,
}
}
}
} else {
Collection {
name: collection_name.clone(),
description: Some(format!(
"Auto-saved requests from {}",
Local::now().format("%Y-%m-%d")
)),
requests: Vec::new(),
variables: HashMap::new(),
auth_profile: None,
}
};
let request_name = if let Some(name) = &cmd.save_as_request {
name.clone()
} else {
format!(
"{}_{}",
operation_id.replace('/', "_"),
Local::now().format("%H%M%S")
)
};
let mut collection_request = CollectionRequest {
name: request_name,
operation: operation_id.to_string(),
params: None,
body: None,
save_as: None,
expect: None,
depends_on: None,
if_condition: None,
skip: None,
run_always: false,
critical: false,
retry: None,
};
let mut params = HashMap::new();
for (key, value) in &request.path_params {
params.insert(key.clone(), value.clone());
}
for (key, value) in &request.query_params {
params.insert(key.clone(), json!(value));
}
if !params.is_empty() {
collection_request.params = Some(params);
}
if let Some(body) = &request.body {
collection_request.body = serde_json::from_str(body).ok();
}
collection.requests.push(collection_request);
let yaml = serde_yaml::to_string(&collection)?;
let mut file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&collection_file)?;
file.write_all(yaml.as_bytes())?;
println!(
"💾 Request saved to collection: {}",
collection_name.bright_cyan()
);
Ok(())
}
fn create_no_url_error(cmd: &RunCommand, request: &Request) -> Result<()> {
let mut attempted = vec![];
let mut msg = String::new();
msg.push_str(&format!(
"{}\n\n",
"Error: No API server URL configured".red().bold()
));
msg.push_str("I looked for a base URL in this order and couldn't find one:\n");
attempted.push(("--url flag", cmd.url.clone()));
attempted.push(("--env flag", cmd.env.clone()));
attempted.push((
"config file",
cmd.env.as_ref().map(|e| format!("config/{}.yaml", e)),
));
attempted.push(("$API_BASE_URL", std::env::var("API_BASE_URL").ok()));
attempted.push(("$MRAPIDS_BASE_URL", std::env::var("MRAPIDS_BASE_URL").ok()));
attempted.push((
"OpenAPI spec servers[]",
if request.base_url == "http://localhost" {
Some("localhost only (blocked)".to_string())
} else if request.base_url.is_empty() {
Some("empty".to_string())
} else {
Some(request.base_url.clone())
},
));
for (source, value) in &attempted {
let status = if value.is_some()
&& !value.as_ref().unwrap().is_empty()
&& !value.as_ref().unwrap().contains("localhost")
{
"✓".green()
} else {
"✗".red()
};
let info = value.clone().unwrap_or_else(|| "not provided".to_string());
msg.push_str(&format!(" {} {}: {}\n", status, source, info.dimmed()));
}
msg.push_str(&format!(
"\n{}\n\n",
"Quick fixes (try these in order):".yellow().bold()
));
msg.push_str(&format!("{}\n", "1) Provide URL directly:".bold()));
msg.push_str(&format!(
" mrapids run {} --url https://api.example.com\n\n",
cmd.operation.as_deref().unwrap_or("<operation>")
));
msg.push_str(&format!("{}\n", "2) Set environment variable:".bold()));
#[cfg(target_os = "windows")]
{
msg.push_str(" # Windows CMD:\n");
msg.push_str(" set API_BASE_URL=https://api.example.com\n\n");
msg.push_str(" # Windows PowerShell (session):\n");
msg.push_str(" $env:API_BASE_URL = \"https://api.example.com\"\n\n");
msg.push_str(" # Windows PowerShell (permanent):\n");
msg.push_str(" [System.Environment]::SetEnvironmentVariable(\"API_BASE_URL\", \"https://api.example.com\", \"User\")\n\n");
}
#[cfg(not(target_os = "windows"))]
{
msg.push_str(" # macOS/Linux:\n");
msg.push_str(" export API_BASE_URL=https://api.example.com\n\n");
msg.push_str(" # Add to ~/.bashrc or ~/.zshrc to persist:\n");
msg.push_str(" echo 'export API_BASE_URL=https://api.example.com' >> ~/.bashrc\n\n");
}
msg.push_str(&format!("{}\n", "3) Create default config:".bold()));
let env_name = cmd.env.as_deref().unwrap_or("development");
msg.push_str(&format!(
" echo \"base_url: https://api.example.com\" > config/{}.yaml\n\n",
if env_name == "development" {
"default"
} else {
env_name
}
));
msg.push_str(&format!(
"{}\n",
"4) Re-initialize to extract from spec:".bold()
));
msg.push_str(" mrapids init --from-url https://api.example.com/openapi.json\n\n");
msg.push_str(&format!("{}\n", "5) Run diagnostics:".bold()));
msg.push_str(" mrapids doctor # Check your setup\n");
Err(ApiError::ValidationError(msg).into())
}
fn load_default_example(operation_id: &str) -> Result<String> {
let example_paths = [
format!("data/examples/{}.json", operation_id),
format!("data/examples/{}.json", operation_id.replace('_', "-")),
format!("examples/{}.json", operation_id),
];
for path in &example_paths {
if let Ok(content) = fs::read_to_string(path) {
let cleaned: String = content
.lines()
.filter(|line| !line.trim().starts_with("//"))
.collect::<Vec<_>>()
.join("\n");
return Ok(cleaned);
}
}
Err(ApiError::ValidationError(format!(
"No example data found for operation '{}'",
operation_id
))
.into())
}
fn needs_body(method: &str) -> bool {
matches!(method.to_uppercase().as_str(), "POST" | "PUT" | "PATCH")
}
fn apply_environment(
request: &mut Request,
env: Option<&str>,
_spec_path: Option<&Path>,
) -> Result<()> {
let loader = match ConfigLoader::load(env) {
Ok(loader) => loader,
Err(e) => {
if std::env::var("MRAPIDS_DEBUG").is_ok() {
eprintln!("Debug: Failed to load config: {}", e);
}
return Ok(());
}
};
if std::env::var("MRAPIDS_DEBUG").is_ok() {
eprintln!("Debug: Config loaded for env '{}'", loader.environment());
eprintln!("Debug: Has headers: {}", !loader.headers().is_empty());
eprintln!("Debug: Has auth: {}", loader.auth().is_some());
}
if let Some(base_url) = loader.base_url() {
let old_url = request.base_url.clone();
request.base_url = base_url.to_string();
if std::env::var("MRAPIDS_DEBUG").is_ok() && old_url != request.base_url {
eprintln!(
"Debug: Overriding base_url from '{}' to '{}'",
old_url, request.base_url
);
}
}
for (key, value) in loader.headers() {
request.headers.insert(key.clone(), value.clone());
}
if let Some(auth_config) = loader.auth() {
if let Some(preferred) = &auth_config.preferred {
if let Some(scheme) = auth_config.schemes.get(preferred) {
apply_auth_scheme_to_request(request, scheme);
}
} else if let Some((_, scheme)) = auth_config.schemes.iter().next() {
apply_auth_scheme_to_request(request, scheme);
}
}
request.headers.insert(
"X-MRapids-Timeout".to_string(),
loader.timeout_ms().to_string(),
);
Ok(())
}
fn apply_command_line_overrides(request: &mut Request, cmd: &RunCommand) -> Result<()> {
if let Some(url) = &cmd.url {
request.base_url = url.clone();
if cmd.verbose {
println!(" Overriding base URL with: {}", url);
}
}
if let Some(auth) = &cmd.auth {
request
.headers
.insert("Authorization".to_string(), auth.clone());
if cmd.verbose {
println!(" Using command-line auth");
}
} else if let Some(api_key) = &cmd.api_key {
request
.headers
.insert("X-API-Key".to_string(), api_key.clone());
if cmd.verbose {
println!(" Using command-line API key");
}
} else if let Some(profile) = &cmd.auth_profile {
apply_oauth_profile_auth(request, profile)?;
if cmd.verbose {
println!(" Using OAuth profile: {}", profile);
}
}
for header in &cmd.headers {
if let Some((key, value)) = header.split_once(':') {
request
.headers
.insert(key.trim().to_string(), value.trim().to_string());
}
}
Ok(())
}
fn apply_auth_scheme_to_request(request: &mut Request, scheme: &AuthScheme) {
match scheme {
AuthScheme::ApiKey {
location,
name,
value,
} => match location.as_str() {
"header" => {
request.headers.insert(name.clone(), value.clone());
}
"query" => {
request.query_params.insert(name.clone(), value.clone());
}
_ => {}
},
AuthScheme::Bearer { token, .. } => {
request
.headers
.insert("Authorization".to_string(), format!("Bearer {}", token));
}
AuthScheme::Basic { username, password } => {
use base64::Engine;
let credentials = base64::engine::general_purpose::STANDARD
.encode(format!("{}:{}", username, password));
request.headers.insert(
"Authorization".to_string(),
format!("Basic {}", credentials),
);
}
AuthScheme::OAuth2 { .. } => {
}
}
}
fn apply_oauth_profile_auth(request: &mut Request, profile: &str) -> Result<()> {
use crate::core::auth;
let mut tokens = auth::load_tokens(profile)?;
if tokens.is_expired() {
println!("🔄 Token expired, refreshing...");
tokens = tokio::runtime::Handle::current().block_on(auth::refresh_tokens(profile))?;
}
request
.headers
.insert("Authorization".to_string(), tokens.auth_header());
Ok(())
}
fn substitute_variables(template: &str, vars: &HashMap<String, String>) -> Result<String> {
let mut result = template.to_string();
for (key, value) in vars {
let pattern = format!("${{{}}}", key);
result = result.replace(&pattern, value);
let pattern_with_default = format!("${{{}:", key);
if result.contains(&pattern_with_default) {
let re = regex::Regex::new(&format!(r"\$\{{{}\:([^}}]+)\}}", regex::escape(key)))?;
result = re.replace_all(&result, value).to_string();
}
}
Ok(result)
}
fn find_env_var_with_value(target_value: &str) -> Option<String> {
use std::env;
let common_vars = [
"DEV_API_TOKEN",
"STAGING_API_TOKEN",
"PROD_API_TOKEN",
"API_TOKEN",
"GITHUB_TOKEN",
"GH_TOKEN",
"DEV_BASIC_AUTH",
"STAGING_BASIC_AUTH",
"PROD_BASIC_AUTH",
"BASIC_AUTH",
"DEV_API_KEY",
"STAGING_API_KEY",
"PROD_API_KEY",
"API_KEY",
];
for var_name in &common_vars {
if let Ok(value) = env::var(var_name) {
if value == target_value {
return Some(var_name.to_string());
}
}
}
for (key, value) in env::vars() {
if value == target_value {
return Some(key);
}
}
None
}
fn print_as_curl(request: &Request) -> Result<()> {
use std::env;
let mut curl_cmd = format!("curl -X {}", request.method);
for (key, value) in &request.headers {
if key == "Authorization" {
if value.starts_with("Bearer ") {
let token = value.trim_start_matches("Bearer ");
let env_var_name = find_env_var_with_value(token).unwrap_or_else(|| {
if env::var("DEV_API_TOKEN").is_ok() {
"DEV_API_TOKEN".to_string()
} else if env::var("STAGING_API_TOKEN").is_ok() {
"STAGING_API_TOKEN".to_string()
} else if env::var("PROD_API_TOKEN").is_ok() {
"PROD_API_TOKEN".to_string()
} else if env::var("API_TOKEN").is_ok() {
"API_TOKEN".to_string()
} else {
"API_TOKEN".to_string() }
});
curl_cmd.push_str(&format!(" -H '{}: Bearer ${}'", key, env_var_name));
} else if value.starts_with("Basic ") {
let basic_value = value.trim_start_matches("Basic ");
let env_var_name =
find_env_var_with_value(basic_value).unwrap_or("BASIC_AUTH".to_string());
curl_cmd.push_str(&format!(" -H '{}: Basic ${}'", key, env_var_name));
} else {
curl_cmd.push_str(&format!(" -H '{}: {}'", key, value));
}
} else if key.to_lowercase().contains("api") && key.to_lowercase().contains("key") {
let env_var_name = find_env_var_with_value(value).unwrap_or("API_KEY".to_string());
curl_cmd.push_str(&format!(" -H '{}: ${}'", key, env_var_name));
} else {
curl_cmd.push_str(&format!(" -H '{}: {}'", key, value));
}
}
if let Some(body) = &request.body {
curl_cmd.push_str(&format!(" -d '{}'", body));
}
let mut url_path = request.path.clone();
for (param_name, param_value) in &request.path_params {
let placeholder = format!("{{{}}}", param_name);
let value_str = match param_value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
_ => param_value.to_string(),
};
url_path = url_path.replace(&placeholder, &value_str);
}
let mut url = format!("{}{}", request.base_url.trim_end_matches('/'), url_path);
if !request.query_params.is_empty() {
let query: Vec<String> = request
.query_params
.iter()
.map(|(k, v)| {
if k.to_lowercase().contains("api") && k.to_lowercase().contains("key") {
format!("{}=$API_KEY", k)
} else if k.to_lowercase() == "token" || k.to_lowercase() == "access_token" {
format!("{}=$API_TOKEN", k)
} else {
format!("{}={}", k, v)
}
})
.collect();
url.push_str(&format!("?{}", query.join("&")));
}
curl_cmd.push_str(&format!(" '{}'", url));
println!("\n{} Equivalent curl command:", "🐚".bright_blue());
println!("{}", curl_cmd.bright_cyan());
if curl_cmd.contains("$API_TOKEN")
|| curl_cmd.contains("$API_KEY")
|| curl_cmd.contains("$BASIC_AUTH")
{
println!(
"\n{} Set environment variables before running:",
"💡".bright_yellow()
);
if curl_cmd.contains("$API_TOKEN") {
println!(" export API_TOKEN=\"your-token-here\"");
}
if curl_cmd.contains("$API_KEY") {
println!(" export API_KEY=\"your-api-key-here\"");
}
if curl_cmd.contains("$BASIC_AUTH") {
println!(" export BASIC_AUTH=\"$(echo -n 'username:password' | base64)\"");
}
}
Ok(())
}
fn print_as_table(json: &Value) {
if let Some(array) = json.as_array() {
if !array.is_empty() {
if let Some(first) = array.first().and_then(|v| v.as_object()) {
let headers: Vec<&str> = first.keys().map(|s| s.as_str()).collect();
println!("{}", headers.join("\t").bright_blue());
for item in array {
if let Some(obj) = item.as_object() {
let values: Vec<String> = headers
.iter()
.map(|h| obj.get(*h).map(|v| format!("{}", v)).unwrap_or_default())
.collect();
println!("{}", values.join("\t"));
}
}
}
}
} else {
println!("{}", serde_json::to_string_pretty(json).unwrap());
}
}
fn generate_required_only_body(
operation: &crate::core::parser::UnifiedOperation,
) -> Result<Option<String>> {
if let Some(request_body) = &operation.request_body {
if let Some((_, media_type)) = request_body.content.iter().next() {
let schema = &media_type.schema;
if let crate::core::parser::SchemaType::Object = schema.schema_type {
let mut obj = serde_json::Map::new();
if let Some(properties) = &schema.properties {
if let Some(required) = &schema.required {
for field_name in required {
if let Some(field_schema) = properties.get(field_name) {
let value = generate_smart_example(field_name, field_schema);
obj.insert(field_name.clone(), value);
}
}
}
if obj.is_empty() {
obj.insert(
"_note".to_string(),
json!("No required fields - add your data here"),
);
}
}
let json_value = json!(obj);
println!("📝 Generated minimal payload with required fields only:");
println!(
"{}",
serde_json::to_string_pretty(&json_value)?.bright_black()
);
return Ok(Some(serde_json::to_string(&json_value)?));
}
}
}
Ok(None)
}
fn smart_decode_param(value: &str) -> String {
if value.contains('%') && looks_like_url_encoded(value) {
match urlencoding::decode(value) {
Ok(decoded) => {
decoded.to_string()
}
Err(_) => {
value.to_string()
}
}
} else {
value.to_string()
}
}
fn looks_like_url_encoded(s: &str) -> bool {
let encoded_pattern = regex::Regex::new(r"%[0-9A-Fa-f]{2}").unwrap();
encoded_pattern.is_match(s)
}
fn load_query_from_file(cmd: &RunCommand, query_file: &PathBuf) -> Result<()> {
if !query_file.exists() {
return Err(ApiError::ValidationError(format!(
"Query file not found: {}",
query_file.display()
))
.into());
}
let content = fs::read_to_string(query_file)?;
let mut new_cmd = cmd.clone();
new_cmd.params = Vec::new();
if content.trim().starts_with('{') {
let params: HashMap<String, String> = serde_json::from_str(&content)?;
for (key, value) in params {
new_cmd.params.push(format!("{}={}", key, value));
}
} else {
for line in content.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with('#') {
new_cmd.params.push(line.to_string());
}
}
}
println!("{}", "📂 Loading query from file:".bright_cyan());
println!(" File: {}", query_file.display());
println!(" Parameters loaded: {}", new_cmd.params.len());
let mut operation_cmd = new_cmd;
operation_cmd.query_file = None; execute(operation_cmd)
}
fn replay_last_query(_cmd: &RunCommand) -> Result<()> {
println!(
"{}",
"❌ Replay feature not available in simplified version".red()
);
println!("💡 Use shell history (up arrow) to replay previous commands");
Ok(())
}
fn generate_template_interactive(cmd: &RunCommand) -> Result<()> {
let op_name = cmd
.operation
.as_ref()
.ok_or_else(|| ApiError::ValidationError("Operation is required".to_string()))?;
println!(
"⚡ {} v{}",
"Micro Rapid".bright_cyan(),
env!("CARGO_PKG_VERSION")
);
let spec_path = get_spec_path(cmd)?;
let spec_content = fs::read_to_string(&spec_path)?;
let spec = crate::core::parser::parse_spec(&spec_content)?;
let operation = crate::core::show::find_operation_with_spec(&spec, op_name)?;
println!(
"📋 Operation: {} ({} {})",
operation.operation_id.bright_cyan(),
operation.method.to_uppercase().bright_green(),
operation.path
);
if let Some(security_reqs) = &operation.security {
if !security_reqs.is_empty() {
let auth_desc = if let Some(req) = security_reqs.first() {
match spec.security_schemes.get(&req.scheme_name) {
Some(scheme) => match scheme.scheme_type.as_str() {
"apiKey" => {
format!("API Key ({})", scheme.name.as_deref().unwrap_or("api_key"))
}
"http" => match scheme.scheme.as_deref() {
Some("bearer") => "Bearer".to_string(),
Some("basic") => "Basic".to_string(),
_ => "HTTP".to_string(),
},
"oauth2" => "OAuth2".to_string(),
_ => req.scheme_name.clone(),
},
None => req.scheme_name.clone(),
}
} else {
"Required".to_string()
};
println!("🔐 Auth: {} (from environment)", auth_desc.bright_yellow());
}
}
let needs_body = needs_body(&operation.method);
if needs_body {
let template = generate_operation_template(operation, &spec, cmd.minimal)?;
if let Some(request_body) = &operation.request_body {
if let Some(media_type) = request_body.content.values().next() {
let schema = &media_type.schema;
let required_fields = schema
.required
.as_ref()
.map(|r| r.clone())
.unwrap_or_default();
let all_fields: Vec<String> = if let serde_json::Value::Object(obj) = &template {
obj.keys().cloned().collect()
} else {
vec![]
};
let optional_fields: Vec<String> = all_fields
.iter()
.filter(|f| !required_fields.contains(f))
.cloned()
.collect();
println!();
if !required_fields.is_empty() {
println!(
"Required fields: {}",
required_fields.join(", ").bright_yellow()
);
}
if !optional_fields.is_empty() && !cmd.minimal {
println!("Optional fields: {}", optional_fields.join(", ").dimmed());
}
}
}
println!("\n📄 Generated template with realistic examples:");
let pretty_json = serde_json::to_string_pretty(&template)?;
println!("{}", pretty_json.bright_white());
let filename = generate_template_filename(&operation.operation_id, cmd.save_as.as_deref());
fs::write(&filename, &pretty_json)?;
println!(
"\n💾 Template saved to: {}",
filename.display().to_string().bright_green()
);
println!("\n👉 Next steps:");
let editor = std::env::var("EDITOR").unwrap_or_else(|_| "vi".to_string());
println!(
" 1. Edit: {} {}",
editor.bright_cyan(),
filename.display()
);
let run_cmd = format!("mrapids run {} --data @{}", op_name, filename.display());
println!(" 2. Run: {}", run_cmd.bright_cyan());
} else {
println!();
let path_params: Vec<_> = operation
.parameters
.iter()
.filter(|p| p.location == crate::core::parser::ParameterLocation::Path)
.collect();
if !path_params.is_empty() {
println!("{}", "PATH PARAMETERS (required):".bright_yellow());
for param in &path_params {
let example = generate_smart_example(¶m.name, ¶m.schema);
println!(
" • {}: {}",
param.name.bright_white(),
serde_json::to_string(&example).unwrap_or_else(|_| "example".to_string())
);
}
println!();
}
let query_params: Vec<_> = operation
.parameters
.iter()
.filter(|p| p.location == crate::core::parser::ParameterLocation::Query)
.cloned()
.collect();
if !query_params.is_empty() {
println!("{}", "QUERY PARAMETERS:".bright_yellow());
for param in &query_params {
let required = if param.required { " (required)" } else { "" };
println!(" --param {}=<value>{}", param.name, required.red());
}
println!();
}
println!("{}", "USAGE EXAMPLES:".bright_green());
let mut example_cmd = format!("mrapids run {}", op_name);
for param in &path_params {
example_cmd.push_str(&format!(" --param {}=<value>", param.name));
}
for param in query_params.iter().filter(|p| p.required) {
example_cmd.push_str(&format!(" --param {}=<value>", param.name));
}
println!("\n {}", example_cmd.bright_cyan());
if !path_params.is_empty() || !query_params.is_empty() {
println!("\n{}", "CONCRETE EXAMPLE:".bright_green());
let mut concrete_cmd = format!("mrapids run {}", op_name);
for param in &path_params {
let example = generate_parameter_example(¶m);
let value_str = match example {
Value::String(s) => s,
Value::Number(n) => n.to_string(),
_ => "value".to_string(),
};
concrete_cmd.push_str(&format!(" --param {}={}", param.name, value_str));
}
for param in query_params.iter().take(3) {
let example = generate_parameter_example(param);
let value_str = match example {
Value::String(s) => format!("'{}'", s),
Value::Number(n) => n.to_string(),
_ => "'value'".to_string(),
};
concrete_cmd.push_str(&format!(" --param {}={}", param.name, value_str));
}
println!(" {}", concrete_cmd.bright_cyan());
}
println!(
"\n💡 Note: {} requests use parameters, not request bodies.",
operation.method.to_uppercase().bright_yellow()
);
println!(" Use --param key=value for parameters");
println!(" Use --query key=value to force query parameters");
}
Ok(())
}
fn generate_operation_template(
operation: &crate::core::parser::UnifiedOperation,
_spec: &crate::core::parser::UnifiedSpec,
minimal: bool,
) -> Result<Value> {
if let Some(request_body) = &operation.request_body {
if let Some(media_type) = request_body.content.values().next() {
generate_template_from_schema(&media_type.schema, "body", minimal)
} else {
Ok(json!({}))
}
} else {
Ok(json!({}))
}
}
fn generate_template_from_schema(
schema: &crate::core::parser::UnifiedSchema,
field_name: &str,
minimal: bool,
) -> Result<Value> {
use crate::core::parser::SchemaType;
match &schema.schema_type {
SchemaType::Object => {
let mut obj = serde_json::Map::new();
if let Some(properties) = &schema.properties {
for (prop_name, prop_schema) in properties {
let required_fields = schema
.required
.as_ref()
.map(|r| r.clone())
.unwrap_or_default();
if minimal && !required_fields.contains(prop_name) {
continue;
}
let value = generate_template_from_schema(prop_schema, prop_name, minimal)?;
obj.insert(prop_name.clone(), value);
}
}
Ok(Value::Object(obj))
}
SchemaType::Array => {
let item = if let Some(items) = &schema.items {
generate_template_from_schema(items, field_name, minimal)?
} else {
json!("example")
};
Ok(json!([item]))
}
_ => {
Ok(generate_smart_example(field_name, schema))
}
}
}
fn generate_template_filename(operation_id: &str, save_as: Option<&Path>) -> PathBuf {
if let Some(custom) = save_as {
return custom.to_path_buf();
}
let base = to_kebab_case(operation_id);
let mut filename = PathBuf::from(format!("{}.json", base));
let mut counter = 2;
while filename.exists() {
filename = PathBuf::from(format!("{}-{}.json", base, counter));
counter += 1;
}
filename
}
fn to_kebab_case(s: &str) -> String {
let mut result = String::new();
let mut prev_upper = false;
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() {
if i > 0 && !prev_upper {
result.push('-');
}
result.push(ch.to_lowercase().next().unwrap());
prev_upper = true;
} else {
result.push(ch);
prev_upper = false;
}
}
result
}
fn load_saved_query(name: &str) -> Result<()> {
use crate::core::saved_queries;
println!("📂 Loading saved query: {}", name.bright_cyan());
let query = saved_queries::load_query(name)?;
saved_queries::display_query_details(name)?;
println!();
println!("{}", "Execute this query? (y/n): ".bright_yellow());
let mut input = String::new();
std::io::stdin().read_line(&mut input)?;
if input.trim().to_lowercase() == "y" || input.trim().to_lowercase() == "yes" {
println!();
println!("{}", "Executing...".bright_green());
println!();
let new_cmd = RunCommand {
operation: Some(query.operation.clone()),
params: query
.params
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect(),
query_params: Vec::new(),
headers: query
.headers
.iter()
.map(|(k, v)| format!("{}: {}", k, v))
.collect(),
data: query.body.clone(),
file: None,
id: None,
name: None,
status: None,
limit: None,
offset: None,
sort: None,
auth: None,
api_key: None,
auth_profile: None,
env: query.env.clone(),
url: None,
output: "pretty".to_string(),
save: None,
template: None,
template_vars: Vec::new(),
required_only: false,
verbose: false,
dry_run: false,
as_curl: false,
edit: false,
stdin: false,
retry: 0,
timeout: 30,
allow_insecure: false,
allow_localhost: false,
no_warnings: false,
redact: false,
interactive: false,
save_as: None,
minimal: false,
help_query: false,
build_query: false,
query_file: None,
replay_last: false,
save_query: None,
load_query: None,
list_queries: false,
save_to_collection: false,
collection: None,
save_as_request: None,
json_output: false,
log_decisions: false,
spec: None,
};
execute_direct_operation(&new_cmd)
} else {
println!("{}", "Cancelled.".dimmed());
Ok(())
}
}
pub fn save_current_query(cmd: &RunCommand, name: &str) -> Result<()> {
use crate::core::saved_queries::{save_query, SavedQuery};
let op_name = cmd.operation.as_ref().ok_or_else(|| {
ApiError::ValidationError("Operation is required to save a query".to_string())
})?;
let query = SavedQuery::from_run_params(
name,
op_name,
&cmd.params,
&cmd.headers,
cmd.data.as_deref(),
cmd.env.as_deref(),
);
let path = save_query(&query)?;
println!();
println!(
"{} Saved query '{}' to {}",
"✓".bright_green(),
name.bright_cyan(),
path.display().to_string().dimmed()
);
println!();
println!(
"{} mrapids run --load-query {}",
"Run with:".bright_blue(),
name
);
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::AuthScheme;
#[test]
fn test_needs_body_post() {
assert!(needs_body("POST"));
assert!(needs_body("post"));
assert!(needs_body("Post"));
}
#[test]
fn test_needs_body_put() {
assert!(needs_body("PUT"));
assert!(needs_body("put"));
}
#[test]
fn test_needs_body_patch() {
assert!(needs_body("PATCH"));
assert!(needs_body("patch"));
}
#[test]
fn test_needs_body_get_delete() {
assert!(!needs_body("GET"));
assert!(!needs_body("DELETE"));
assert!(!needs_body("HEAD"));
assert!(!needs_body("OPTIONS"));
}
#[test]
fn test_to_kebab_case_camel() {
assert_eq!(to_kebab_case("getUserById"), "get-user-by-id");
assert_eq!(to_kebab_case("createOrder"), "create-order");
}
#[test]
fn test_to_kebab_case_pascal() {
assert_eq!(to_kebab_case("GetUser"), "get-user");
assert_eq!(to_kebab_case("CreateNewCustomer"), "create-new-customer");
}
#[test]
fn test_to_kebab_case_already_lowercase() {
assert_eq!(to_kebab_case("getuser"), "getuser");
assert_eq!(to_kebab_case("simple"), "simple");
}
#[test]
fn test_to_kebab_case_with_numbers() {
assert_eq!(to_kebab_case("getUser123"), "get-user123");
}
#[test]
fn test_looks_like_url_encoded_true() {
assert!(looks_like_url_encoded("hello%20world"));
assert!(looks_like_url_encoded("name%3Dvalue"));
assert!(looks_like_url_encoded("%2F%2Fpath"));
}
#[test]
fn test_looks_like_url_encoded_false() {
assert!(!looks_like_url_encoded("hello world"));
assert!(!looks_like_url_encoded("simple"));
assert!(!looks_like_url_encoded("100%")); assert!(!looks_like_url_encoded("%ZZ")); }
#[test]
fn test_smart_decode_param_encoded() {
assert_eq!(smart_decode_param("hello%20world"), "hello world");
assert_eq!(smart_decode_param("name%3Dvalue"), "name=value");
}
#[test]
fn test_smart_decode_param_not_encoded() {
assert_eq!(smart_decode_param("hello world"), "hello world");
assert_eq!(smart_decode_param("simple"), "simple");
}
#[test]
fn test_smart_decode_param_partial() {
assert_eq!(smart_decode_param("100%"), "100%");
}
#[test]
fn test_substitute_variables_simple() {
let mut vars = HashMap::new();
vars.insert("NAME".to_string(), "John".to_string());
vars.insert("AGE".to_string(), "30".to_string());
let result = substitute_variables("Hello ${NAME}, you are ${AGE}", &vars).unwrap();
assert_eq!(result, "Hello John, you are 30");
}
#[test]
fn test_substitute_variables_missing() {
let vars = HashMap::new();
let result = substitute_variables("Hello ${NAME}", &vars).unwrap();
assert_eq!(result, "Hello ${NAME}");
}
#[test]
fn test_substitute_variables_with_default() {
let mut vars = HashMap::new();
vars.insert("NAME".to_string(), "John".to_string());
let result = substitute_variables("Hello ${NAME:default}", &vars).unwrap();
assert_eq!(result, "Hello John");
}
#[test]
fn test_substitute_variables_empty() {
let vars = HashMap::new();
let result = substitute_variables("No variables here", &vars).unwrap();
assert_eq!(result, "No variables here");
}
fn create_test_request() -> Request {
Request {
method: "GET".to_string(),
path: "/users/{id}".to_string(),
base_url: "https://api.example.com".to_string(),
headers: HashMap::new(),
query_params: HashMap::new(),
path_params: HashMap::new(),
body: None,
spec_content_type: None,
}
}
#[test]
fn test_apply_auth_bearer_token() {
let mut request = create_test_request();
let scheme = AuthScheme::Bearer {
token: "my-secret-token".to_string(),
format: None,
};
apply_auth_scheme_to_request(&mut request, &scheme);
assert_eq!(
request.headers.get("Authorization"),
Some(&"Bearer my-secret-token".to_string())
);
}
#[test]
fn test_apply_auth_api_key_header() {
let mut request = create_test_request();
let scheme = AuthScheme::ApiKey {
location: "header".to_string(),
name: "X-API-Key".to_string(),
value: "api-key-123".to_string(),
};
apply_auth_scheme_to_request(&mut request, &scheme);
assert_eq!(
request.headers.get("X-API-Key"),
Some(&"api-key-123".to_string())
);
}
#[test]
fn test_apply_auth_api_key_query() {
let mut request = create_test_request();
let scheme = AuthScheme::ApiKey {
location: "query".to_string(),
name: "api_key".to_string(),
value: "query-key-456".to_string(),
};
apply_auth_scheme_to_request(&mut request, &scheme);
assert_eq!(
request.query_params.get("api_key"),
Some(&"query-key-456".to_string())
);
}
#[test]
fn test_apply_auth_basic() {
let mut request = create_test_request();
let scheme = AuthScheme::Basic {
username: "user".to_string(),
password: "pass".to_string(),
};
apply_auth_scheme_to_request(&mut request, &scheme);
let auth_header = request.headers.get("Authorization").unwrap();
assert!(auth_header.starts_with("Basic "));
use base64::Engine;
let encoded = auth_header.trim_start_matches("Basic ");
let decoded = base64::engine::general_purpose::STANDARD
.decode(encoded)
.unwrap();
let credentials = String::from_utf8(decoded).unwrap();
assert_eq!(credentials, "user:pass");
}
#[test]
fn test_find_env_var_with_value_found() {
std::env::set_var("TEST_MRAPIDS_TOKEN", "test-token-12345");
let result = find_env_var_with_value("test-token-12345");
assert_eq!(result, Some("TEST_MRAPIDS_TOKEN".to_string()));
std::env::remove_var("TEST_MRAPIDS_TOKEN");
}
#[test]
fn test_find_env_var_with_value_not_found() {
let result = find_env_var_with_value("this-value-does-not-exist-anywhere");
assert_eq!(result, None);
}
#[test]
fn test_request_headers_can_be_overridden() {
let mut request = create_test_request();
request
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
request
.headers
.insert("Accept".to_string(), "application/json".to_string());
request
.headers
.insert("Content-Type".to_string(), "application/xml".to_string());
assert_eq!(
request.headers.get("Content-Type"),
Some(&"application/xml".to_string())
);
}
#[test]
fn test_request_query_params() {
let mut request = create_test_request();
request
.query_params
.insert("limit".to_string(), "10".to_string());
request
.query_params
.insert("offset".to_string(), "0".to_string());
assert_eq!(request.query_params.len(), 2);
assert_eq!(request.query_params.get("limit"), Some(&"10".to_string()));
}
#[test]
fn test_request_with_body() {
let mut request = create_test_request();
request.method = "POST".to_string();
request.body = Some(r#"{"name": "test"}"#.to_string());
assert!(request.body.is_some());
assert!(needs_body(&request.method));
}
#[test]
fn test_content_type_from_spec() {
let mut request = create_test_request();
request.method = "POST".to_string();
request.body = Some(r#"{"name": "test"}"#.to_string());
request.spec_content_type = Some("application/json".to_string());
if request.body.is_some() {
if let Some(spec_ct) = &request.spec_content_type {
request
.headers
.insert("Content-Type".to_string(), spec_ct.clone());
}
}
assert_eq!(
request.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
}
#[test]
fn test_content_type_fallback_to_json() {
let mut request = create_test_request();
request.method = "POST".to_string();
request.body = Some(r#"{"name": "test"}"#.to_string());
request.spec_content_type = None;
if request.body.is_some() {
if let Some(spec_ct) = &request.spec_content_type {
request
.headers
.insert("Content-Type".to_string(), spec_ct.clone());
} else if !request.headers.contains_key("Content-Type") {
request
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
}
}
assert_eq!(
request.headers.get("Content-Type"),
Some(&"application/json".to_string())
);
}
#[test]
fn test_content_type_not_set_for_get() {
let mut request = create_test_request();
request.method = "GET".to_string();
request.body = None;
request.spec_content_type = None;
if request.body.is_some() {
if let Some(spec_ct) = &request.spec_content_type {
request
.headers
.insert("Content-Type".to_string(), spec_ct.clone());
} else if !request.headers.contains_key("Content-Type") {
request
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
}
}
assert_eq!(request.headers.get("Content-Type"), None);
}
#[test]
fn test_content_type_preserves_existing() {
let mut request = create_test_request();
request.method = "POST".to_string();
request.body = Some(r#"<xml>test</xml>"#.to_string());
request.spec_content_type = None;
request
.headers
.insert("Content-Type".to_string(), "application/xml".to_string());
if request.body.is_some() {
if let Some(spec_ct) = &request.spec_content_type {
request
.headers
.insert("Content-Type".to_string(), spec_ct.clone());
} else if !request.headers.contains_key("Content-Type") {
request
.headers
.insert("Content-Type".to_string(), "application/json".to_string());
}
}
assert_eq!(
request.headers.get("Content-Type"),
Some(&"application/xml".to_string())
);
}
}