Skip to main content

database_mcp/db/
identifier.rs

1//! Shared identifier validation for all database backends.
2
3use crate::error::AppError;
4
5/// Validates that `name` is a non-empty identifier without control characters.
6///
7/// # Errors
8///
9/// Returns [`AppError::InvalidIdentifier`] if the name is empty,
10/// whitespace-only, or contains control characters.
11pub fn validate_identifier(name: &str) -> Result<(), AppError> {
12    if name.is_empty() || name.chars().all(char::is_whitespace) {
13        return Err(AppError::InvalidIdentifier(name.to_string()));
14    }
15    if name.chars().any(char::is_control) {
16        return Err(AppError::InvalidIdentifier(name.to_string()));
17    }
18    Ok(())
19}
20
21#[cfg(test)]
22mod tests {
23    use super::*;
24
25    #[test]
26    fn accepts_standard_names() {
27        assert!(validate_identifier("users").is_ok());
28        assert!(validate_identifier("my_table").is_ok());
29        assert!(validate_identifier("DB_123").is_ok());
30    }
31
32    #[test]
33    fn accepts_hyphenated_names() {
34        assert!(validate_identifier("eu-docker").is_ok());
35        assert!(validate_identifier("access-logs").is_ok());
36    }
37
38    #[test]
39    fn accepts_special_chars() {
40        assert!(validate_identifier("my.db").is_ok());
41        assert!(validate_identifier("123db").is_ok());
42        assert!(validate_identifier("café").is_ok());
43        assert!(validate_identifier("a b").is_ok());
44    }
45
46    #[test]
47    fn rejects_empty() {
48        assert!(validate_identifier("").is_err());
49    }
50
51    #[test]
52    fn rejects_whitespace_only() {
53        assert!(validate_identifier("   ").is_err());
54        assert!(validate_identifier("\t").is_err());
55    }
56
57    #[test]
58    fn rejects_control_chars() {
59        assert!(validate_identifier("test\x00db").is_err());
60        assert!(validate_identifier("test\ndb").is_err());
61        assert!(validate_identifier("test\x1Fdb").is_err());
62    }
63}