1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
7use std::error::Error;
8
9#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum DatabaseKind {
12 #[default]
14 Relational,
15 Document,
17 KeyValue,
19 Graph,
21 Search,
23 TimeSeries,
25 Columnar,
27 Object,
29 Other,
31}
32
33impl DatabaseKind {
34 #[must_use]
36 pub const fn as_str(self) -> &'static str {
37 match self {
38 Self::Relational => "relational",
39 Self::Document => "document",
40 Self::KeyValue => "key-value",
41 Self::Graph => "graph",
42 Self::Search => "search",
43 Self::TimeSeries => "time-series",
44 Self::Columnar => "columnar",
45 Self::Object => "object",
46 Self::Other => "other",
47 }
48 }
49}
50
51impl fmt::Display for DatabaseKind {
52 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
53 formatter.write_str(self.as_str())
54 }
55}
56
57#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub enum DatabaseObjectKind {
60 Database,
62 Schema,
64 #[default]
66 Table,
67 Collection,
69 Column,
71 Index,
73 Constraint,
75 Relation,
77 View,
79 Migration,
81 Other,
83}
84
85impl DatabaseObjectKind {
86 #[must_use]
88 pub const fn as_str(self) -> &'static str {
89 match self {
90 Self::Database => "database",
91 Self::Schema => "schema",
92 Self::Table => "table",
93 Self::Collection => "collection",
94 Self::Column => "column",
95 Self::Index => "index",
96 Self::Constraint => "constraint",
97 Self::Relation => "relation",
98 Self::View => "view",
99 Self::Migration => "migration",
100 Self::Other => "other",
101 }
102 }
103}
104
105impl fmt::Display for DatabaseObjectKind {
106 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
107 formatter.write_str(self.as_str())
108 }
109}
110
111macro_rules! database_label_type {
112 ($type_name:ident, $error_empty:expr) => {
113 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
114 pub struct $type_name(String);
115
116 impl $type_name {
117 pub fn new(input: impl AsRef<str>) -> Result<Self, DatabaseError> {
123 validate_label(input.as_ref(), $error_empty).map(|value| Self(value.to_owned()))
124 }
125
126 #[must_use]
128 pub fn as_str(&self) -> &str {
129 &self.0
130 }
131
132 #[must_use]
134 pub fn into_string(self) -> String {
135 self.0
136 }
137 }
138
139 impl AsRef<str> for $type_name {
140 fn as_ref(&self) -> &str {
141 self.as_str()
142 }
143 }
144
145 impl fmt::Display for $type_name {
146 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
147 formatter.write_str(self.as_str())
148 }
149 }
150
151 impl FromStr for $type_name {
152 type Err = DatabaseError;
153
154 fn from_str(input: &str) -> Result<Self, Self::Err> {
155 Self::new(input)
156 }
157 }
158
159 impl TryFrom<&str> for $type_name {
160 type Error = DatabaseError;
161
162 fn try_from(value: &str) -> Result<Self, Self::Error> {
163 Self::new(value)
164 }
165 }
166 };
167}
168
169database_label_type!(DatabaseEngine, DatabaseError::EmptyEngine);
170database_label_type!(DatabaseFeature, DatabaseError::EmptyFeature);
171database_label_type!(DatabaseCapability, DatabaseError::EmptyCapability);
172database_label_type!(DatabaseVersion, DatabaseError::EmptyVersion);
173database_label_type!(DatabaseDialect, DatabaseError::EmptyDialect);
174
175#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub enum DatabaseError {
178 EmptyEngine,
180 EmptyFeature,
182 EmptyCapability,
184 EmptyVersion,
186 EmptyDialect,
188 ControlCharacter,
190}
191
192impl fmt::Display for DatabaseError {
193 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 Self::EmptyEngine => formatter.write_str("database engine label cannot be empty"),
196 Self::EmptyFeature => formatter.write_str("database feature label cannot be empty"),
197 Self::EmptyCapability => {
198 formatter.write_str("database capability label cannot be empty")
199 },
200 Self::EmptyVersion => formatter.write_str("database version label cannot be empty"),
201 Self::EmptyDialect => formatter.write_str("database dialect label cannot be empty"),
202 Self::ControlCharacter => {
203 formatter.write_str("database label cannot contain control characters")
204 },
205 }
206 }
207}
208
209impl Error for DatabaseError {}
210
211pub type DatabaseResult<T> = Result<T, DatabaseError>;
213
214fn validate_label(input: &str, empty_error: DatabaseError) -> Result<&str, DatabaseError> {
215 if input.chars().any(char::is_control) {
216 return Err(DatabaseError::ControlCharacter);
217 }
218 let trimmed = input.trim();
219 if trimmed.is_empty() {
220 return Err(empty_error);
221 }
222 Ok(trimmed)
223}
224
225#[cfg(test)]
226mod tests {
227 use super::{DatabaseDialect, DatabaseEngine, DatabaseError, DatabaseKind, DatabaseObjectKind};
228
229 #[test]
230 fn formats_database_kinds_and_objects() {
231 assert_eq!(DatabaseKind::Document.to_string(), "document");
232 assert_eq!(DatabaseObjectKind::Collection.to_string(), "collection");
233 }
234
235 #[test]
236 fn validates_labels() -> Result<(), DatabaseError> {
237 let engine = DatabaseEngine::new(" postgres ")?;
238 let dialect = DatabaseDialect::new("sql")?;
239
240 assert_eq!(engine.as_str(), "postgres");
241 assert_eq!(dialect.to_string(), "sql");
242 assert_eq!(DatabaseEngine::new(" "), Err(DatabaseError::EmptyEngine));
243 Ok(())
244 }
245}