camel-config 0.18.0

Configuration and route discovery for rust-camel
Documentation
use camel_api::CamelError;
use tracing_subscriber::EnvFilter;

pub(crate) fn is_raw_env_filter(s: &str) -> bool {
    s.contains(',') || s.contains('=') || s.contains('[') || s.contains("::")
}

pub(crate) fn expand_bare_level(level: &str) -> Option<&'static str> {
    match level.to_lowercase().as_str() {
        "trace" => Some("warn,camel=trace,camel_tracer=off"),
        "debug" => Some("warn,camel=debug,camel_tracer=off"),
        "info" => Some("warn,camel=info,camel_tracer=off"),
        "warn" | "warning" => Some("warn,camel=warn,camel_tracer=off"),
        "error" => Some("error,camel=error,camel_tracer=off"),
        "off" => Some("off"),
        _ => None,
    }
}

pub(crate) fn inject_camel_tracer_off(filter_str: &str) -> String {
    if filter_str.contains("camel_tracer") {
        filter_str.to_string()
    } else {
        format!("{filter_str},camel_tracer=off")
    }
}

pub(crate) fn build_env_filter(
    log_level: &str,
    rust_log: Option<&str>,
) -> Result<EnvFilter, CamelError> {
    if let Some(rust_log_val) = rust_log {
        let filter_str = inject_camel_tracer_off(rust_log_val);
        return EnvFilter::try_new(&filter_str).map_err(|e| {
            CamelError::Config(format!(
                "Invalid RUST_LOG environment variable '{}': {e}",
                rust_log_val
            ))
        });
    }

    let filter_str = if is_raw_env_filter(log_level) {
        inject_camel_tracer_off(log_level)
    } else {
        match expand_bare_level(log_level) {
            Some(expanded) => expanded.to_string(),
            None => {
                return Err(CamelError::Config(format!(
                    "Unknown log level '{}' in Camel.toml. Expected: trace, debug, info, warn, error, off, or a full EnvFilter directive.",
                    log_level
                )));
            }
        }
    };

    EnvFilter::try_new(&filter_str).map_err(|e| {
        CamelError::Config(format!(
            "Invalid log_level '{}' in Camel.toml: {e}",
            log_level
        ))
    })
}

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

    #[test]
    fn build_env_filter_bare_levels_expand_correctly() {
        let f = build_env_filter("trace", None).unwrap();
        let s = f.to_string().replace(' ', "");
        assert!(s.contains("camel=trace"), "trace: {s}");

        let f = build_env_filter("debug", None).unwrap();
        assert!(f.to_string().contains("camel=debug"));

        let f = build_env_filter("info", None).unwrap();
        assert!(f.to_string().contains("camel=info"));

        let f = build_env_filter("warn", None).unwrap();
        assert!(f.to_string().contains("camel=warn"));

        let f = build_env_filter("error", None).unwrap();
        assert!(f.to_string().contains("camel=error"));
    }

    #[test]
    fn build_env_filter_unknown_bare_level_returns_error() {
        let result = build_env_filter("infp", None);
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("Unknown log level"), "msg: {msg}");
    }

    #[test]
    fn build_env_filter_raw_directive_parses_as_is() {
        let f = build_env_filter("warn,camel=info,sqlx=warn", None).unwrap();
        let s = f.to_string().replace(' ', "");
        assert!(s.contains("camel=info"));
        assert!(s.contains("sqlx=warn"));
        assert!(s.contains("camel_tracer=off"));
    }

    #[test]
    fn build_env_filter_raw_directive_preserves_user_camel_tracer() {
        let f = build_env_filter("info,camel_tracer=debug", None).unwrap();
        let s = f.to_string().replace(' ', "");
        assert!(s.contains("camel_tracer=debug"));
        assert!(!s.contains("camel_tracer=off"));
    }

    #[test]
    fn build_env_filter_invalid_returns_config_error() {
        let result = build_env_filter("info,bad===value", None);
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("Invalid log_level"));
    }

    #[test]
    fn build_env_filter_is_case_insensitive() {
        let f = build_env_filter("TRACE", None).unwrap();
        assert!(f.to_string().contains("camel=trace"));

        let f = build_env_filter("DeBuG", None).unwrap();
        assert!(f.to_string().contains("camel=debug"));
    }

    #[test]
    fn build_env_filter_empty_string_returns_error() {
        let result = build_env_filter("", None);
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("Unknown log level"), "msg: {msg}");
    }

    #[test]
    fn build_env_filter_warning_alias_expands_to_warn() {
        let f = build_env_filter("warning", None).unwrap();
        assert!(f.to_string().contains("camel=warn"));
    }

    #[test]
    fn build_env_filter_rust_log_overrides_config() {
        let f = build_env_filter("error", Some("my_crate=debug")).unwrap();
        let s = f.to_string().replace(' ', "");
        assert!(s.contains("my_crate=debug"));
        assert!(s.contains("camel_tracer=off"));
    }

    #[test]
    fn build_env_filter_rust_log_invalid_returns_config_error() {
        let result = build_env_filter("info", Some("bad===value"));
        assert!(result.is_err());
        let msg = result.unwrap_err().to_string();
        assert!(msg.contains("Invalid RUST_LOG"));
    }

    #[test]
    fn build_env_filter_rust_log_preserves_user_camel_tracer() {
        let f = build_env_filter("error", Some("debug,camel_tracer=info")).unwrap();
        let s = f.to_string().replace(' ', "");
        assert!(s.contains("camel_tracer=info"));
        assert!(!s.contains("camel_tracer=off"));
    }

    #[test]
    fn build_env_filter_bare_levels_include_warn_default_and_camel_tracer_off() {
        let f = build_env_filter("debug", None).unwrap();
        let s = f.to_string();
        assert!(s.contains("warn"), "expected warn global default: {s}");
        assert!(
            s.contains("camel_tracer=off"),
            "expected camel_tracer silenced: {s}"
        );
    }

    #[test]
    fn build_env_filter_off_bare_level() {
        let f = build_env_filter("off", None).unwrap();
        let s = f.to_string().replace(' ', "");
        assert_eq!(s, "off");
    }
}