road-runner-common 0.11.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
//! Standard startup banner for CEX services.

use std::{env, io::Write};

const BANNER: &str = include_str!("../assets/startup_banner.txt");
const VINU_PURPLE: &str = "\x1b[38;2;180;124;255m";
const RESET: &str = "\x1b[0m";

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct StartupBanner<'a> {
    pub service_name: &'a str,
    pub description: &'a str,
    pub version: &'a str,
    pub environment: &'a str,
}

pub fn runtime_environment() -> String {
    ["APP_ENV", "RUST_ENV", "ENVIRONMENT", "NODE_ENV"]
        .iter()
        .find_map(|key| env::var(key).ok().filter(|value| !value.trim().is_empty()))
        .unwrap_or_else(|| "production".to_string())
}

pub fn print(config: StartupBanner<'_>) {
    let stdout = std::io::stdout();
    let mut output = stdout.lock();
    let _ = write(&mut output, config);
}

fn write(mut output: impl Write, config: StartupBanner<'_>) -> std::io::Result<()> {
    if !env_flag_enabled("STARTUP_BANNER", true) {
        return Ok(());
    }

    let color = color_enabled();
    write_banner(&mut output, color)?;
    writeln!(output)?;

    if color {
        write!(output, "{RESET}")?;
    }

    writeln!(output, "Service     : {}", config.service_name)?;
    writeln!(output, "Description : {}", config.description)?;
    writeln!(output, "Version     : {}", config.version)?;
    write!(output, "Environment : {}", config.environment)?;
    writeln!(output)
}

fn write_banner(mut output: impl Write, color: bool) -> std::io::Result<()> {
    for line in BANNER.trim_matches('\n').lines() {
        if color {
            write!(output, "{VINU_PURPLE}")?;
        }
        writeln!(output, "{line}")?;
    }

    Ok(())
}

fn color_enabled() -> bool {
    if env::var_os("NO_COLOR").is_some() {
        return false;
    }

    !env::var("LOG_FORMAT")
        .map(|format| format.eq_ignore_ascii_case("json"))
        .unwrap_or(false)
}

fn env_flag_enabled(name: &str, default: bool) -> bool {
    match env::var(name) {
        Ok(value) => !matches!(
            value.trim().to_ascii_lowercase().as_str(),
            "0" | "false" | "off" | "no"
        ),
        Err(_) => default,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::{Mutex, OnceLock};

    fn env_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    fn clear_env() {
        for key in [
            "APP_ENV",
            "RUST_ENV",
            "ENVIRONMENT",
            "NODE_ENV",
            "STARTUP_BANNER",
            "NO_COLOR",
            "LOG_FORMAT",
        ] {
            env::remove_var(key);
        }
    }

    fn config() -> StartupBanner<'static> {
        StartupBanner {
            service_name: "cex-test",
            description: "Test service",
            version: "1.2.3",
            environment: "test",
        }
    }

    #[test]
    fn environment_uses_priority_order_and_default() {
        let _guard = env_lock().lock().unwrap();
        clear_env();

        assert_eq!(runtime_environment(), "production");
        env::set_var("NODE_ENV", "node");
        env::set_var("ENVIRONMENT", "environment");
        env::set_var("RUST_ENV", "rust");
        env::set_var("APP_ENV", "app");
        assert_eq!(runtime_environment(), "app");
    }

    #[test]
    fn disabled_banner_writes_nothing() {
        let _guard = env_lock().lock().unwrap();
        clear_env();
        env::set_var("STARTUP_BANNER", "no");

        let mut output = Vec::new();
        write(&mut output, config()).unwrap();

        assert!(output.is_empty());
    }

    #[test]
    fn banner_contains_metadata_without_log_prefixes() {
        let _guard = env_lock().lock().unwrap();
        clear_env();
        env::set_var("NO_COLOR", "1");

        let mut output = Vec::new();
        write(&mut output, config()).unwrap();
        let output = String::from_utf8(output).unwrap();

        assert!(output.starts_with(BANNER));
        assert!(output.contains("\n\nService     : cex-test\n"));
        assert!(output.contains("Description : Test service\n"));
        assert!(output.contains("Version     : 1.2.3\n"));
        assert!(output.ends_with("Environment : test\n"));
        assert!(!output.contains("ThreadId"));
        assert!(!output.contains(" INFO "));
    }

    #[test]
    fn banner_uses_vinu_purple_by_default() {
        let _guard = env_lock().lock().unwrap();
        clear_env();

        let mut output = Vec::new();
        write(&mut output, config()).unwrap();
        let output = String::from_utf8(output).unwrap();

        assert!(output.starts_with(VINU_PURPLE));
        assert_eq!(
            BANNER.trim_matches('\n').lines().count(),
            output.matches(VINU_PURPLE).count()
        );
        assert!(output.contains(&format!("{RESET}Service")));
        assert!(!output.ends_with("\x1b[0m\n"));
    }

    #[test]
    fn json_output_disables_color() {
        let _guard = env_lock().lock().unwrap();
        clear_env();
        env::set_var("LOG_FORMAT", "JSON");

        let mut output = Vec::new();
        write(&mut output, config()).unwrap();

        assert!(!String::from_utf8(output).unwrap().contains("\x1b["));
    }
}