Skip to main content

use_db_name/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Strongly typed database names for `RustUse`.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9macro_rules! database_name_type {
10    ($type_name:ident) => {
11        #[doc = concat!("A strongly typed database identifier wrapper: `", stringify!($type_name), "`.")]
12        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
13        pub struct $type_name(String);
14
15        impl $type_name {
16            /// Creates a database identifier from non-empty text.
17            ///
18            /// # Errors
19            ///
20            /// Returns [`DatabaseNameError`] when the value is empty or contains control characters.
21            pub fn new(input: impl AsRef<str>) -> Result<Self, DatabaseNameError> {
22                validate_name(input.as_ref()).map(|value| Self(value.to_owned()))
23            }
24
25            /// Returns the stored identifier text.
26            #[must_use]
27            pub fn as_str(&self) -> &str {
28                &self.0
29            }
30
31            /// Consumes the identifier and returns the owned string.
32            #[must_use]
33            pub fn into_string(self) -> String {
34                self.0
35            }
36        }
37
38        impl AsRef<str> for $type_name {
39            fn as_ref(&self) -> &str {
40                self.as_str()
41            }
42        }
43
44        impl fmt::Display for $type_name {
45            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46                formatter.write_str(self.as_str())
47            }
48        }
49
50        impl FromStr for $type_name {
51            type Err = DatabaseNameError;
52
53            fn from_str(input: &str) -> Result<Self, Self::Err> {
54                Self::new(input)
55            }
56        }
57
58        impl TryFrom<&str> for $type_name {
59            type Error = DatabaseNameError;
60
61            fn try_from(value: &str) -> Result<Self, Self::Error> {
62                Self::new(value)
63            }
64        }
65    };
66}
67
68database_name_type!(DatabaseName);
69database_name_type!(SchemaName);
70database_name_type!(TableName);
71database_name_type!(ColumnName);
72database_name_type!(CollectionName);
73database_name_type!(IndexName);
74database_name_type!(ConstraintName);
75database_name_type!(RelationName);
76database_name_type!(MigrationName);
77database_name_type!(DriverName);
78database_name_type!(PoolName);
79database_name_type!(ConnectionName);
80
81/// Error returned when a database identifier wrapper rejects input.
82#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum DatabaseNameError {
84    /// The name was empty after trimming whitespace.
85    Empty,
86    /// The name contained a Unicode control character.
87    ControlCharacter,
88}
89
90impl fmt::Display for DatabaseNameError {
91    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92        match self {
93            Self::Empty => formatter.write_str("database identifier cannot be empty"),
94            Self::ControlCharacter => {
95                formatter.write_str("database identifier cannot contain control characters")
96            },
97        }
98    }
99}
100
101impl Error for DatabaseNameError {}
102
103/// Returns whether the input is accepted by the generic database name validator.
104#[must_use]
105pub fn is_valid_database_name(input: &str) -> bool {
106    validate_name(input).is_ok()
107}
108
109fn validate_name(input: &str) -> Result<&str, DatabaseNameError> {
110    if input.chars().any(char::is_control) {
111        return Err(DatabaseNameError::ControlCharacter);
112    }
113    let trimmed = input.trim();
114    if trimmed.is_empty() {
115        return Err(DatabaseNameError::Empty);
116    }
117    Ok(trimmed)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::{
123        ColumnName, DatabaseName, DatabaseNameError, SchemaName, TableName, is_valid_database_name,
124    };
125
126    #[test]
127    fn creates_and_formats_names() -> Result<(), DatabaseNameError> {
128        let database = DatabaseName::new(" app ")?;
129        let schema = SchemaName::new("public")?;
130        let table = TableName::new("users")?;
131        let column = ColumnName::new("id")?;
132
133        assert_eq!(database.as_str(), "app");
134        assert_eq!(schema.to_string(), "public");
135        assert_eq!(table.into_string(), "users");
136        assert_eq!(column.as_ref(), "id");
137        Ok(())
138    }
139
140    #[test]
141    fn rejects_empty_and_control_names() {
142        assert_eq!(DatabaseName::new("  "), Err(DatabaseNameError::Empty));
143        assert_eq!(
144            TableName::new("users\n"),
145            Err(DatabaseNameError::ControlCharacter)
146        );
147        assert!(is_valid_database_name("tenant-01"));
148    }
149
150    #[test]
151    fn derives_ordering_and_hashing() -> Result<(), DatabaseNameError> {
152        let mut names = [TableName::new("users")?, TableName::new("accounts")?];
153        names.sort();
154
155        assert_eq!(names[0].as_str(), "accounts");
156        assert_eq!(names[1].as_str(), "users");
157        Ok(())
158    }
159}