#[derive(Debug)]
pub enum SharedSchemaError {
InvalidIdentifier(String),
}
impl std::fmt::Display for SharedSchemaError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SharedSchemaError::InvalidIdentifier(s) => {
write!(f, "invalid SQL identifier: {s:?}")
}
}
}
}
impl std::error::Error for SharedSchemaError {}
pub struct ColumnPair<'a> {
pub src: &'a str,
pub dst: &'a str,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IdKind {
Heer,
Ranj,
}
impl IdKind {
pub fn flip_fn(&self) -> &'static str {
match self {
IdKind::Heer => "heerid_to_desc",
IdKind::Ranj => "ranjid_to_desc",
}
}
}
pub fn validate_ident(s: &str) -> Result<(), SharedSchemaError> {
if s.is_empty() || s.len() > 63 {
return Err(SharedSchemaError::InvalidIdentifier(s.to_string()));
}
let mut chars = s.chars();
let first = chars.next().expect("non-empty checked above");
if !(first.is_ascii_alphabetic() || first == '_') {
return Err(SharedSchemaError::InvalidIdentifier(s.to_string()));
}
if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(SharedSchemaError::InvalidIdentifier(s.to_string()));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_ident_rejects_sql_injection_attempts() {
assert!(validate_ident("tbl; DROP TABLE users").is_err());
assert!(validate_ident("\"quoted\"").is_err());
assert!(validate_ident("it's").is_err());
assert!(validate_ident("tbl--").is_err());
assert!(validate_ident("two words").is_err());
assert!(validate_ident("tab\tname").is_err());
assert!(validate_ident("nl\nname").is_err());
assert!(validate_ident("").is_err());
assert!(validate_ident(&"x".repeat(64)).is_err());
assert!(validate_ident("1tbl").is_err());
assert!(validate_ident("tbl-name").is_err());
assert!(validate_ident("tbl.name").is_err());
}
#[test]
fn validate_ident_accepts_valid_identifiers() {
assert!(validate_ident("tbl").is_ok());
assert!(validate_ident("_internal_thing").is_ok());
assert!(validate_ident("events_v2").is_ok());
assert!(validate_ident("A").is_ok());
assert!(validate_ident("_").is_ok());
assert!(validate_ident("id_desc").is_ok());
assert!(validate_ident(&"a".repeat(63)).is_ok());
}
#[test]
fn id_kind_flip_fn_matches_sql_names() {
assert_eq!(IdKind::Heer.flip_fn(), "heerid_to_desc");
assert_eq!(IdKind::Ranj.flip_fn(), "ranjid_to_desc");
}
}