use std::net::SocketAddr;
use std::path::{Path, PathBuf};
use clap::{Parser, Subcommand};
use tracing::{error, info};
use tracing_subscriber::util::SubscriberInitExt;
use ferro_hgvs::service::{create_app, spawn_health_check_task, ServiceConfig};
#[derive(Parser)]
#[command(name = "ferro-web")]
#[command(about = "Multi-tool HGVS variant normalization web service")]
#[command(version)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Serve {
#[arg(short, long, default_value = "config/service.toml")]
config: PathBuf,
#[arg(long)]
host: Option<String>,
#[arg(short, long)]
port: Option<u16>,
#[arg(long, default_value = "info")]
log_level: String,
#[arg(long)]
json_logs: bool,
#[arg(long)]
open: bool,
},
Config {
#[arg(short, long, default_value = "config/service.toml")]
output: PathBuf,
#[arg(long)]
force: bool,
},
Check {
#[arg(short, long, default_value = "config/service.toml")]
config: PathBuf,
},
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
match cli.command {
Commands::Serve {
config,
host,
port,
log_level,
json_logs,
open,
} => serve_command(config, host, port, log_level, json_logs, open).await,
Commands::Config { output, force } => config_command(output, force).await,
Commands::Check { config } => check_command(config).await,
}
}
async fn serve_command(
config_path: PathBuf,
host_override: Option<String>,
port_override: Option<u16>,
log_level: String,
json_logs: bool,
open_browser: bool,
) -> Result<(), Box<dyn std::error::Error>> {
init_tracing(&log_level, json_logs)?;
info!("Starting ferro-hgvs web service");
let mut config = load_or_create_config(&config_path).await?;
if let Some(host) = host_override {
config.server.host = host;
}
if let Some(port) = port_override {
config.server.port = port;
}
if let Err(e) = config.validate() {
error!("Configuration validation failed: {}", e);
return Err(e.into());
}
info!("Configuration loaded successfully");
info!("Enabled tools: {:?}", config.enabled_tools());
let (app, state) = create_app(config.clone())?;
spawn_health_check_task(state);
let addr = SocketAddr::new(config.server.host.parse()?, config.server.port);
info!("Starting server on http://{}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await?;
let browser_host = if config.server.host == "0.0.0.0" {
"localhost"
} else {
&config.server.host
};
let url = format!("http://{}:{}", browser_host, config.server.port);
#[cfg(debug_assertions)]
{
info!("Ferro HGVS web service running on {}", url);
info!("API documentation available at {}/", url);
info!("Health check available at {}/health", url);
}
#[cfg(not(debug_assertions))]
{
info!("Ferro HGVS web service running on {}", url);
info!("API documentation available at {}/", url);
info!("Health check available at {}/health", url);
}
if open_browser {
if let Err(e) = open::that(&url) {
error!("Failed to open browser: {}", e);
}
}
axum::serve(listener, app).await?;
Ok(())
}
async fn config_command(
output_path: PathBuf,
force: bool,
) -> Result<(), Box<dyn std::error::Error>> {
if output_path.exists() && !force {
error!(
"Configuration file already exists: {}",
output_path.display()
);
error!("Use --force to overwrite");
std::process::exit(1);
}
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent)?;
}
let config = ServiceConfig::default();
config.to_file(&output_path)?;
println!(
"Sample configuration file created: {}",
output_path.display()
);
println!("Edit the file to configure your tools and settings");
Ok(())
}
async fn check_command(config_path: PathBuf) -> Result<(), Box<dyn std::error::Error>> {
println!("Checking configuration and tool availability...");
let config = load_or_create_config(&config_path).await?;
match config.validate() {
Ok(()) => println!("Configuration is valid"),
Err(e) => {
println!("Configuration validation failed: {}", e);
return Err(e.into());
}
}
println!("\nTool Status:");
let enabled_tools = config.enabled_tools();
if enabled_tools.is_empty() {
println!("WARNING: No tools are enabled");
return Ok(());
}
match create_app(config) {
Ok((_, state)) => {
println!("Application created successfully");
let health_results = state.tool_manager.health_check_all().await;
for (tool_name, result) in health_results {
use ferro_hgvs::service::types::health_check::HealthCheckResult;
match result {
HealthCheckResult::Healthy => println!(" OK {}: Available", tool_name),
HealthCheckResult::Degraded { reason } => {
println!(" WARN {}: Degraded - {}", tool_name, reason)
}
HealthCheckResult::Unhealthy { reason } => {
println!(" ERROR {}: {}", tool_name, reason)
}
}
}
}
Err(e) => {
println!("Failed to initialize application: {}", e);
return Err(e.into());
}
}
println!("\nHealth check completed");
Ok(())
}
async fn load_or_create_config(
config_path: &Path,
) -> Result<ServiceConfig, Box<dyn std::error::Error>> {
if config_path.exists() {
info!("Loading configuration from {}", config_path.display());
Ok(ServiceConfig::from_file(config_path)?)
} else {
info!("Configuration file not found, using defaults");
println!(
"WARNING: Configuration file not found: {}",
config_path.display()
);
println!("TIP: Run 'ferro-web config' to generate a sample configuration file");
Ok(ServiceConfig::default())
}
}
fn init_tracing(level: &str, _json_logs: bool) -> Result<(), Box<dyn std::error::Error>> {
use tracing_subscriber::{fmt, layer::SubscriberExt, EnvFilter};
let filter =
EnvFilter::try_new(level).map_err(|e| format!("Invalid log level '{}': {}", level, e))?;
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer())
.init();
info!("Tracing initialized with level: {}", level);
Ok(())
}