hl7v2-server 1.3.0

HTTP/REST API server for HL7v2 message processing
//! HL7v2 HTTP/REST API server binary.

use hl7v2_server::{Server, ServerConfig};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let command = parse_args(std::env::args().skip(1))
        .map_err(|message| std::io::Error::new(std::io::ErrorKind::InvalidInput, message))?;
    if command == ServerCommand::Help {
        println!("{}", usage());
        return Ok(());
    }

    let config = ServerConfig::from_env()?;
    if command == ServerCommand::PrintConfig {
        println!(
            "{}",
            serde_json::to_string_pretty(&config.to_public_config())?
        );
        return Ok(());
    }

    // Initialize tracing/logging
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "hl7v2_server=info,tower_http=debug".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    tracing::info!("Starting HL7v2 HTTP server");
    tracing::info!("Bind address: {}", config.bind_address);
    if config.api_key.is_some() {
        tracing::info!("API key authentication enabled");
    } else {
        tracing::warn!("API key authentication disabled (public access enabled)");
    }
    tracing::info!("CORS allowed origins: {:?}", config.cors_allowed_origins);

    // Create and run server
    let server = Server::new(config);

    server.serve().await?;

    Ok(())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ServerCommand {
    Serve,
    PrintConfig,
    Help,
}

fn parse_args<I, S>(args: I) -> Result<ServerCommand, String>
where
    I: IntoIterator<Item = S>,
    S: AsRef<str>,
{
    let mut command = ServerCommand::Serve;

    for arg in args {
        match arg.as_ref() {
            "--print-config" => command = ServerCommand::PrintConfig,
            "-h" | "--help" => command = ServerCommand::Help,
            unknown => {
                return Err(format!(
                    "unknown argument '{unknown}'. Run hl7v2-server --help for usage."
                ));
            }
        }
    }

    Ok(command)
}

fn usage() -> &'static str {
    "Usage: hl7v2-server [--print-config]\n\nOptions:\n  --print-config  Print sanitized effective server configuration as JSON and exit\n  -h, --help      Print help\n\nEnvironment:\n  HL7V2_CONFIG                Optional TOML/YAML config file with [server], [ack], and [quarantine] settings\n  BIND_ADDRESS                Override bind address, for example 0.0.0.0:8080\n  HL7V2_API_KEY               API key for protected /hl7/* routes\n  HL7V2_CORS_ALLOWED_ORIGINS  Comma-separated CORS origins, or * for any\n  HL7V2_PROFILE_PATHS         Profile files that must load before readiness passes\n  HL7V2_BUNDLE_OUTPUT_ROOT    Existing writable directory for server-generated evidence bundles"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_args_defaults_to_serve() {
        assert_eq!(
            parse_args(std::iter::empty::<&str>()),
            Ok(ServerCommand::Serve)
        );
    }

    #[test]
    fn parse_args_accepts_print_config() {
        assert_eq!(
            parse_args(["--print-config"]),
            Ok(ServerCommand::PrintConfig)
        );
    }

    #[test]
    fn parse_args_accepts_help() {
        assert_eq!(parse_args(["--help"]), Ok(ServerCommand::Help));
        assert_eq!(parse_args(["-h"]), Ok(ServerCommand::Help));
    }

    #[test]
    fn parse_args_rejects_unknown_arguments() {
        assert!(matches!(
            parse_args(["--unknown"]),
            Err(error) if error.contains("unknown argument")
        ));
    }
}