#![allow(dead_code)]
use crate::core::api::ApiError;
use crate::core::mcp_types::{
AuthDetail, BodyPropertyDetail, ParameterDetail, ParameterHint, QueryBuilderOutput,
RequestBodyDetail, RiskProfile,
};
use crate::core::parser::{ParameterLocation, UnifiedOperation, UnifiedSpec};
use anyhow::Result;
use colored::*;
pub fn get_query_builder_output(
spec: &UnifiedSpec,
operation_input: &str,
) -> Result<QueryBuilderOutput> {
let operation = find_operation(spec, operation_input)?;
build_query_builder_output(spec, operation)
}
fn build_query_builder_output(
spec: &UnifiedSpec,
operation: &UnifiedOperation,
) -> Result<QueryBuilderOutput> {
let base_url = if spec.base_url.is_empty() || spec.base_url == "http://localhost" {
"$API_BASE_URL".to_string()
} else {
spec.base_url.clone()
};
let url = format!("{}{}", base_url, operation.path);
let path_parameters: Vec<ParameterDetail> = operation
.parameters
.iter()
.filter(|p| p.location == ParameterLocation::Path)
.map(|p| param_to_detail(p))
.collect();
let query_parameters: Vec<ParameterDetail> = operation
.parameters
.iter()
.filter(|p| p.location == ParameterLocation::Query)
.map(|p| param_to_detail(p))
.collect();
let header_parameters: Vec<ParameterDetail> = operation
.parameters
.iter()
.filter(|p| p.location == ParameterLocation::Header && !is_auth_header(&p.name))
.map(|p| param_to_detail(p))
.collect();
let request_body = operation.request_body.as_ref().map(|rb| {
let (content_type, schema) = rb
.content
.iter()
.next()
.map(|(ct, mt)| (ct.clone(), &mt.schema))
.unwrap_or((
"application/json".to_string(),
&rb.content.values().next().unwrap().schema,
));
let required_fields = schema.required.clone();
let properties: Vec<BodyPropertyDetail> = schema
.properties
.as_ref()
.map(|props| {
props
.iter()
.map(|(name, prop_schema)| {
let is_required = required_fields
.as_ref()
.map(|r| r.contains(name))
.unwrap_or(false);
BodyPropertyDetail {
name: name.clone(),
prop_type: prop_schema.schema_type.to_string(),
required: is_required,
description: prop_schema.description.clone(),
example: prop_schema.example.clone(),
enum_values: prop_schema.enum_values.as_ref().map(|vals| {
vals.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
}
})
.collect()
})
.unwrap_or_default();
let json_template = build_json_template(schema);
RequestBodyDetail {
required: rb.required,
content_type,
required_fields,
properties,
json_template,
}
});
let has_auth = if let Some(security) = &operation.security {
!security.is_empty()
} else {
!spec.security_schemes.is_empty() || infer_auth_from_url(&spec.base_url)
};
let (auth_type, auth_flag, setup_hint) = if has_auth {
let (at, hint) = if let Some(security) = &operation.security {
get_auth_type_with_hint(spec, security)
} else if !spec.security_schemes.is_empty() {
get_default_auth_hint(spec)
} else {
infer_auth_type_from_url(&spec.base_url)
};
let flag = if at.contains("API Key") {
Some("--api-key \"$API_KEY\"".to_string())
} else if at.contains("Bearer") {
Some("--auth \"Bearer $TOKEN\"".to_string())
} else if at.contains("Basic") {
Some("--auth \"Basic $CREDENTIALS\"".to_string())
} else if at.contains("OAuth") {
Some("--profile \"$PROFILE\"".to_string())
} else {
Some("--auth \"$AUTH\"".to_string())
};
(Some(at), flag, Some(hint))
} else {
(None, None, None)
};
let auth = AuthDetail {
required: has_auth,
auth_type,
auth_flag: auth_flag.clone(),
setup_hint,
};
let mut cmd_parts = vec![format!("mrapids run {}", operation.operation_id)];
if let Some(ref flag) = auth_flag {
cmd_parts.push(flag.clone());
}
for param in &path_parameters {
let example = param
.example
.as_ref()
.map(|e| e.to_string().trim_matches('"').to_string())
.unwrap_or_else(|| "123".to_string());
cmd_parts.push(format!("--param {}={}", param.name, example));
}
for param in query_parameters.iter().filter(|p| p.required).take(3) {
let example = param
.example
.as_ref()
.map(|e| e.to_string().trim_matches('"').to_string())
.unwrap_or_else(|| "<value>".to_string());
cmd_parts.push(format!("--param {}={}", param.name, example));
}
if let Some(ref rb) = request_body {
if rb.json_template.len() < 80 {
cmd_parts.push(format!("--data '{}'", rb.json_template));
} else {
cmd_parts.push("--data @body.json".to_string());
}
}
let copy_run_command = if cmd_parts.len() == 1 {
cmd_parts[0].clone()
} else {
cmd_parts.join(" \\\n ")
};
let risk = RiskProfile::from_method(&operation.method);
let tips = vec![
"--dry-run to preview".to_string(),
"-v for verbose".to_string(),
"--as-curl for curl command".to_string(),
];
Ok(QueryBuilderOutput {
operation_id: operation.operation_id.clone(),
method: operation.method.to_uppercase(),
path: operation.path.clone(),
summary: operation.summary.clone(),
url,
path_parameters,
query_parameters,
header_parameters,
request_body,
auth,
risk,
copy_run_command,
tips,
parameter_hints: build_parameter_hints(&operation.parameters, &operation.request_body),
missing_required: find_missing_required(&operation.parameters, &operation.request_body),
clarifying_question_candidates: build_clarifying_questions(
&operation.parameters,
&operation.request_body,
),
})
}
fn build_parameter_hints(
parameters: &[crate::core::parser::UnifiedParameter],
request_body: &Option<crate::core::parser::UnifiedRequestBody>,
) -> std::collections::HashMap<String, ParameterHint> {
let mut hints = std::collections::HashMap::new();
for param in parameters {
let mut examples = Vec::new();
if let Some(ex) = ¶m.example {
examples.push(ex.clone());
}
if let Some(ex) = ¶m.schema.example {
if !examples.contains(ex) {
examples.push(ex.clone());
}
}
let mut constraints = serde_json::Map::new();
if param.schema.enum_values.is_some() {
constraints.insert("enum".to_string(), serde_json::json!(true));
}
if let Some(min) = param.schema.minimum {
constraints.insert("minimum".to_string(), serde_json::json!(min));
}
if let Some(max) = param.schema.maximum {
constraints.insert("maximum".to_string(), serde_json::json!(max));
}
let location = match param.location {
ParameterLocation::Path => "path",
ParameterLocation::Query => "query",
ParameterLocation::Header => "header",
ParameterLocation::Cookie => "cookie",
};
hints.insert(
param.name.clone(),
ParameterHint {
possible_values: param.schema.enum_values.clone(),
hint_type: param.schema.schema_type.to_string(),
format: param.schema.format.clone(),
required: param.required,
examples,
constraints: serde_json::Value::Object({
constraints.insert("location".to_string(), serde_json::json!(location));
constraints
}),
},
);
}
if let Some(rb) = request_body {
for (_content_type, media_type) in &rb.content {
let required_fields: Vec<String> =
media_type.schema.required.clone().unwrap_or_default();
if let Some(props) = &media_type.schema.properties {
for (name, schema) in props {
if hints.contains_key(name) {
continue; }
let mut examples = Vec::new();
if let Some(ex) = &schema.example {
examples.push(ex.clone());
}
let mut constraints = serde_json::Map::new();
if schema.enum_values.is_some() {
constraints.insert("enum".to_string(), serde_json::json!(true));
}
if let Some(min) = schema.minimum {
constraints.insert("minimum".to_string(), serde_json::json!(min));
}
if let Some(max) = schema.maximum {
constraints.insert("maximum".to_string(), serde_json::json!(max));
}
constraints.insert("location".to_string(), serde_json::json!("body"));
hints.insert(
name.clone(),
ParameterHint {
possible_values: schema.enum_values.clone(),
hint_type: schema.schema_type.to_string(),
format: schema.format.clone(),
required: required_fields.contains(name),
examples,
constraints: serde_json::Value::Object(constraints),
},
);
}
}
}
}
hints
}
fn find_missing_required(
parameters: &[crate::core::parser::UnifiedParameter],
request_body: &Option<crate::core::parser::UnifiedRequestBody>,
) -> Vec<String> {
let mut missing = Vec::new();
for param in parameters {
if param.required
&& param.example.is_none()
&& param.schema.example.is_none()
&& param.schema.default.is_none()
{
missing.push(param.name.clone());
}
}
if let Some(rb) = request_body {
for (_ct, media_type) in &rb.content {
let required_fields: Vec<String> =
media_type.schema.required.clone().unwrap_or_default();
if let Some(props) = &media_type.schema.properties {
for name in &required_fields {
if let Some(schema) = props.get(name) {
if schema.example.is_none() && schema.default.is_none() {
missing.push(format!("body.{}", name));
}
}
}
}
}
}
missing
}
fn build_clarifying_questions(
parameters: &[crate::core::parser::UnifiedParameter],
request_body: &Option<crate::core::parser::UnifiedRequestBody>,
) -> Vec<String> {
let mut questions = Vec::new();
for param in parameters {
if param.required
&& param.example.is_none()
&& param.schema.example.is_none()
&& param.schema.default.is_none()
{
let location = match param.location {
ParameterLocation::Path => "path",
ParameterLocation::Query => "query",
ParameterLocation::Header => "header",
ParameterLocation::Cookie => "cookie",
};
let desc = param.description.as_deref().unwrap_or("");
let suffix = if desc.is_empty() {
String::new()
} else {
format!(". {}", desc)
};
questions.push(format!(
"What {}? (required, {}, {} parameter{})",
param.name, param.schema.schema_type, location, suffix,
));
}
}
if let Some(rb) = request_body {
for (_ct, media_type) in &rb.content {
let required_fields: Vec<String> =
media_type.schema.required.clone().unwrap_or_default();
if let Some(props) = &media_type.schema.properties {
for name in &required_fields {
if let Some(schema) = props.get(name) {
if schema.example.is_none() && schema.default.is_none() {
let desc = schema.description.as_deref().unwrap_or("");
let suffix = if desc.is_empty() {
String::new()
} else {
format!(". {}", desc)
};
questions.push(format!(
"What {}? (required, {}, body property{})",
name, schema.schema_type, suffix,
));
}
}
}
}
}
}
questions
}
fn param_to_detail(param: &crate::core::parser::UnifiedParameter) -> ParameterDetail {
ParameterDetail {
name: param.name.clone(),
param_type: param.schema.schema_type.to_string(),
required: param.required,
description: param.description.clone(),
example: param.schema.example.clone().or_else(|| {
let name_lower = param.name.to_lowercase();
if name_lower.contains("id") {
Some(serde_json::json!(123))
} else if name_lower.contains("limit") {
Some(serde_json::json!(20))
} else if name_lower.contains("page") {
Some(serde_json::json!(1))
} else {
None
}
}),
enum_values: param.schema.enum_values.as_ref().map(|vals| {
vals.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
}),
default: param.schema.default.clone(),
format: param.schema.format.clone(),
}
}
pub fn run_query_builder(spec: &UnifiedSpec, operation_input: &str) -> Result<()> {
let operation = find_operation(spec, operation_input)?;
display_query_builder_output(spec, operation)?;
Ok(())
}
fn find_operation<'a>(spec: &'a UnifiedSpec, input: &str) -> Result<&'a UnifiedOperation> {
if let Some(op) = spec.operations.iter().find(|op| op.operation_id == input) {
return Ok(op);
}
let input_lower = input.to_lowercase();
if let Some(op) = spec
.operations
.iter()
.find(|op| op.operation_id.to_lowercase() == input_lower)
{
return Ok(op);
}
if let Some(op) = spec.operations.iter().find(|op| {
op.operation_id.to_lowercase().contains(&input_lower)
|| input_lower.contains(&op.operation_id.to_lowercase())
}) {
return Ok(op);
}
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() == 2 {
let method = parts[0].to_uppercase();
let path = parts[1];
if let Some(op) = spec.operations.iter().find(|op| {
op.method.to_uppercase() == method
&& (op.path == path || op.path.trim_end_matches('/') == path.trim_end_matches('/'))
}) {
return Ok(op);
}
}
Err(ApiError::OperationNotFound(format!(
"Operation '{}' not found in spec.\n\nTry:\n mrapids run -Q (to see all operations)\n mrapids list operations",
input
)).into())
}
fn display_query_builder_output(spec: &UnifiedSpec, operation: &UnifiedOperation) -> Result<()> {
const BOX_WIDTH: usize = 72;
println!();
println!(
"{}",
format!("┌{}┐", "─".repeat(BOX_WIDTH - 2)).bright_blue()
);
let method_path = format!("{} {}", operation.method.to_uppercase(), operation.path);
let op_line = format!(" {} {}", operation.operation_id, method_path);
let padding = BOX_WIDTH - 2 - visible_len(&op_line);
println!(
"{}{}{}{}",
"│".bright_blue(),
format!(" {}", operation.operation_id).bright_cyan(),
format!(" {}", method_path).bright_green(),
format!(
"{}{}",
" ".repeat(padding.saturating_sub(method_path.len() + 2)),
"│"
)
.bright_blue()
);
if let Some(summary) = &operation.summary {
let summary_display = truncate_str(summary, BOX_WIDTH - 6);
let padding = BOX_WIDTH - 4 - summary_display.len();
println!(
"{} {}{}{}",
"│".bright_blue(),
summary_display.dimmed(),
" ".repeat(padding),
"│".bright_blue()
);
}
println!(
"{}{}{}",
"│".bright_blue(),
" ".repeat(BOX_WIDTH - 2),
"│".bright_blue()
);
let base_url = if spec.base_url.is_empty() || spec.base_url == "http://localhost" {
"$API_BASE_URL".to_string()
} else {
spec.base_url.clone()
};
let full_url = format!("{}{}", base_url, operation.path);
let url_display = truncate_str(&full_url, BOX_WIDTH - 12);
let url_line = format!(" URL: {}", url_display);
let padding = BOX_WIDTH - 2 - url_line.len();
println!(
"{} {} {}{}{}",
"│".bright_blue(),
"URL:".dimmed(),
url_display.bright_white(),
" ".repeat(padding),
"│".bright_blue()
);
let has_auth = if let Some(security) = &operation.security {
!security.is_empty()
} else {
!spec.security_schemes.is_empty() || infer_auth_from_url(&spec.base_url)
};
let (auth_type, auth_flag) = if has_auth {
let (auth_type, auth_hint) = if let Some(security) = &operation.security {
get_auth_type_with_hint(spec, security)
} else if !spec.security_schemes.is_empty() {
get_default_auth_hint(spec)
} else {
infer_auth_type_from_url(&spec.base_url)
};
let auth_flag = if auth_type.contains("API Key") {
if auth_type.contains("(") && auth_type.contains(")") {
"--api-key \"$API_KEY\"".to_string()
} else {
"--api-key \"$API_KEY\"".to_string()
}
} else if auth_type.contains("Bearer") {
"--auth \"Bearer $TOKEN\"".to_string()
} else if auth_type.contains("Basic") {
"--auth \"Basic $CREDENTIALS\"".to_string()
} else if auth_type.contains("OAuth") {
"--profile \"$PROFILE\"".to_string()
} else {
"--auth \"$AUTH\"".to_string()
};
let auth_display = if auth_hint.is_empty() {
auth_type.clone()
} else {
format!("{} ({})", auth_type, auth_hint)
};
let auth_truncated = truncate_str(&auth_display, BOX_WIDTH - 10);
let padding = BOX_WIDTH - 2 - 6 - auth_truncated.len();
println!(
"{} {} {}{}{}",
"│".bright_blue(),
"🔐",
auth_truncated.bright_yellow(),
" ".repeat(padding.max(0)),
"│".bright_blue()
);
(auth_type, Some(auth_flag))
} else {
(String::new(), None)
};
let _ = auth_type;
println!(
"{}",
format!("└{}┘", "─".repeat(BOX_WIDTH - 2)).bright_blue()
);
let path_params: Vec<_> = operation
.parameters
.iter()
.filter(|p| p.location == ParameterLocation::Path)
.collect();
let query_params: Vec<_> = operation
.parameters
.iter()
.filter(|p| p.location == ParameterLocation::Query)
.collect();
let header_params: Vec<_> = operation
.parameters
.iter()
.filter(|p| p.location == ParameterLocation::Header)
.collect();
let has_body = operation.request_body.is_some();
let has_filter_param = query_params
.iter()
.any(|p| p.name == "filter" || p.name == "query" || p.name.contains("["));
if has_filter_param {
println!();
println!("{}", "FILTER:".bright_yellow());
println!();
for param in &query_params {
if param.name == "filter" || param.name == "query" {
println!(
" {} {}=<expression>",
"-p".bright_cyan(),
param.name.bright_white()
);
println!();
println!(" {} = != > < >= <= IN LIKE", "Operators:".dimmed());
println!(" {} AND OR", "Combine:".dimmed());
println!();
if let Some(desc) = ¶m.description {
println!(" {} {}", "Description:".dimmed(), desc.dimmed());
}
println!();
}
}
}
let regular_query_params: Vec<_> = query_params
.iter()
.filter(|p| p.name != "filter" && p.name != "query" && !p.name.contains("["))
.collect();
if !path_params.is_empty() || !regular_query_params.is_empty() || !header_params.is_empty() {
println!();
println!("{}", "PARAMETERS:".bright_yellow());
println!();
println!(
" {:<16} {:<8} {:<6} {}",
"Name".bright_white(),
"Type".bright_white(),
"Req".bright_white(),
"Description / Values".bright_white()
);
println!(" {}", "─".repeat(68).dimmed());
for param in &path_params {
let type_str = format_type_with_enum(¶m.schema);
let desc = get_param_description_or_values(param);
println!(
" {:<16} {:<8} {:<6} {}",
param.name.bright_cyan(),
type_str.dimmed(),
"✓".bright_red(),
truncate_str(&desc, 38).dimmed()
);
}
for param in ®ular_query_params {
let type_str = format_type_with_enum(¶m.schema);
let required = if param.required {
"✓".bright_red().to_string()
} else {
" ".dimmed().to_string()
};
let desc = get_param_description_or_values(param);
println!(
" {:<16} {:<8} {:<6} {}",
param.name,
type_str.dimmed(),
required,
truncate_str(&desc, 38).dimmed()
);
}
for param in &header_params {
if !is_auth_header(¶m.name) {
let type_str = format_type_with_enum(¶m.schema);
let required = if param.required {
"✓".bright_red().to_string()
} else {
" ".dimmed().to_string()
};
let desc = get_param_description_or_values(param);
println!(
" {:<16} {:<8} {:<6} {} {}",
param.name,
type_str.dimmed(),
required,
truncate_str(&desc, 30).dimmed(),
"[header]".dimmed()
);
}
}
}
if has_body {
if let Some(request_body) = &operation.request_body {
println!();
println!(
"{} {}",
"REQUEST BODY".bright_yellow(),
if request_body.required {
"(required):".bright_red()
} else {
"(optional):".dimmed()
}
);
println!();
let content_types: Vec<_> = request_body.content.keys().collect();
if !content_types.is_empty() {
println!(" {} {}", "Content-Type:".dimmed(), content_types[0]);
}
if let Some((_, media_type)) = request_body.content.iter().next() {
let schema = &media_type.schema;
if let Some(properties) = &schema.properties {
println!();
println!(" {{");
let required_fields: Vec<&String> = schema
.required
.as_ref()
.map(|r| r.iter().collect())
.unwrap_or_default();
for (i, (name, prop_schema)) in properties.iter().enumerate() {
let is_required = required_fields.contains(&name);
let type_str = prop_schema.schema_type.to_string();
let example = get_schema_example(name, prop_schema);
let required_marker = if is_required { " // required" } else { "" };
let comma = if i < properties.len() - 1 { "," } else { "" };
println!(
" \"{}\": {}{:<20} {}",
name.bright_white(),
example,
comma,
format!("// {}{}", type_str, required_marker).dimmed()
);
}
println!(" }}");
}
}
}
}
println!();
println!("{}", "─".repeat(68).dimmed());
println!();
println!("{}", "COPY & RUN:".bright_green());
println!();
let mut cmd_parts = vec![format!("mrapids run {}", operation.operation_id)];
if let Some(ref flag) = auth_flag {
cmd_parts.push(flag.clone());
}
for param in &path_params {
let example = get_param_example(¶m);
cmd_parts.push(format!("--param {}={}", param.name, example));
}
let example_params: Vec<_> = regular_query_params
.iter()
.filter(|p| p.required || is_common_param(&p.name))
.take(3)
.collect();
for param in example_params {
let example = get_param_example(¶m);
cmd_parts.push(format!("--param {}={}", param.name, example));
}
if has_body {
if let Some(request_body) = &operation.request_body {
if let Some((_, media_type)) = request_body.content.iter().next() {
let json_template = build_json_template(&media_type.schema);
if json_template.len() < 80 {
cmd_parts.push(format!("--data '{}'", json_template));
} else {
cmd_parts.push("--data @body.json".to_string());
}
} else {
cmd_parts.push("--data '{}'".to_string());
}
}
}
if cmd_parts.len() == 1 {
println!(" {}", cmd_parts[0].bright_cyan());
} else {
println!(" {} \\", cmd_parts[0].bright_cyan());
for (i, part) in cmd_parts[1..].iter().enumerate() {
if i < cmd_parts.len() - 2 {
println!(" {} \\", part.bright_cyan());
} else {
println!(" {}", part.bright_cyan());
}
}
}
println!();
println!("{}", "─".repeat(68).dimmed());
println!(
"{} {} to preview | {} for verbose | {} for curl",
"TIP:".bright_blue(),
"--dry-run".bright_cyan(),
"-v".bright_cyan(),
"--as-curl".bright_cyan()
);
println!();
Ok(())
}
fn infer_auth_from_url(url: &str) -> bool {
let url_lower = url.to_lowercase();
url_lower.contains("api.github.com")
|| url_lower.contains("api.stripe.com")
|| url_lower.contains("api.twilio.com")
|| url_lower.contains("api.slack.com")
|| url_lower.contains("api.openai.com")
|| url_lower.contains("api.anthropic.com")
|| url_lower.contains("graph.microsoft.com")
|| url_lower.contains("googleapis.com")
}
fn infer_auth_type_from_url(url: &str) -> (String, String) {
let url_lower = url.to_lowercase();
if url_lower.contains("api.github.com") {
return (
"Bearer Token".to_string(),
"GITHUB_TOKEN env or --auth 'Bearer $TOKEN'".to_string(),
);
}
if url_lower.contains("api.stripe.com") {
return (
"API Key".to_string(),
"STRIPE_API_KEY env or --auth 'Bearer sk_...'".to_string(),
);
}
if url_lower.contains("api.openai.com") {
return (
"Bearer Token".to_string(),
"OPENAI_API_KEY env or --auth 'Bearer $KEY'".to_string(),
);
}
if url_lower.contains("api.anthropic.com") {
return ("API Key".to_string(), "ANTHROPIC_API_KEY env".to_string());
}
if url_lower.contains("api.twilio.com") {
return (
"Basic Auth".to_string(),
"--auth 'Basic $ACCOUNT_SID:$AUTH_TOKEN'".to_string(),
);
}
if url_lower.contains("googleapis.com") {
return (
"OAuth2 / API Key".to_string(),
"mrapids auth connect".to_string(),
);
}
(
"Bearer Token".to_string(),
"--auth 'Bearer $TOKEN'".to_string(),
)
}
fn get_default_auth_hint(spec: &UnifiedSpec) -> (String, String) {
if let Some((name, scheme)) = spec.security_schemes.iter().next() {
return match scheme.scheme_type.as_str() {
"apiKey" => {
let header_name = scheme.name.as_deref().unwrap_or("X-API-Key");
(
format!("API Key ({})", header_name),
format!("--api-key $KEY",),
)
}
"http" => match scheme.scheme.as_deref() {
Some("bearer") => (
"Bearer Token".to_string(),
"--auth 'Bearer $TOKEN'".to_string(),
),
Some("basic") => (
"Basic Auth".to_string(),
"--auth 'Basic $BASE64'".to_string(),
),
_ => ("HTTP Auth".to_string(), "--auth".to_string()),
},
"oauth2" => ("OAuth2".to_string(), "mrapids auth connect".to_string()),
_ => (name.clone(), String::new()),
};
}
("Required".to_string(), String::new())
}
fn get_auth_type_with_hint(
spec: &UnifiedSpec,
security: &[crate::core::parser::SecurityRequirement],
) -> (String, String) {
if let Some(req) = security.first() {
if let Some(scheme) = spec.security_schemes.get(&req.scheme_name) {
return match scheme.scheme_type.as_str() {
"apiKey" => {
let header_name = scheme.name.as_deref().unwrap_or("X-API-Key");
(
format!("API Key ({})", header_name),
format!(
"--api-key $KEY or {}_API_KEY env",
header_name.to_uppercase().replace("-", "_")
),
)
}
"http" => match scheme.scheme.as_deref() {
Some("bearer") => (
"Bearer Token".to_string(),
"--auth 'Bearer $TOKEN' or *_TOKEN env".to_string(),
),
Some("basic") => (
"Basic Auth".to_string(),
"--auth 'Basic $BASE64'".to_string(),
),
_ => ("HTTP Auth".to_string(), "--auth".to_string()),
},
"oauth2" => ("OAuth2".to_string(), "mrapids auth connect".to_string()),
"openIdConnect" => (
"OpenID Connect".to_string(),
"mrapids auth connect".to_string(),
),
_ => (req.scheme_name.clone(), String::new()),
};
}
return (req.scheme_name.clone(), String::new());
}
("None".to_string(), String::new())
}
fn truncate_str(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_string()
} else if max_len > 3 {
format!("{}...", &s[..max_len - 3])
} else {
s[..max_len].to_string()
}
}
fn visible_len(s: &str) -> usize {
s.len()
}
fn format_type_with_enum(schema: &crate::core::parser::UnifiedSchema) -> String {
if let Some(enum_vals) = &schema.enum_values {
let values: Vec<String> = enum_vals
.iter()
.filter_map(|v| v.as_str().map(String::from))
.take(3)
.collect();
if !values.is_empty() {
if values.len() < enum_vals.len() {
return format!("{}...", values.join("|"));
}
return values.join("|");
}
}
schema.schema_type.to_string()
}
fn get_param_description_or_values(param: &crate::core::parser::UnifiedParameter) -> String {
if let Some(desc) = ¶m.description {
return desc.clone();
}
if let Some(enum_vals) = ¶m.schema.enum_values {
let values: Vec<String> = enum_vals
.iter()
.filter_map(|v| v.as_str().map(String::from))
.take(5)
.collect();
if !values.is_empty() {
if values.len() < enum_vals.len() {
return format!("Options: {} ...", values.join(", "));
}
return format!("Options: {}", values.join(", "));
}
}
if let Some(format) = ¶m.schema.format {
return match format.as_str() {
"date" => "Format: YYYY-MM-DD".to_string(),
"date-time" => "Format: ISO 8601".to_string(),
"email" => "Format: email".to_string(),
"uri" | "url" => "Format: URL".to_string(),
"uuid" => "Format: UUID".to_string(),
_ => format!("Format: {}", format),
};
}
if let Some(default) = ¶m.schema.default {
return format!("Default: {}", default);
}
String::new()
}
fn build_json_template(schema: &crate::core::parser::UnifiedSchema) -> String {
if let Some(properties) = &schema.properties {
let required_fields: std::collections::HashSet<&String> = schema
.required
.as_ref()
.map(|r| r.iter().collect())
.unwrap_or_default();
let mut obj = serde_json::Map::new();
for (name, prop_schema) in properties.iter() {
if required_fields.contains(name) {
let value = get_schema_example_value(name, prop_schema);
obj.insert(name.clone(), value);
}
}
if obj.is_empty() {
for (name, prop_schema) in properties.iter().take(3) {
let value = get_schema_example_value(name, prop_schema);
obj.insert(name.clone(), value);
}
}
return serde_json::to_string(&serde_json::Value::Object(obj))
.unwrap_or_else(|_| "{}".to_string());
}
"{}".to_string()
}
fn get_schema_example_value(
name: &str,
schema: &crate::core::parser::UnifiedSchema,
) -> serde_json::Value {
if let Some(example) = &schema.example {
return example.clone();
}
if let Some(enum_vals) = &schema.enum_values {
if let Some(first) = enum_vals.first() {
return first.clone();
}
}
let name_lower = name.to_lowercase();
match schema.schema_type {
crate::core::parser::SchemaType::String => {
if name_lower.contains("email") {
serde_json::json!("user@example.com")
} else if name_lower.contains("name") {
serde_json::json!("example")
} else if name_lower.contains("url") || name_lower.contains("uri") {
serde_json::json!("https://example.com")
} else if name_lower.contains("description") {
serde_json::json!("Description text")
} else {
serde_json::json!("string")
}
}
crate::core::parser::SchemaType::Integer => serde_json::json!(1),
crate::core::parser::SchemaType::Number => serde_json::json!(1.0),
crate::core::parser::SchemaType::Boolean => serde_json::json!(true),
crate::core::parser::SchemaType::Array => serde_json::json!([]),
crate::core::parser::SchemaType::Object => serde_json::json!({}),
_ => serde_json::Value::Null,
}
}
fn get_param_example(param: &crate::core::parser::UnifiedParameter) -> String {
if let Some(example) = ¶m.schema.example {
return example.to_string().trim_matches('"').to_string();
}
if let Some(enum_vals) = ¶m.schema.enum_values {
if let Some(first) = enum_vals.first() {
return first.as_str().unwrap_or("value").to_string();
}
}
if let Some(default) = ¶m.schema.default {
return default.to_string().trim_matches('"').to_string();
}
let name_lower = param.name.to_lowercase();
if name_lower.contains("id") {
return "123".to_string();
}
if name_lower.contains("limit") {
return "20".to_string();
}
if name_lower.contains("page") {
return "1".to_string();
}
if name_lower.contains("offset") {
return "0".to_string();
}
if name_lower.contains("sort") {
return "created_at:desc".to_string();
}
if name_lower.contains("status") {
return "active".to_string();
}
if name_lower.contains("email") {
return "user@example.com".to_string();
}
if name_lower.contains("name") {
return "example".to_string();
}
match param.schema.schema_type {
crate::core::parser::SchemaType::Integer => "10".to_string(),
crate::core::parser::SchemaType::Number => "10.5".to_string(),
crate::core::parser::SchemaType::Boolean => "true".to_string(),
crate::core::parser::SchemaType::String => "<value>".to_string(),
_ => "<value>".to_string(),
}
}
fn get_schema_example(name: &str, schema: &crate::core::parser::UnifiedSchema) -> String {
if let Some(example) = &schema.example {
return serde_json::to_string(example).unwrap_or_else(|_| "\"example\"".to_string());
}
if let Some(enum_vals) = &schema.enum_values {
if let Some(first) = enum_vals.first() {
return serde_json::to_string(first).unwrap_or_else(|_| "\"value\"".to_string());
}
}
let name_lower = name.to_lowercase();
match schema.schema_type {
crate::core::parser::SchemaType::String => {
if name_lower.contains("email") {
"\"user@example.com\"".to_string()
} else if name_lower.contains("name") {
"\"Example Name\"".to_string()
} else if name_lower.contains("url") || name_lower.contains("uri") {
"\"https://example.com\"".to_string()
} else if name_lower.contains("id") {
"\"abc123\"".to_string()
} else if name_lower.contains("date") {
"\"2024-01-01\"".to_string()
} else {
"\"string\"".to_string()
}
}
crate::core::parser::SchemaType::Integer => {
if name_lower.contains("age") {
"25".to_string()
} else if name_lower.contains("count") || name_lower.contains("quantity") {
"1".to_string()
} else if name_lower.contains("amount") || name_lower.contains("price") {
"100".to_string()
} else {
"0".to_string()
}
}
crate::core::parser::SchemaType::Number => "0.0".to_string(),
crate::core::parser::SchemaType::Boolean => {
if name_lower.contains("active") || name_lower.contains("enabled") {
"true".to_string()
} else {
"false".to_string()
}
}
crate::core::parser::SchemaType::Array => "[...]".to_string(),
crate::core::parser::SchemaType::Object => "{...}".to_string(),
_ => "null".to_string(),
}
}
fn is_auth_header(name: &str) -> bool {
let lower = name.to_lowercase();
lower == "authorization"
|| lower.contains("api-key")
|| lower.contains("apikey")
|| lower.contains("x-api-key")
|| lower.contains("token")
}
fn is_common_param(name: &str) -> bool {
let lower = name.to_lowercase();
lower == "limit"
|| lower == "page"
|| lower == "offset"
|| lower == "sort"
|| lower == "order"
|| lower == "status"
}
pub fn show_all_operations(spec: &UnifiedSpec) -> Result<()> {
println!();
println!("{}", "Available operations:".bright_yellow());
println!();
println!(
" {:<30} {:<8} {:<30} {}",
"Operation ID".bright_white(),
"Method".bright_white(),
"Path".bright_white(),
"Summary".bright_white()
);
println!(" {}", "─".repeat(90).dimmed());
for op in &spec.operations {
let summary = op
.summary
.as_deref()
.unwrap_or("")
.chars()
.take(30)
.collect::<String>();
let method_colored = match op.method.to_uppercase().as_str() {
"GET" => op.method.to_uppercase().bright_green(),
"POST" => op.method.to_uppercase().bright_blue(),
"PUT" => op.method.to_uppercase().bright_yellow(),
"DELETE" => op.method.to_uppercase().bright_red(),
"PATCH" => op.method.to_uppercase().bright_magenta(),
_ => op.method.to_uppercase().normal(),
};
println!(
" {:<30} {:<8} {:<30} {}",
op.operation_id.bright_cyan(),
method_colored,
op.path,
summary.dimmed()
);
}
println!();
println!("{} mrapids run <operation> -Q", "Usage:".bright_blue());
println!();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_infer_auth_from_url() {
assert!(infer_auth_from_url("https://api.github.com/repos"));
assert!(infer_auth_from_url("https://api.stripe.com/v1/charges"));
assert!(infer_auth_from_url("https://api.openai.com/v1/completions"));
assert!(infer_auth_from_url("https://api.anthropic.com/v1/messages"));
assert!(infer_auth_from_url("https://api.twilio.com/v1/messages"));
assert!(infer_auth_from_url("https://graph.microsoft.com/v1.0/me"));
assert!(infer_auth_from_url(
"https://www.googleapis.com/drive/v3/files"
));
assert!(!infer_auth_from_url("https://httpbin.org/get"));
assert!(!infer_auth_from_url(
"https://jsonplaceholder.typicode.com/posts"
));
assert!(!infer_auth_from_url("http://localhost:8080/api"));
}
#[test]
fn test_infer_auth_type_from_url() {
let (auth_type, hint) = infer_auth_type_from_url("https://api.github.com/repos");
assert_eq!(auth_type, "Bearer Token");
assert!(hint.contains("GITHUB_TOKEN"));
let (auth_type, hint) = infer_auth_type_from_url("https://api.stripe.com/v1/charges");
assert_eq!(auth_type, "API Key");
assert!(hint.contains("STRIPE_API_KEY"));
let (auth_type, hint) = infer_auth_type_from_url("https://api.openai.com/v1/completions");
assert_eq!(auth_type, "Bearer Token");
assert!(hint.contains("OPENAI_API_KEY"));
let (auth_type, hint) = infer_auth_type_from_url("https://api.twilio.com/v1/messages");
assert_eq!(auth_type, "Basic Auth");
assert!(hint.contains("ACCOUNT_SID"));
let (auth_type, hint) = infer_auth_type_from_url("https://api.unknown.com/v1");
assert_eq!(auth_type, "Bearer Token");
assert!(hint.contains("--auth"));
}
#[test]
fn test_truncate_str() {
assert_eq!(truncate_str("hello", 10), "hello");
assert_eq!(truncate_str("hello world", 8), "hello...");
assert_eq!(truncate_str("abc", 3), "abc");
assert_eq!(truncate_str("abcdef", 5), "ab...");
assert_eq!(truncate_str("ab", 2), "ab");
assert_eq!(truncate_str("abc", 2), "ab"); }
#[test]
fn test_is_auth_header() {
assert!(is_auth_header("Authorization"));
assert!(is_auth_header("AUTHORIZATION"));
assert!(is_auth_header("X-API-Key"));
assert!(is_auth_header("x-api-key"));
assert!(is_auth_header("apikey"));
assert!(is_auth_header("Bearer-Token"));
assert!(!is_auth_header("Content-Type"));
assert!(!is_auth_header("Accept"));
assert!(!is_auth_header("X-Request-ID"));
}
#[test]
fn test_is_common_param() {
assert!(is_common_param("limit"));
assert!(is_common_param("LIMIT"));
assert!(is_common_param("page"));
assert!(is_common_param("offset"));
assert!(is_common_param("sort"));
assert!(is_common_param("order"));
assert!(is_common_param("status"));
assert!(!is_common_param("user_id"));
assert!(!is_common_param("name"));
assert!(!is_common_param("filter"));
}
#[test]
fn test_visible_len() {
assert_eq!(visible_len("hello"), 5);
assert_eq!(visible_len("hello world"), 11);
assert_eq!(visible_len(""), 0);
}
#[test]
fn test_format_type_with_enum() {
use crate::core::parser::{SchemaType, UnifiedSchema};
let schema = UnifiedSchema {
schema_type: SchemaType::String,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
};
assert_eq!(format_type_with_enum(&schema), "string");
let schema_with_enum = UnifiedSchema {
schema_type: SchemaType::String,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: Some(vec![
serde_json::json!("active"),
serde_json::json!("inactive"),
]),
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
};
assert_eq!(format_type_with_enum(&schema_with_enum), "active|inactive");
let schema_many_enums = UnifiedSchema {
schema_type: SchemaType::String,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: Some(vec![
serde_json::json!("a"),
serde_json::json!("b"),
serde_json::json!("c"),
serde_json::json!("d"),
serde_json::json!("e"),
]),
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
};
assert_eq!(format_type_with_enum(&schema_many_enums), "a|b|c...");
}
#[test]
fn test_get_schema_example() {
use crate::core::parser::{SchemaType, UnifiedSchema};
let schema = UnifiedSchema {
schema_type: SchemaType::String,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
};
assert_eq!(get_schema_example("email", &schema), "\"user@example.com\"");
assert_eq!(get_schema_example("name", &schema), "\"Example Name\"");
assert_eq!(
get_schema_example("url", &schema),
"\"https://example.com\""
);
assert_eq!(get_schema_example("id", &schema), "\"abc123\"");
assert_eq!(get_schema_example("random", &schema), "\"string\"");
let int_schema = UnifiedSchema {
schema_type: SchemaType::Integer,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
};
assert_eq!(get_schema_example("age", &int_schema), "25");
assert_eq!(get_schema_example("count", &int_schema), "1");
assert_eq!(get_schema_example("amount", &int_schema), "100");
let bool_schema = UnifiedSchema {
schema_type: SchemaType::Boolean,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
};
assert_eq!(get_schema_example("active", &bool_schema), "true");
assert_eq!(get_schema_example("disabled", &bool_schema), "false");
}
#[test]
fn test_build_json_template() {
use crate::core::parser::{SchemaType, UnifiedSchema};
use std::collections::HashMap;
let empty_schema = UnifiedSchema {
schema_type: SchemaType::Object,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
};
assert_eq!(build_json_template(&empty_schema), "{}");
let mut properties = HashMap::new();
properties.insert(
"name".to_string(),
UnifiedSchema {
schema_type: SchemaType::String,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
},
);
properties.insert(
"optional_field".to_string(),
UnifiedSchema {
schema_type: SchemaType::String,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
},
);
let schema_with_props = UnifiedSchema {
schema_type: SchemaType::Object,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: Some(properties),
required: Some(vec!["name".to_string()]),
items: None,
};
let template = build_json_template(&schema_with_props);
assert!(template.contains("name"));
}
use crate::core::parser::{SchemaType, UnifiedParameter, UnifiedSchema};
fn make_test_param(name: &str, required: bool, schema: UnifiedSchema) -> UnifiedParameter {
UnifiedParameter {
name: name.to_string(),
location: ParameterLocation::Path,
required,
schema,
description: Some(format!("The {}", name)),
example: None,
}
}
fn make_simple_schema(schema_type: SchemaType) -> UnifiedSchema {
UnifiedSchema {
schema_type,
format: None,
description: None,
example: None,
default: None,
minimum: None,
maximum: None,
enum_values: None,
pattern: None,
min_length: None,
max_length: None,
properties: None,
required: None,
items: None,
}
}
#[test]
fn test_hint_enum_values() {
let params = vec![make_test_param(
"status",
true,
UnifiedSchema {
schema_type: SchemaType::String,
enum_values: Some(vec![
serde_json::json!("available"),
serde_json::json!("pending"),
serde_json::json!("sold"),
]),
..make_simple_schema(SchemaType::String)
},
)];
let hints = build_parameter_hints(¶ms, &None);
let hint = hints.get("status").unwrap();
assert!(hint.possible_values.is_some());
assert_eq!(hint.possible_values.as_ref().unwrap().len(), 3);
}
#[test]
fn test_hint_required_missing() {
let params = vec![
make_test_param("petId", true, make_simple_schema(SchemaType::Integer)),
make_test_param(
"optional_field",
false,
make_simple_schema(SchemaType::String),
),
];
let missing = find_missing_required(¶ms, &None);
assert!(missing.contains(&"petId".to_string()));
assert!(!missing.contains(&"optional_field".to_string()));
}
#[test]
fn test_clarifying_question_format() {
let params = vec![make_test_param(
"petId",
true,
make_simple_schema(SchemaType::Integer),
)];
let questions = build_clarifying_questions(¶ms, &None);
assert_eq!(questions.len(), 1);
assert!(questions[0].contains("petId"));
assert!(questions[0].contains("required"));
assert!(questions[0].contains("integer"));
assert!(questions[0].contains("path"));
}
#[test]
fn test_hint_examples_from_spec() {
let mut schema = make_simple_schema(SchemaType::Integer);
schema.example = Some(serde_json::json!(42));
let params = vec![make_test_param("petId", true, schema)];
let hints = build_parameter_hints(¶ms, &None);
let hint = hints.get("petId").unwrap();
assert!(!hint.examples.is_empty());
assert_eq!(hint.examples[0], serde_json::json!(42));
}
#[test]
fn test_hint_constraints() {
let mut schema = make_simple_schema(SchemaType::Integer);
schema.minimum = Some(1.0);
schema.maximum = Some(100.0);
let params = vec![make_test_param("limit", false, schema)];
let hints = build_parameter_hints(¶ms, &None);
let hint = hints.get("limit").unwrap();
assert_eq!(hint.constraints["minimum"], serde_json::json!(1.0));
assert_eq!(hint.constraints["maximum"], serde_json::json!(100.0));
}
}