use crate::Error;
pub const MAX_DATABASE_ENUM_VARIANTS: usize = 64;
pub const MAX_DATABASE_ENUM_LABEL_BYTES: usize = 63;
pub trait DatabaseEnum {
const TYPE_NAME: &'static str;
const VARIANTS: &'static [&'static str];
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct NativeEnumDefinition {
pub type_name: &'static str,
pub variants: &'static [&'static str],
}
pub(super) fn validated_definition<E: DatabaseEnum>() -> Result<NativeEnumDefinition, Error> {
validate_native_enum(E::TYPE_NAME, E::VARIANTS)?;
Ok(NativeEnumDefinition {
type_name: E::TYPE_NAME,
variants: E::VARIANTS,
})
}
pub(super) fn validate_native_enum(type_name: &str, variants: &[&str]) -> Result<(), Error> {
let type_name_bytes = type_name.as_bytes();
if type_name_bytes.is_empty()
|| type_name_bytes.len() > MAX_DATABASE_ENUM_LABEL_BYTES
|| !(type_name_bytes[0].is_ascii_alphabetic() || type_name_bytes[0] == b'_')
|| !type_name_bytes
.iter()
.all(|byte| byte.is_ascii_alphanumeric() || *byte == b'_')
{
return Err(Error::Validation(format!(
"database enum type names must be 1-{MAX_DATABASE_ENUM_LABEL_BYTES} byte unqualified ASCII identifiers"
)));
}
if variants.is_empty() || variants.len() > MAX_DATABASE_ENUM_VARIANTS {
return Err(Error::Validation(format!(
"database enums require 1-{MAX_DATABASE_ENUM_VARIANTS} variants"
)));
}
let mut seen = std::collections::BTreeSet::new();
for variant in variants {
if variant.is_empty()
|| variant.len() > MAX_DATABASE_ENUM_LABEL_BYTES
|| !variant
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b' '))
{
return Err(Error::Validation(format!(
"database enum labels must be 1-{MAX_DATABASE_ENUM_LABEL_BYTES} ASCII letters, digits, spaces, underscores or hyphens"
)));
}
if !seen.insert(*variant) {
return Err(Error::Validation(
"database enum labels must be unique".to_string(),
));
}
}
Ok(())
}
pub(super) fn quoted_label(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn enum_metadata_rejects_unsafe_or_ambiguous_shapes() {
assert!(validate_native_enum("account_status", &["Active", "Paused"]).is_ok());
assert!(validate_native_enum("public.account_status", &["Active"]).is_err());
assert!(validate_native_enum("1account_status", &["Active"]).is_err());
assert!(validate_native_enum("status;drop", &["Active"]).is_err());
assert!(
validate_native_enum(&"x".repeat(MAX_DATABASE_ENUM_LABEL_BYTES + 1), &["Active"])
.is_err()
);
assert!(validate_native_enum("status", &[]).is_err());
assert!(validate_native_enum("status", &["Active", "Active"]).is_err());
assert!(validate_native_enum("status", &["line\nfeed"]).is_err());
assert!(validate_native_enum("status", &["owner's"]).is_err());
assert!(validate_native_enum("status", &["back\\slash"]).is_err());
assert!(
validate_native_enum("status", &[&"x".repeat(MAX_DATABASE_ENUM_LABEL_BYTES + 1)])
.is_err()
);
}
#[test]
fn enum_labels_are_sql_quoted_defensively() {
assert_eq!(quoted_label("awaiting_review"), "'awaiting_review'");
assert_eq!(quoted_label("owner's"), "'owner''s'");
}
}