hl7v2-server 1.4.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(());
    }

    init_tracing();

    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\n  RUST_LOG                    tracing filter, for example hl7v2_server=info,tower_http=debug\n  RUST_LOG_FORMAT             set to json for JSON logs; any other value uses text logs"
}

fn init_tracing() {
    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| "hl7v2_server=info,tower_http=debug".into());
    let subscriber = tracing_subscriber::registry().with(env_filter);

    match log_format_from_env() {
        LogFormat::Json => subscriber
            .with(tracing_subscriber::fmt::layer().json())
            .init(),
        LogFormat::Text => subscriber.with(tracing_subscriber::fmt::layer()).init(),
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LogFormat {
    Text,
    Json,
}

fn log_format_from_env() -> LogFormat {
    std::env::var("RUST_LOG_FORMAT")
        .ok()
        .and_then(|value| parse_log_format(&value))
        .unwrap_or(LogFormat::Text)
}

fn parse_log_format(value: &str) -> Option<LogFormat> {
    match value.trim().to_ascii_lowercase().as_str() {
        "" | "text" | "pretty" => Some(LogFormat::Text),
        "json" => Some(LogFormat::Json),
        _ => None,
    }
}

#[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")
        ));
    }

    #[test]
    fn parse_log_format_accepts_json_and_text_values() {
        assert_eq!(parse_log_format("json"), Some(LogFormat::Json));
        assert_eq!(parse_log_format(" JSON "), Some(LogFormat::Json));
        assert_eq!(parse_log_format("text"), Some(LogFormat::Text));
        assert_eq!(parse_log_format("pretty"), Some(LogFormat::Text));
        assert_eq!(parse_log_format(""), Some(LogFormat::Text));
        assert_eq!(parse_log_format("xml"), None);
    }
}