use-db-name 0.1.0

Strongly typed database identifier wrappers for RustUse
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

//! Strongly typed database names for `RustUse`.

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 {
            /// Creates a database identifier from non-empty text.
            ///
            /// # Errors
            ///
            /// Returns [`DatabaseNameError`] when the value is empty or contains control characters.
            pub fn new(input: impl AsRef<str>) -> Result<Self, DatabaseNameError> {
                validate_name(input.as_ref()).map(|value| Self(value.to_owned()))
            }

            /// Returns the stored identifier text.
            #[must_use]
            pub fn as_str(&self) -> &str {
                &self.0
            }

            /// Consumes the identifier and returns the owned string.
            #[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);

/// Error returned when a database identifier wrapper rejects input.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DatabaseNameError {
    /// The name was empty after trimming whitespace.
    Empty,
    /// The name contained a Unicode control character.
    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 {}

/// Returns whether the input is accepted by the generic database name validator.
#[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(())
    }
}