#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
macro_rules! database_name_type {
($type_name:ident) => {
#[doc = concat!("A strongly typed database identifier wrapper: `", stringify!($type_name), "`.")]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $type_name(String);
impl $type_name {
pub fn new(input: impl AsRef<str>) -> Result<Self, DatabaseNameError> {
validate_name(input.as_ref()).map(|value| Self(value.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for $type_name {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for $type_name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for $type_name {
type Err = DatabaseNameError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Self::new(input)
}
}
impl TryFrom<&str> for $type_name {
type Error = DatabaseNameError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::new(value)
}
}
};
}
database_name_type!(DatabaseName);
database_name_type!(SchemaName);
database_name_type!(TableName);
database_name_type!(ColumnName);
database_name_type!(CollectionName);
database_name_type!(IndexName);
database_name_type!(ConstraintName);
database_name_type!(RelationName);
database_name_type!(MigrationName);
database_name_type!(DriverName);
database_name_type!(PoolName);
database_name_type!(ConnectionName);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DatabaseNameError {
Empty,
ControlCharacter,
}
impl fmt::Display for DatabaseNameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("database identifier cannot be empty"),
Self::ControlCharacter => {
formatter.write_str("database identifier cannot contain control characters")
},
}
}
}
impl Error for DatabaseNameError {}
#[must_use]
pub fn is_valid_database_name(input: &str) -> bool {
validate_name(input).is_ok()
}
fn validate_name(input: &str) -> Result<&str, DatabaseNameError> {
if input.chars().any(char::is_control) {
return Err(DatabaseNameError::ControlCharacter);
}
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(DatabaseNameError::Empty);
}
Ok(trimmed)
}
#[cfg(test)]
mod tests {
use super::{
ColumnName, DatabaseName, DatabaseNameError, SchemaName, TableName, is_valid_database_name,
};
#[test]
fn creates_and_formats_names() -> Result<(), DatabaseNameError> {
let database = DatabaseName::new(" app ")?;
let schema = SchemaName::new("public")?;
let table = TableName::new("users")?;
let column = ColumnName::new("id")?;
assert_eq!(database.as_str(), "app");
assert_eq!(schema.to_string(), "public");
assert_eq!(table.into_string(), "users");
assert_eq!(column.as_ref(), "id");
Ok(())
}
#[test]
fn rejects_empty_and_control_names() {
assert_eq!(DatabaseName::new(" "), Err(DatabaseNameError::Empty));
assert_eq!(
TableName::new("users\n"),
Err(DatabaseNameError::ControlCharacter)
);
assert!(is_valid_database_name("tenant-01"));
}
#[test]
fn derives_ordering_and_hashing() -> Result<(), DatabaseNameError> {
let mut names = [TableName::new("users")?, TableName::new("accounts")?];
names.sort();
assert_eq!(names[0].as_str(), "accounts");
assert_eq!(names[1].as_str(), "users");
Ok(())
}
}