road-runner-common 0.29.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(()))
    }

    /// Take the env lock, recovering it if a previous test panicked while
    /// holding it.
    ///
    /// These tests share the process environment, so they serialize on one
    /// mutex. With a plain `.unwrap()` the first genuine failure poisons that
    /// mutex and every later test dies with `PoisonError` — one broken
    /// assertion is then reported as five, and the four bystanders bury the
    /// real one. That is exactly how the `starts_with(BANNER)` bug below stayed
    /// unread.
    ///
    /// Recovering is sound here because the lock guards no invariant of its
    /// own: it only serializes access, and every test calls `clear_env()`
    /// before it reads anything.
    fn lock_env() -> std::sync::MutexGuard<'static, ()> {
        env_lock().lock().unwrap_or_else(|poisoned| poisoned.into_inner())
    }

    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 = lock_env();
        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 = lock_env();
        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 = lock_env();
        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();

        // `BANNER` is the raw asset, which is padded with blank lines at both
        // ends (`\n\n` … `\n\n\n`). `write_banner` strips that padding on
        // purpose, so the output can never start with the untrimmed constant —
        // this assertion has been false since the module was written. Assert
        // what the code promises: the banner, without the file's padding.
        assert!(
            output.starts_with(BANNER.trim_matches('\n')),
            "the banner should be printed with the asset's blank-line padding stripped"
        );
        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 = lock_env();
        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 = lock_env();
        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["));
    }
}