bijux-cli 0.3.6

Command-line runtime for automation, plugin-driven tools, and interactive workflows with structured output.
Documentation
#![forbid(unsafe_code)]

use super::error::ConfigError;

pub(crate) fn normalize_key(raw: &str) -> Result<String, ConfigError> {
    let key = raw.trim();
    if key.is_empty() {
        return Err(ConfigError::validation("Key cannot be empty"));
    }
    if !key.is_ascii() {
        return Err(ConfigError::validation(
            "Non-ASCII characters are not allowed in keys or values.",
        ));
    }
    if key.contains('.') {
        return Err(ConfigError::validation(format!("Unknown config section in key: {key}")));
    }

    let normalized = key
        .strip_prefix("BIJUXCLI_")
        .or_else(|| key.strip_prefix("BIJUX_"))
        .unwrap_or(key)
        .to_ascii_lowercase();
    if !normalized.chars().all(|ch| ch.is_ascii_alphanumeric() || ch == '_') {
        return Err(ConfigError::validation(
            "Invalid key: only alphanumerics and underscore allowed.",
        ));
    }

    Ok(normalized)
}

pub(crate) fn validate_value(value: &str) -> Result<(), ConfigError> {
    if !value.is_ascii() {
        return Err(ConfigError::validation(
            "Non-ASCII characters are not allowed in keys or values.",
        ));
    }
    if value.chars().any(|ch| matches!(ch, '\r' | '\n' | '\t' | '\u{000B}' | '\u{000C}')) {
        return Err(ConfigError::validation(
            "Control characters are not allowed in config values.",
        ));
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::{normalize_key, validate_value};

    #[test]
    fn key_rejects_empty_and_whitespace_only() {
        let empty = normalize_key("").expect_err("empty key should fail validation");
        let spaces = normalize_key("   ").expect_err("whitespace key should fail validation");
        assert!(empty.to_string().contains("Key cannot be empty"));
        assert!(spaces.to_string().contains("Key cannot be empty"));
    }

    #[test]
    fn key_accepts_lower_mixed_underscore_and_alphanumeric() {
        assert_eq!(normalize_key("alpha").expect("lower"), "alpha");
        assert_eq!(normalize_key("MixedCase").expect("mixed"), "mixedcase");
        assert_eq!(normalize_key("BIJUXCLI_ALPHA").expect("prefix"), "alpha");
        assert_eq!(normalize_key("BIJUX_ALPHA").expect("legacy prefix"), "alpha");
        assert_eq!(normalize_key("_").expect("underscore"), "_");
        assert_eq!(normalize_key("a1b2").expect("alphanumeric"), "a1b2");
    }

    #[test]
    fn key_rejects_invalid_punctuation_dots_dashes_and_non_ascii() {
        let punctuation = normalize_key("bad!key").expect_err("punctuation should be rejected");
        let dotted = normalize_key("group.key").expect_err("dot should be rejected");
        let dashed = normalize_key("bad-key").expect_err("dash should be rejected");
        let non_ascii = normalize_key("näme").expect_err("non-ascii should be rejected");
        assert!(punctuation.to_string().contains("Invalid key"));
        assert!(dotted.to_string().contains("Unknown config section"));
        assert!(dashed.to_string().contains("Invalid key"));
        assert!(non_ascii.to_string().contains("Non-ASCII"));
    }

    #[test]
    fn value_accepts_ascii_empty_and_spaces() {
        assert!(validate_value("plain-ascii").is_ok());
        assert!(validate_value("").is_ok());
        assert!(validate_value("value with spaces").is_ok());
    }

    #[test]
    fn value_rejects_newline_tab_and_control_characters() {
        assert!(validate_value("line\nbreak").is_err());
        assert!(validate_value("tab\tvalue").is_err());
        assert!(validate_value("vert\u{000B}tab").is_err());
        assert!(validate_value("accent-é").is_err(), "non-ascii values should be rejected");
    }
}