use crate::cli::DoctorCommand;
use anyhow::Result;
use colored::*;
use std::collections::HashMap;
use std::env;
use std::fs;
use std::path::Path;
#[derive(Debug)]
struct DiagnosticResult {
category: String,
status: DiagnosticStatus,
message: String,
fix_hint: Option<String>,
}
#[derive(Debug, PartialEq)]
enum DiagnosticStatus {
Ok,
Warning,
Error,
}
pub fn run_diagnostics(cmd: DoctorCommand, env: Option<String>) -> Result<()> {
let mut results = Vec::new();
let env_name = if let Some(env) = env {
env
} else {
let manifest_path = std::path::PathBuf::from("mrapids.yaml");
if manifest_path.exists() {
let content = fs::read_to_string(&manifest_path)?;
let manifest: serde_yaml::Value = serde_yaml::from_str(&content)?;
manifest
.get("default_env")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| "development".to_string())
} else {
"development".to_string()
}
};
println!(
"\n{} {} {}",
"🏥".bright_cyan(),
"MRapids Configuration Check".bold(),
format!("[{}]", env_name).bright_blue()
);
println!("{}", "━".repeat(50).dimmed());
let check_all = cmd.check == "all";
if check_all || cmd.check == "config" {
results.extend(check_project_structure(&cmd.path)?);
}
if check_all || cmd.check == "config" {
results.extend(check_configuration(&cmd.path)?);
}
if check_all || cmd.check == "auth" {
results.extend(check_authentication(&cmd.path, &env_name)?);
}
if check_all || cmd.check == "env" {
results.extend(check_base_url_resolution(&cmd.path, &env_name)?);
}
if check_all || cmd.check == "spec" {
results.extend(check_openapi_spec(&cmd.path)?);
}
if check_all || cmd.check == "env" {
results.extend(check_env_consistency(&cmd.path, &env_name)?);
}
if check_all || cmd.check == "config" {
results.extend(check_decision_logging());
}
if cmd.fix {
apply_fixes(&cmd.path, &mut results)?;
}
display_results(&results, &cmd)?;
display_summary(&results, cmd.fix)?;
Ok(())
}
fn check_project_structure(project_path: &Path) -> Result<Vec<DiagnosticResult>> {
let mut results = Vec::new();
println!("\n{} {}", "✅".green(), "Project Structure".bold());
let mrapids_yaml = project_path.join("mrapids.yaml");
if mrapids_yaml.exists() {
if let Ok(content) = fs::read_to_string(&mrapids_yaml) {
if let Ok(_manifest) = serde_yaml::from_str::<crate::core::config::Manifest>(&content) {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Ok,
message: "mrapids.yaml manifest found and valid".to_string(),
fix_hint: None,
});
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Warning,
message: "mrapids.yaml found but invalid format".to_string(),
fix_hint: Some("Check YAML syntax in mrapids.yaml".to_string()),
});
}
}
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Error,
message: "mrapids.yaml not found".to_string(),
fix_hint: Some("Run: mrapids init <project-name>".to_string()),
});
}
let specs_dir = project_path.join("specs");
if specs_dir.exists() {
let has_spec = ["api.yaml", "api.json", "openapi.yaml", "openapi.json"]
.iter()
.any(|f| specs_dir.join(f).exists());
if has_spec {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Ok,
message: "specs/ directory with API specification found".to_string(),
fix_hint: None,
});
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Warning,
message: "specs/ directory exists but no API spec found".to_string(),
fix_hint: Some("Add your OpenAPI spec to specs/api.yaml".to_string()),
});
}
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Error,
message: "specs/ directory not found".to_string(),
fix_hint: Some("Run: mrapids init <project-name>".to_string()),
});
}
let config_dir = project_path.join("config");
if config_dir.exists() {
let env_configs = [
"default.yaml",
"development.yaml",
"staging.yaml",
"production.yaml",
];
let found_configs: Vec<_> = env_configs
.iter()
.filter(|f| config_dir.join(f).exists())
.collect();
if !found_configs.is_empty() {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Ok,
message: format!(
"config/ directory with {} environment configs",
found_configs.len()
),
fix_hint: None,
});
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Warning,
message: "config/ directory exists but no configs found".to_string(),
fix_hint: Some("Add config/default.yaml or config/dev.yaml".to_string()),
});
}
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Warning,
message: "config/ directory not found".to_string(),
fix_hint: Some("Run: mrapids init <project-name>".to_string()),
});
}
let env_dir = project_path.join("env");
if env_dir.exists() {
let env_files = [".env.development", ".env.staging", ".env.production"];
let found_envs: Vec<_> = env_files
.iter()
.filter(|f| env_dir.join(f).exists())
.collect();
if !found_envs.is_empty() {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Ok,
message: format!("env/ directory with {} env files", found_envs.len()),
fix_hint: None,
});
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Warning,
message: "env/ directory exists but no .env files found".to_string(),
fix_hint: Some(
"Add env/.env.development with your environment variables".to_string(),
),
});
}
} else {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Warning,
message: "env/ directory not found".to_string(),
fix_hint: Some("Run: mrapids init <project-name>".to_string()),
});
}
let auth_yaml = project_path.join("auth.yaml");
if auth_yaml.exists() {
results.push(DiagnosticResult {
category: "Structure".to_string(),
status: DiagnosticStatus::Ok,
message: "auth.yaml file found".to_string(),
fix_hint: None,
});
}
Ok(results)
}
fn check_configuration(project_path: &Path) -> Result<Vec<DiagnosticResult>> {
let mut results = Vec::new();
println!("\n{} {}", "⚠️".yellow(), "Configuration Issues".bold());
let default_config = project_path.join("config/default.yaml");
if default_config.exists() {
if let Ok(content) = fs::read_to_string(&default_config) {
if content.contains("base_url:") {
results.push(DiagnosticResult {
category: "Config".to_string(),
status: DiagnosticStatus::Ok,
message: "config/default.yaml exists with base_url".to_string(),
fix_hint: None,
});
} else {
results.push(DiagnosticResult {
category: "Config".to_string(),
status: DiagnosticStatus::Warning,
message: "config/default.yaml missing base_url field".to_string(),
fix_hint: Some("Add: base_url: https://api.example.com".to_string()),
});
}
}
} else {
results.push(DiagnosticResult {
category: "Config".to_string(),
status: DiagnosticStatus::Error,
message: "config/default.yaml missing".to_string(),
fix_hint: Some("Run: mrapids init <project-name> to create config".to_string()),
});
}
let env_file = project_path.join(".env");
let env_example = project_path.join("config/.env.example");
if !env_file.exists() && env_example.exists() {
results.push(DiagnosticResult {
category: "Config".to_string(),
status: DiagnosticStatus::Warning,
message: "No .env file found".to_string(),
fix_hint: Some("Copy config/.env.example to .env and fill in values".to_string()),
});
} else if env_file.exists() {
results.push(DiagnosticResult {
category: "Config".to_string(),
status: DiagnosticStatus::Ok,
message: ".env file exists".to_string(),
fix_hint: None,
});
}
Ok(results)
}
fn check_authentication(project_path: &Path, env_name: &str) -> Result<Vec<DiagnosticResult>> {
let mut results = Vec::new();
println!(
"\n{} {} {}",
"🔑".yellow(),
"Authentication".bold(),
format!("[{}]", env_name).bright_blue()
);
let spec_files = [
"specs/api.yaml",
"specs/api.json",
"specs/openapi.yaml",
"specs/openapi.json",
];
let mut auth_methods = Vec::new();
for spec_file in &spec_files {
let spec_path = project_path.join(spec_file);
if spec_path.exists() {
if let Ok(content) = fs::read_to_string(&spec_path) {
if content.contains("bearerAuth") || content.contains("scheme: bearer") {
auth_methods.push("Bearer Token");
}
if content.contains("apiKey") || content.contains("type: apiKey") {
auth_methods.push("API Key");
}
if content.contains("basic") || content.contains("scheme: basic") {
auth_methods.push("Basic Auth");
}
if content.contains("oauth2") || content.contains("type: oauth2") {
auth_methods.push("OAuth 2.0");
}
}
break;
}
}
if !auth_methods.is_empty() {
results.push(DiagnosticResult {
category: "Auth".to_string(),
status: DiagnosticStatus::Ok,
message: format!("API spec defines: {}", auth_methods.join(", ")),
fix_hint: None,
});
}
let env_prefix = env_name.to_uppercase();
let has_bearer = env::var(format!("{}_{}", env_prefix.replace("-", "_"), "API_TOKEN")).is_ok()
|| env::var(format!("{}_{}", env_prefix.replace("-", "_"), "TOKEN")).is_ok();
let has_api_key = env::var(format!("{}_{}", env_prefix.replace("-", "_"), "API_KEY")).is_ok();
let has_basic = env::var(format!("{}_{}", env_prefix.replace("-", "_"), "BASIC_AUTH")).is_ok();
let has_generic_token = env::var("API_TOKEN").is_ok() || env::var("AUTH_TOKEN").is_ok();
let has_generic_key = env::var("API_KEY").is_ok();
if !has_bearer && !has_api_key && !has_basic && !has_generic_token && !has_generic_key {
results.push(DiagnosticResult {
category: "Auth".to_string(),
status: DiagnosticStatus::Warning,
message: "No auth credentials found in environment".to_string(),
fix_hint: Some(format!(
"Add credentials to env/.env.{} (e.g., {}_API_TOKEN, {}_API_KEY)",
env_name,
env_prefix.replace("-", "_"),
env_prefix.replace("-", "_")
)),
});
} else {
let mut configured = Vec::new();
if has_bearer || has_generic_token {
configured.push("Bearer/Token");
}
if has_api_key || has_generic_key {
configured.push("API Key");
}
if has_basic {
configured.push("Basic Auth");
}
results.push(DiagnosticResult {
category: "Auth".to_string(),
status: DiagnosticStatus::Ok,
message: format!("Auth credentials found: {}", configured.join(", ")),
fix_hint: None,
});
}
Ok(results)
}
fn check_base_url_resolution(project_path: &Path, env_name: &str) -> Result<Vec<DiagnosticResult>> {
let mut results = Vec::new();
println!(
"\n{} {} {}",
"🌐".bright_blue(),
"Base URL Resolution".bold(),
format!("[{}]", env_name).bright_blue()
);
println!(" Checking resolution order:");
let mut found_url = None;
let mut resolution_steps = Vec::new();
let env_config = project_path.join(format!("config/{}.yaml", env_name));
if env_config.exists() {
if let Ok(content) = fs::read_to_string(&env_config) {
if let Some(line) = content.lines().find(|l| l.trim().starts_with("base_url:")) {
let url = line.split(':').nth(1).map(|s| s.trim()).unwrap_or("");
if !url.is_empty() && !url.starts_with("$") {
found_url = Some(url.to_string());
resolution_steps.push(format!(
" 1. config/{}.yaml: {} {}",
env_name,
"✓".green(),
url
));
} else if url.starts_with("$") {
resolution_steps.push(format!(
" 1. config/{}.yaml: {} Uses env variable: {}",
env_name,
"✓".green(),
url
));
} else {
resolution_steps.push(format!(
" 1. config/{}.yaml: {} No base_url field",
env_name,
"✗".red()
));
}
} else {
resolution_steps.push(format!(
" 1. config/{}.yaml: {} No base_url field",
env_name,
"✗".red()
));
}
}
} else {
resolution_steps.push(format!(
" 1. config/{}.yaml: {} File not found",
env_name,
"✗".red()
));
}
let default_config = project_path.join("config/default.yaml");
if default_config.exists() && found_url.is_none() {
if let Ok(content) = fs::read_to_string(&default_config) {
if let Some(line) = content.lines().find(|l| l.trim().starts_with("base_url:")) {
let url = line.split(':').nth(1).map(|s| s.trim()).unwrap_or("");
if !url.is_empty() && !url.starts_with("$") {
found_url = Some(url.to_string());
resolution_steps.push(format!(
" 2. config/default.yaml: {} {}",
"✓".green(),
url
));
} else {
resolution_steps.push(format!(
" 2. config/default.yaml: {} Uses env variable",
"✓".green()
));
}
} else {
resolution_steps.push(format!(
" 2. config/default.yaml: {} No base_url field",
"✗".red()
));
}
}
} else {
resolution_steps.push(format!(
" 2. config/default.yaml: {} File not found",
"✗".red()
));
}
let env_prefix = env_name.to_uppercase().replace("-", "_");
let env_base_url_var = format!("{}_BASE_URL", env_prefix);
if let Ok(url) = env::var(&env_base_url_var) {
if found_url.is_none() {
found_url = Some(url.clone());
}
resolution_steps.push(format!(
" 3. ${}: {} {}",
env_base_url_var,
"✓".green(),
url
));
} else {
resolution_steps.push(format!(
" 3. ${}: {} Not set",
env_base_url_var,
"✗".red()
));
}
if let Ok(url) = env::var("API_BASE_URL") {
if found_url.is_none() {
found_url = Some(url.clone());
}
resolution_steps.push(format!(" 4. $API_BASE_URL: {} {}", "✓".green(), url));
} else {
resolution_steps.push(format!(" 4. $API_BASE_URL: {} Not set", "✗".red()));
}
if let Ok(url) = env::var("MRAPIDS_BASE_URL") {
if found_url.is_none() {
found_url = Some(url.clone());
}
resolution_steps.push(format!(" 5. $MRAPIDS_BASE_URL: {} {}", "✓".green(), url));
} else {
resolution_steps.push(format!(" 5. $MRAPIDS_BASE_URL: {} Not set", "✗".red()));
}
let spec_files = [
"specs/api.yaml",
"specs/api.json",
"specs/openapi.yaml",
"specs/openapi.json",
];
let mut spec_url = None;
for spec_file in &spec_files {
let spec_path = project_path.join(spec_file);
if spec_path.exists() {
if let Ok(content) = fs::read_to_string(&spec_path) {
if let Some(servers_line) = content.lines().find(|l| l.trim().starts_with("- url:"))
{
let url = servers_line
.split(':')
.skip(1)
.collect::<Vec<_>>()
.join(":")
.trim()
.to_string();
spec_url = Some(url.clone());
if found_url.is_none() && !url.contains("localhost") {
found_url = Some(url.clone());
}
break;
}
}
}
}
if let Some(url) = spec_url {
resolution_steps.push(format!(" 6. OpenAPI servers[0]: {} {}", "✓".green(), url));
} else {
resolution_steps.push(format!(
" 6. OpenAPI servers[0]: {} Missing or empty",
"✗".red()
));
}
for step in resolution_steps {
println!("{}", step);
}
if let Some(url) = found_url {
println!("\n Will use: {}", url.bright_green());
results.push(DiagnosticResult {
category: "URL".to_string(),
status: DiagnosticStatus::Ok,
message: format!("Base URL resolved to: {}", url),
fix_hint: None,
});
} else {
results.push(DiagnosticResult {
category: "URL".to_string(),
status: DiagnosticStatus::Error,
message: "No base URL could be resolved".to_string(),
fix_hint: Some("Set API_BASE_URL env var or add to config/default.yaml".to_string()),
});
}
Ok(results)
}
fn check_openapi_spec(project_path: &Path) -> Result<Vec<DiagnosticResult>> {
let mut results = Vec::new();
let spec_files = [
"specs/api.yaml",
"specs/api.json",
"specs/openapi.yaml",
"specs/openapi.json",
];
let mut spec_found = false;
for spec_file in &spec_files {
let spec_path = project_path.join(spec_file);
if spec_path.exists() {
spec_found = true;
if let Ok(content) = fs::read_to_string(&spec_path) {
if content.contains("openapi:") || content.contains("\"openapi\"") {
results.push(DiagnosticResult {
category: "Spec".to_string(),
status: DiagnosticStatus::Ok,
message: format!("Valid OpenAPI spec found: {}", spec_file),
fix_hint: None,
});
} else if content.contains("swagger:") || content.contains("\"swagger\"") {
results.push(DiagnosticResult {
category: "Spec".to_string(),
status: DiagnosticStatus::Ok,
message: format!("Valid Swagger spec found: {}", spec_file),
fix_hint: None,
});
}
}
break;
}
}
if !spec_found {
results.push(DiagnosticResult {
category: "Spec".to_string(),
status: DiagnosticStatus::Error,
message: "No OpenAPI/Swagger spec found".to_string(),
fix_hint: Some("Run: mrapids init --from-url <openapi-url>".to_string()),
});
}
Ok(results)
}
fn check_env_consistency(project_path: &Path, env_name: &str) -> Result<Vec<DiagnosticResult>> {
let mut results = Vec::new();
println!(
"\n{} {} {}",
"🔄".bright_green(),
"Environment Variable Consistency".bold(),
format!("[{}]", env_name).bright_blue()
);
let config_file = project_path.join(format!("config/{}.yaml", env_name));
let default_config = project_path.join("config/default.yaml");
let env_file = project_path.join(format!("env/.env.{}", env_name));
let mut config_vars = Vec::new();
if config_file.exists() {
if let Ok(content) = fs::read_to_string(&config_file) {
config_vars.extend(extract_env_vars(&content));
}
}
if default_config.exists() {
if let Ok(content) = fs::read_to_string(&default_config) {
config_vars.extend(extract_env_vars(&content));
}
}
config_vars.sort();
config_vars.dedup();
if config_vars.is_empty() {
results.push(DiagnosticResult {
category: "EnvConsistency".to_string(),
status: DiagnosticStatus::Ok,
message: "No environment variables referenced in config".to_string(),
fix_hint: None,
});
return Ok(results);
}
let mut env_vars = HashMap::new();
if env_file.exists() {
if let Ok(content) = fs::read_to_string(&env_file) {
for line in content.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with('#') {
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].trim().to_string();
let value = line[eq_pos + 1..].trim().to_string();
env_vars.insert(key, value);
}
}
}
}
}
for var in &config_vars {
if env::var(var).is_ok() && !env_vars.contains_key(var) {
env_vars.insert(var.clone(), "<from environment>".to_string());
}
}
let mut missing_vars = Vec::new();
let mut defined_vars = Vec::new();
println!(
" Checking {} variables referenced in config:",
config_vars.len()
);
for var in &config_vars {
if env_vars.contains_key(var) {
let value = &env_vars[var];
let display_value = if value == "<from environment>" {
value.to_string()
} else if value.is_empty() {
"<empty>".to_string()
} else if value.len() > 20 {
format!("{}...", &value[..20])
} else {
value.clone()
};
println!(" {} ${}: {}", "✓".green(), var, display_value.dimmed());
defined_vars.push(var.clone());
} else {
println!(" {} ${}: {}", "✗".red(), var, "Not defined".red());
missing_vars.push(var.clone());
}
}
if missing_vars.is_empty() {
results.push(DiagnosticResult {
category: "EnvConsistency".to_string(),
status: DiagnosticStatus::Ok,
message: format!("All {} config variables are defined", config_vars.len()),
fix_hint: None,
});
} else {
results.push(DiagnosticResult {
category: "EnvConsistency".to_string(),
status: DiagnosticStatus::Error,
message: format!(
"{} of {} config variables are missing",
missing_vars.len(),
config_vars.len()
),
fix_hint: Some(format!(
"Add to env/.env.{}: {}",
env_name,
missing_vars.join(", ")
)),
});
}
let unused_vars: Vec<String> = env_vars
.keys()
.filter(|k| !config_vars.contains(k) && *k != "<from environment>")
.cloned()
.collect();
if !unused_vars.is_empty() {
println!(
"\n {} Unused variables in env/.env.{}:",
"⚠️".yellow(),
env_name
);
for var in &unused_vars {
println!(" - {}", var.yellow());
}
results.push(DiagnosticResult {
category: "EnvConsistency".to_string(),
status: DiagnosticStatus::Warning,
message: format!("{} unused variables in .env file", unused_vars.len()),
fix_hint: Some(format!("Consider removing: {}", unused_vars.join(", "))),
});
}
Ok(results)
}
fn extract_env_vars(content: &str) -> Vec<String> {
let mut vars = Vec::new();
let re = regex::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)(:-[^}]*)?\}").unwrap();
for cap in re.captures_iter(content) {
if let Some(var_name) = cap.get(1) {
vars.push(var_name.as_str().to_string());
}
}
let re2 = regex::Regex::new(r"\$([A-Z_][A-Z0-9_]*)").unwrap();
for cap in re2.captures_iter(content) {
if let Some(var_name) = cap.get(1) {
let var = var_name.as_str().to_string();
if !content.contains(&format!("${{{}", var))
&& !content.contains(&format!("${{{}:", var))
{
vars.push(var);
}
}
}
vars
}
fn apply_fixes(project_path: &Path, results: &mut Vec<DiagnosticResult>) -> Result<()> {
println!(
"\n{} {}",
"🔧".bright_yellow(),
"Auto-fixing issues...".bold()
);
let mut fixed_count = 0;
for result in results.iter_mut() {
if result.status != DiagnosticStatus::Ok {
match result.message.as_str() {
"config/default.yaml missing" => {
let config_dir = project_path.join("config");
fs::create_dir_all(&config_dir)?;
let default_config = r#"# Default environment configuration
name: default
description: Default environment
# Set your API base URL
base_url: ${API_BASE_URL:-https://api.example.com}
# Common headers
headers:
Accept: application/json
User-Agent: MRapids/1.0
"#;
fs::write(config_dir.join("default.yaml"), default_config)?;
result.status = DiagnosticStatus::Ok;
result.message =
"Created config/default.yaml with extracted base URL".to_string();
fixed_count += 1;
println!(" {} Created config/default.yaml", "✓".green());
}
"No .env file found" => {
let env_example = project_path.join("config/.env.example");
let env_file = project_path.join(".env");
if env_example.exists() {
fs::copy(&env_example, &env_file)?;
result.status = DiagnosticStatus::Ok;
result.message = "Created .env from .env.example template".to_string();
fixed_count += 1;
println!(" {} Created .env from template", "✓".green());
}
}
_ => {}
}
}
}
if fixed_count > 0 {
println!(
"\nFixed {} of {} issues.",
fixed_count,
results
.iter()
.filter(|r| r.status != DiagnosticStatus::Ok)
.count()
+ fixed_count
);
} else {
println!(" No auto-fixable issues found.");
}
Ok(())
}
fn display_results(results: &[DiagnosticResult], cmd: &DoctorCommand) -> Result<()> {
if cmd.format == "json" {
let json_results: Vec<HashMap<String, String>> = results
.iter()
.map(|r| {
let mut map = HashMap::new();
map.insert("category".to_string(), r.category.clone());
map.insert("status".to_string(), format!("{:?}", r.status));
map.insert("message".to_string(), r.message.clone());
if let Some(fix) = &r.fix_hint {
map.insert("fix".to_string(), fix.clone());
}
map
})
.collect();
println!("{}", serde_json::to_string_pretty(&json_results)?);
} else {
for result in results {
if result.status != DiagnosticStatus::Ok {
let icon = match result.status {
DiagnosticStatus::Warning => "⚠️".yellow(),
DiagnosticStatus::Error => "✗".red(),
_ => "✓".green(),
};
println!(" {} {}", icon, result.message);
if let Some(fix) = &result.fix_hint {
println!(" {} {}", "→".dimmed(), fix.bright_cyan());
}
}
}
}
Ok(())
}
fn display_summary(results: &[DiagnosticResult], auto_fixed: bool) -> Result<()> {
let errors = results
.iter()
.filter(|r| r.status == DiagnosticStatus::Error)
.count();
let warnings = results
.iter()
.filter(|r| r.status == DiagnosticStatus::Warning)
.count();
println!("\n{} {}", "📊".bright_cyan(), "Summary".bold());
println!(" Issues found: {}", errors + warnings);
println!(" Errors: {}", errors);
println!(" Warnings: {}", warnings);
if errors == 0 && warnings == 0 {
println!("\n{} Everything configured correctly!", "✅".green());
} else if !auto_fixed {
println!("\n{} {}", "⚡".yellow(), "Quick Fixes".bold());
let mut fix_num = 1;
for result in results {
if result.status != DiagnosticStatus::Ok {
if let Some(fix) = &result.fix_hint {
println!(" {}. {}", fix_num, fix.bright_cyan());
fix_num += 1;
}
}
}
println!("\n Run 'mrapids doctor --fix' to auto-fix issues where possible");
}
Ok(())
}
fn check_decision_logging() -> Vec<DiagnosticResult> {
let mut results = Vec::new();
let decision_log_status = match env::var("MRAPIDS_DECISION_LOG") {
Ok(val) if val == "true" || val == "1" => {
let default_path = dirs::home_dir()
.map(|h| h.join(".mrapids").join("decisions.jsonl"))
.map(|p| p.display().to_string())
.unwrap_or_else(|| "~/.mrapids/decisions.jsonl".to_string());
(
DiagnosticStatus::Ok,
format!("Enabled → {}", default_path),
None,
)
}
Ok(val) if !val.is_empty() => (DiagnosticStatus::Ok, format!("Enabled → {}", val), None),
_ => (
DiagnosticStatus::Ok,
"Disabled (opt-in only)".to_string(),
Some("Set MRAPIDS_DECISION_LOG=true or use --log-decisions flag".to_string()),
),
};
results.push(DiagnosticResult {
category: "Decision Logging".to_string(),
status: decision_log_status.0,
message: decision_log_status.1,
fix_hint: decision_log_status.2,
});
results
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_diagnostic_status_equality() {
assert_eq!(DiagnosticStatus::Ok, DiagnosticStatus::Ok);
assert_eq!(DiagnosticStatus::Warning, DiagnosticStatus::Warning);
assert_eq!(DiagnosticStatus::Error, DiagnosticStatus::Error);
assert_ne!(DiagnosticStatus::Ok, DiagnosticStatus::Error);
}
#[test]
fn test_diagnostic_result_creation() {
let result = DiagnosticResult {
category: "Test".to_string(),
status: DiagnosticStatus::Ok,
message: "Test passed".to_string(),
fix_hint: None,
};
assert_eq!(result.category, "Test");
assert_eq!(result.status, DiagnosticStatus::Ok);
}
#[test]
fn test_diagnostic_result_with_fix_hint() {
let result = DiagnosticResult {
category: "Config".to_string(),
status: DiagnosticStatus::Error,
message: "Missing config".to_string(),
fix_hint: Some("Run mrapids init".to_string()),
};
assert!(result.fix_hint.is_some());
assert_eq!(result.fix_hint.unwrap(), "Run mrapids init");
}
#[test]
fn test_extract_env_vars_curly_brace() {
let content = "base_url: ${API_BASE_URL}";
let vars = extract_env_vars(content);
assert!(vars.contains(&"API_BASE_URL".to_string()));
}
#[test]
fn test_extract_env_vars_with_default() {
let content = "base_url: ${API_BASE_URL:-https://api.example.com}";
let vars = extract_env_vars(content);
assert!(vars.contains(&"API_BASE_URL".to_string()));
}
#[test]
fn test_extract_env_vars_multiple() {
let content = r#"
base_url: ${API_BASE_URL}
headers:
Authorization: Bearer ${API_TOKEN}
X-API-Key: ${API_KEY}
"#;
let vars = extract_env_vars(content);
assert!(vars.contains(&"API_BASE_URL".to_string()));
assert!(vars.contains(&"API_TOKEN".to_string()));
assert!(vars.contains(&"API_KEY".to_string()));
}
#[test]
fn test_extract_env_vars_dollar_sign() {
let content = "token: $AUTH_TOKEN";
let vars = extract_env_vars(content);
assert!(vars.contains(&"AUTH_TOKEN".to_string()));
}
#[test]
fn test_extract_env_vars_empty() {
let content = "base_url: https://api.example.com";
let vars = extract_env_vars(content);
assert!(vars.is_empty());
}
#[test]
fn test_check_project_structure_missing_all() {
let temp_dir = TempDir::new().unwrap();
let results = check_project_structure(temp_dir.path()).unwrap();
let errors: Vec<_> = results
.iter()
.filter(|r| r.status == DiagnosticStatus::Error)
.collect();
assert!(!errors.is_empty());
}
#[test]
fn test_check_project_structure_with_manifest() {
let temp_dir = TempDir::new().unwrap();
let manifest = r#"
name: test-project
version: 1.0.0
default_spec: specs/api.yaml
default_env: development
"#;
fs::write(temp_dir.path().join("mrapids.yaml"), manifest).unwrap();
let results = check_project_structure(temp_dir.path()).unwrap();
let manifest_ok = results
.iter()
.any(|r| r.status == DiagnosticStatus::Ok && r.message.contains("mrapids.yaml"));
assert!(manifest_ok);
}
#[test]
fn test_check_project_structure_with_specs() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("specs")).unwrap();
fs::write(
temp_dir.path().join("specs/api.yaml"),
"openapi: 3.0.0\ninfo:\n title: Test\n version: 1.0.0\npaths: {}",
)
.unwrap();
let results = check_project_structure(temp_dir.path()).unwrap();
let specs_ok = results
.iter()
.any(|r| r.status == DiagnosticStatus::Ok && r.message.contains("specs/"));
assert!(specs_ok);
}
#[test]
fn test_check_project_structure_with_config() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("config")).unwrap();
fs::write(
temp_dir.path().join("config/default.yaml"),
"base_url: http://localhost",
)
.unwrap();
fs::write(
temp_dir.path().join("config/development.yaml"),
"base_url: http://localhost",
)
.unwrap();
let results = check_project_structure(temp_dir.path()).unwrap();
let config_ok = results
.iter()
.any(|r| r.status == DiagnosticStatus::Ok && r.message.contains("config/"));
assert!(config_ok);
}
#[test]
fn test_check_project_structure_with_env() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("env")).unwrap();
fs::write(
temp_dir.path().join("env/.env.development"),
"API_TOKEN=test",
)
.unwrap();
let results = check_project_structure(temp_dir.path()).unwrap();
let env_ok = results
.iter()
.any(|r| r.status == DiagnosticStatus::Ok && r.message.contains("env/"));
assert!(env_ok);
}
#[test]
fn test_check_configuration_missing_default() {
let temp_dir = TempDir::new().unwrap();
let results = check_configuration(temp_dir.path()).unwrap();
let has_error = results.iter().any(|r| {
r.status == DiagnosticStatus::Error && r.message.contains("default.yaml missing")
});
assert!(has_error);
}
#[test]
fn test_check_configuration_with_base_url() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("config")).unwrap();
fs::write(
temp_dir.path().join("config/default.yaml"),
"base_url: https://api.example.com",
)
.unwrap();
let results = check_configuration(temp_dir.path()).unwrap();
let has_ok = results
.iter()
.any(|r| r.status == DiagnosticStatus::Ok && r.message.contains("base_url"));
assert!(has_ok);
}
#[test]
fn test_check_configuration_missing_base_url() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("config")).unwrap();
fs::write(
temp_dir.path().join("config/default.yaml"),
"timeout: 30000",
)
.unwrap();
let results = check_configuration(temp_dir.path()).unwrap();
let has_warning = results.iter().any(|r| {
r.status == DiagnosticStatus::Warning && r.message.contains("missing base_url")
});
assert!(has_warning);
}
#[test]
fn test_check_openapi_spec_not_found() {
let temp_dir = TempDir::new().unwrap();
let results = check_openapi_spec(temp_dir.path()).unwrap();
let has_error = results
.iter()
.any(|r| r.status == DiagnosticStatus::Error && r.message.contains("No OpenAPI"));
assert!(has_error);
}
#[test]
fn test_check_openapi_spec_valid_openapi() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("specs")).unwrap();
fs::write(
temp_dir.path().join("specs/api.yaml"),
"openapi: 3.0.0\ninfo:\n title: Test\n version: 1.0.0",
)
.unwrap();
let results = check_openapi_spec(temp_dir.path()).unwrap();
let has_ok = results
.iter()
.any(|r| r.status == DiagnosticStatus::Ok && r.message.contains("Valid OpenAPI"));
assert!(has_ok);
}
#[test]
fn test_check_openapi_spec_valid_swagger() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("specs")).unwrap();
fs::write(
temp_dir.path().join("specs/api.yaml"),
"swagger: \"2.0\"\ninfo:\n title: Test\n version: 1.0.0",
)
.unwrap();
let results = check_openapi_spec(temp_dir.path()).unwrap();
let has_ok = results
.iter()
.any(|r| r.status == DiagnosticStatus::Ok && r.message.contains("Valid Swagger"));
assert!(has_ok);
}
#[test]
fn test_check_authentication_with_bearer_in_spec() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("specs")).unwrap();
fs::write(
temp_dir.path().join("specs/api.yaml"),
r#"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
"#,
)
.unwrap();
let results = check_authentication(temp_dir.path(), "development").unwrap();
let has_bearer = results.iter().any(|r| r.message.contains("Bearer Token"));
assert!(has_bearer);
}
#[test]
fn test_check_authentication_with_api_key_in_spec() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("specs")).unwrap();
fs::write(
temp_dir.path().join("specs/api.yaml"),
r#"
openapi: 3.0.0
info:
title: Test
version: 1.0.0
components:
securitySchemes:
apiKey:
type: apiKey
in: header
name: X-API-Key
"#,
)
.unwrap();
let results = check_authentication(temp_dir.path(), "development").unwrap();
let has_api_key = results.iter().any(|r| r.message.contains("API Key"));
assert!(has_api_key);
}
#[test]
fn test_check_env_consistency_no_vars() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("config")).unwrap();
fs::write(
temp_dir.path().join("config/development.yaml"),
"base_url: https://api.example.com",
)
.unwrap();
let results = check_env_consistency(temp_dir.path(), "development").unwrap();
let has_ok = results.iter().any(|r| {
r.status == DiagnosticStatus::Ok && r.message.contains("No environment variables")
});
assert!(has_ok);
}
#[test]
fn test_check_env_consistency_with_defined_vars() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("config")).unwrap();
fs::write(
temp_dir.path().join("config/development.yaml"),
"base_url: ${API_URL}",
)
.unwrap();
fs::create_dir_all(temp_dir.path().join("env")).unwrap();
fs::write(
temp_dir.path().join("env/.env.development"),
"API_URL=https://api.example.com",
)
.unwrap();
let results = check_env_consistency(temp_dir.path(), "development").unwrap();
let has_ok = results.iter().any(|r| {
r.status == DiagnosticStatus::Ok && r.message.contains("variables are defined")
});
assert!(has_ok);
}
#[test]
fn test_check_env_consistency_with_missing_vars() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path().join("config")).unwrap();
fs::write(
temp_dir.path().join("config/development.yaml"),
"base_url: ${MISSING_VAR}",
)
.unwrap();
fs::create_dir_all(temp_dir.path().join("env")).unwrap();
fs::write(
temp_dir.path().join("env/.env.development"),
"OTHER_VAR=value",
)
.unwrap();
let results = check_env_consistency(temp_dir.path(), "development").unwrap();
let has_error = results
.iter()
.any(|r| r.status == DiagnosticStatus::Error && r.message.contains("missing"));
assert!(has_error);
}
#[test]
fn test_full_project_all_ok() {
let temp_dir = TempDir::new().unwrap();
let manifest = r#"
name: test-project
version: 1.0.0
default_spec: specs/api.yaml
default_env: development
"#;
fs::write(temp_dir.path().join("mrapids.yaml"), manifest).unwrap();
fs::create_dir_all(temp_dir.path().join("specs")).unwrap();
fs::write(
temp_dir.path().join("specs/api.yaml"),
"openapi: 3.0.0\ninfo:\n title: Test\n version: 1.0.0\npaths: {}",
)
.unwrap();
fs::create_dir_all(temp_dir.path().join("config")).unwrap();
fs::write(
temp_dir.path().join("config/default.yaml"),
"base_url: https://api.example.com",
)
.unwrap();
fs::write(
temp_dir.path().join("config/development.yaml"),
"base_url: https://dev.api.example.com",
)
.unwrap();
fs::create_dir_all(temp_dir.path().join("env")).unwrap();
fs::write(
temp_dir.path().join("env/.env.development"),
"# Development env",
)
.unwrap();
let mut all_results = Vec::new();
all_results.extend(check_project_structure(temp_dir.path()).unwrap());
all_results.extend(check_configuration(temp_dir.path()).unwrap());
all_results.extend(check_openapi_spec(temp_dir.path()).unwrap());
let error_count = all_results
.iter()
.filter(|r| r.status == DiagnosticStatus::Error)
.count();
assert_eq!(
error_count,
0,
"Expected no errors but found: {:?}",
all_results
.iter()
.filter(|r| r.status == DiagnosticStatus::Error)
.collect::<Vec<_>>()
);
}
}