bamboo-server 2026.5.3

HTTP server and API layer for the Bamboo agent framework
Documentation
use crate::error::AppError;
use bamboo_infrastructure::keyword_masking::{KeywordEntry, KeywordMaskingConfig};

use super::{
    constants::{MAX_ENTRIES, MAX_PATTERN_LENGTH},
    types::{map_validation_errors, ValidationError},
};

pub(super) fn build_validated_config(
    entries: Vec<KeywordEntry>,
) -> Result<KeywordMaskingConfig, AppError> {
    validate_entry_limits(&entries)?;

    let config = KeywordMaskingConfig { entries };
    if let Err(errors) = config.validate() {
        return Err(AppError::BadRequest(format!(
            "Validation failed: {:?}",
            map_validation_errors(errors)
        )));
    }

    Ok(config)
}

pub(super) fn validate_entries_only(
    entries: Vec<KeywordEntry>,
) -> Result<(), Vec<ValidationError>> {
    let config = KeywordMaskingConfig { entries };
    config.validate().map_err(map_validation_errors)
}

fn validate_entry_limits(entries: &[KeywordEntry]) -> Result<(), AppError> {
    if entries.len() > MAX_ENTRIES {
        return Err(AppError::BadRequest(format!(
            "Too many entries: {} (max {})",
            entries.len(),
            MAX_ENTRIES
        )));
    }

    for (idx, entry) in entries.iter().enumerate() {
        if entry.pattern.len() > MAX_PATTERN_LENGTH {
            return Err(AppError::BadRequest(format!(
                "Pattern at index {} too long: {} chars (max {})",
                idx,
                entry.pattern.len(),
                MAX_PATTERN_LENGTH
            )));
        }
    }

    Ok(())
}

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

    #[test]
    fn test_validate_entries_with_empty_list() {
        let entries = vec![];
        let result = validate_entries_only(entries);
        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_entries_with_valid_entry() {
        let entries = vec![KeywordEntry {
            pattern: "test".to_string(),
            match_type: MatchType::Exact,
            enabled: true,
        }];
        let result = validate_entries_only(entries);
        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_entries_with_invalid_regex() {
        let entries = vec![KeywordEntry {
            pattern: "[invalid(regex".to_string(),
            match_type: MatchType::Regex,
            enabled: true,
        }];
        let result = validate_entries_only(entries);
        assert!(result.is_err());
        let errors = result.unwrap_err();
        assert!(!errors.is_empty());
    }

    #[test]
    fn test_validate_entry_limits_with_too_many_entries() {
        let entries: Vec<KeywordEntry> = (0..=MAX_ENTRIES)
            .map(|i| KeywordEntry {
                pattern: format!("pattern{}", i),
                match_type: MatchType::Exact,
                enabled: true,
            })
            .collect();
        let result = validate_entry_limits(&entries);
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_entry_limits_with_max_entries() {
        let entries: Vec<KeywordEntry> = (0..MAX_ENTRIES)
            .map(|i| KeywordEntry {
                pattern: format!("pattern{}", i),
                match_type: MatchType::Exact,
                enabled: true,
            })
            .collect();
        let result = validate_entry_limits(&entries);
        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_entry_limits_with_pattern_too_long() {
        let entries = vec![KeywordEntry {
            pattern: "a".repeat(MAX_PATTERN_LENGTH + 1),
            match_type: MatchType::Exact,
            enabled: true,
        }];
        let result = validate_entry_limits(&entries);
        assert!(result.is_err());
    }

    #[test]
    fn test_validate_entry_limits_with_max_pattern_length() {
        let entries = vec![KeywordEntry {
            pattern: "a".repeat(MAX_PATTERN_LENGTH),
            match_type: MatchType::Exact,
            enabled: true,
        }];
        let result = validate_entry_limits(&entries);
        assert!(result.is_ok());
    }

    #[test]
    fn test_build_validated_config_with_empty_entries() {
        let entries = vec![];
        let result = build_validated_config(entries);
        assert!(result.is_ok());
        let config = result.unwrap();
        assert!(config.entries.is_empty());
    }

    #[test]
    fn test_build_validated_config_with_valid_entries() {
        let entries = vec![
            KeywordEntry {
                pattern: "secret1".to_string(),
                match_type: MatchType::Exact,
                enabled: true,
            },
            KeywordEntry {
                pattern: r"\d{4}".to_string(),
                match_type: MatchType::Regex,
                enabled: true,
            },
        ];
        let result = build_validated_config(entries);
        assert!(result.is_ok());
        let config = result.unwrap();
        assert_eq!(config.entries.len(), 2);
    }

    #[test]
    fn test_build_validated_config_with_invalid_regex() {
        let entries = vec![KeywordEntry {
            pattern: "[invalid".to_string(),
            match_type: MatchType::Regex,
            enabled: true,
        }];
        let result = build_validated_config(entries);
        assert!(result.is_err());
    }

    #[test]
    fn test_build_validated_config_with_too_many_entries() {
        let entries: Vec<KeywordEntry> = (0..=MAX_ENTRIES)
            .map(|i| KeywordEntry {
                pattern: format!("pattern{}", i),
                match_type: MatchType::Exact,
                enabled: true,
            })
            .collect();
        let result = build_validated_config(entries);
        assert!(result.is_err());
    }

    #[test]
    fn test_build_validated_config_with_disabled_entry() {
        let entries = vec![KeywordEntry {
            pattern: "test".to_string(),
            match_type: MatchType::Exact,
            enabled: false,
        }];
        let result = build_validated_config(entries);
        assert!(result.is_ok());
    }
}