1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use 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 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 #[must_use]
27 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30
31 #[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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub enum DatabaseNameError {
84 Empty,
86 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#[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}