Skip to main content

riffdb_types/
database.rs

1//! Operator-visible database routing identities.
2
3use std::error::Error;
4use std::fmt;
5
6/// Maximum number of databases one RiffDB process may host.
7pub const MAX_DATABASES_PER_PROCESS: usize = 512;
8/// Maximum bytes in one canonical database alias.
9pub const MAX_DATABASE_ALIAS_BYTES: usize = 64;
10/// Compatibility alias assigned to the legacy single-database configuration.
11pub const DEFAULT_DATABASE_ALIAS: &str = "default";
12
13/// A canonical, non-secret database selector.
14#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
15pub struct DatabaseAlias(String);
16
17impl DatabaseAlias {
18    /// Validates an exact lowercase ASCII database alias.
19    pub fn new(value: impl Into<String>) -> Result<Self, DatabaseAliasError> {
20        let value = value.into();
21        let bytes = value.as_bytes();
22        if bytes.is_empty() {
23            return Err(DatabaseAliasError::Empty);
24        }
25        if bytes.len() > MAX_DATABASE_ALIAS_BYTES {
26            return Err(DatabaseAliasError::TooLong {
27                actual: bytes.len(),
28                maximum: MAX_DATABASE_ALIAS_BYTES,
29            });
30        }
31        if !bytes[0].is_ascii_lowercase() {
32            return Err(DatabaseAliasError::InvalidCharacter { index: 0 });
33        }
34        if let Some(index) = bytes.iter().position(|byte| {
35            !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && !matches!(byte, b'_' | b'-')
36        }) {
37            return Err(DatabaseAliasError::InvalidCharacter { index });
38        }
39        Ok(Self(value))
40    }
41
42    /// Returns the legacy single-database alias.
43    #[must_use]
44    pub fn default_alias() -> Self {
45        Self(DEFAULT_DATABASE_ALIAS.to_owned())
46    }
47
48    /// Borrows the exact canonical selector.
49    #[must_use]
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53
54    /// Borrows the exact canonical ASCII bytes.
55    #[must_use]
56    pub fn as_bytes(&self) -> &[u8] {
57        self.0.as_bytes()
58    }
59}
60
61impl fmt::Debug for DatabaseAlias {
62    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63        formatter
64            .debug_tuple("DatabaseAlias")
65            .field(&self.0)
66            .finish()
67    }
68}
69
70impl fmt::Display for DatabaseAlias {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        self.0.fmt(formatter)
73    }
74}
75
76/// A database alias did not match the public canonical grammar.
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub enum DatabaseAliasError {
79    /// The alias was empty.
80    Empty,
81    /// The alias exceeded its byte bound.
82    TooLong {
83        /// Submitted byte length.
84        actual: usize,
85        /// Accepted byte length.
86        maximum: usize,
87    },
88    /// One byte violated `[a-z][a-z0-9_-]{0,63}`.
89    InvalidCharacter {
90        /// Zero-based byte position.
91        index: usize,
92    },
93}
94
95impl fmt::Display for DatabaseAliasError {
96    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
97        formatter.write_str("invalid database alias")
98    }
99}
100
101impl Error for DatabaseAliasError {}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn canonical_aliases_preserve_exact_text_and_sort_by_bytes() {
109        let mut aliases = [
110            DatabaseAlias::new("zeta-2").expect("valid alias"),
111            DatabaseAlias::new("ea_local").expect("valid alias"),
112            DatabaseAlias::default_alias(),
113        ];
114        aliases.sort();
115        assert_eq!(
116            aliases.map(|alias| alias.as_str().to_owned()),
117            ["default", "ea_local", "zeta-2"]
118        );
119    }
120
121    #[test]
122    fn grammar_and_bounds_fail_closed() {
123        assert_eq!(DatabaseAlias::new(""), Err(DatabaseAliasError::Empty));
124        assert_eq!(
125            DatabaseAlias::new("A"),
126            Err(DatabaseAliasError::InvalidCharacter { index: 0 })
127        );
128        assert_eq!(
129            DatabaseAlias::new("1database"),
130            Err(DatabaseAliasError::InvalidCharacter { index: 0 })
131        );
132        assert_eq!(
133            DatabaseAlias::new("data.base"),
134            Err(DatabaseAliasError::InvalidCharacter { index: 4 })
135        );
136        assert_eq!(
137            DatabaseAlias::new("a".repeat(MAX_DATABASE_ALIAS_BYTES + 1)),
138            Err(DatabaseAliasError::TooLong {
139                actual: MAX_DATABASE_ALIAS_BYTES + 1,
140                maximum: MAX_DATABASE_ALIAS_BYTES,
141            })
142        );
143        assert!(DatabaseAlias::new("a".repeat(MAX_DATABASE_ALIAS_BYTES)).is_ok());
144    }
145}