Skip to main content

use_db_core/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Shared database vocabulary for `RustUse`.
5
6use core::{fmt, str::FromStr};
7use std::error::Error;
8
9/// Common database storage families.
10#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum DatabaseKind {
12    /// Relational database systems.
13    #[default]
14    Relational,
15    /// Document database systems.
16    Document,
17    /// Key-value database systems.
18    KeyValue,
19    /// Graph database systems.
20    Graph,
21    /// Search database systems.
22    Search,
23    /// Time-series database systems.
24    TimeSeries,
25    /// Column-oriented analytical database systems.
26    Columnar,
27    /// Object database systems.
28    Object,
29    /// Other or intentionally unspecified systems.
30    Other,
31}
32
33impl DatabaseKind {
34    /// Returns a stable lowercase label.
35    #[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/// Common database object kinds.
58#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
59pub enum DatabaseObjectKind {
60    /// A database/catalog object.
61    Database,
62    /// A schema or namespace object.
63    Schema,
64    /// A relational table-like object.
65    #[default]
66    Table,
67    /// A document collection-like object.
68    Collection,
69    /// A column or field-like object.
70    Column,
71    /// An index object.
72    Index,
73    /// A constraint object.
74    Constraint,
75    /// A relation or relationship object.
76    Relation,
77    /// A view-like object.
78    View,
79    /// A migration object.
80    Migration,
81    /// Other or intentionally unspecified object.
82    Other,
83}
84
85impl DatabaseObjectKind {
86    /// Returns a stable lowercase label.
87    #[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            /// Creates a database label from non-empty text.
118            ///
119            /// # Errors
120            ///
121            /// Returns [`DatabaseError`] when the label is empty or contains control characters.
122            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            /// Returns the stored label.
127            #[must_use]
128            pub fn as_str(&self) -> &str {
129                &self.0
130            }
131
132            /// Consumes the label and returns the owned string.
133            #[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/// Error returned by primitive database vocabulary constructors.
176#[derive(Clone, Copy, Debug, Eq, PartialEq)]
177pub enum DatabaseError {
178    /// A database engine label was empty after trimming.
179    EmptyEngine,
180    /// A database feature label was empty after trimming.
181    EmptyFeature,
182    /// A database capability label was empty after trimming.
183    EmptyCapability,
184    /// A database version label was empty after trimming.
185    EmptyVersion,
186    /// A database dialect label was empty after trimming.
187    EmptyDialect,
188    /// A label contained a control character.
189    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
211/// Result alias for primitive database vocabulary operations.
212pub 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}