Skip to main content

eventuary_sqlite/
relation.rs

1use eventuary_core::{Error, Result};
2
3#[derive(Debug, Clone, Eq, PartialEq, Hash)]
4pub struct SqliteRelationName {
5    table: String,
6}
7
8impl SqliteRelationName {
9    pub fn new(name: impl Into<String>) -> Result<Self> {
10        let raw = name.into();
11        validate_identifier(&raw)?;
12        Ok(Self { table: raw })
13    }
14
15    pub fn table(&self) -> &str {
16        &self.table
17    }
18
19    pub fn render(&self) -> String {
20        format!("\"{}\"", self.table)
21    }
22}
23
24fn validate_identifier(identifier: &str) -> Result<()> {
25    if identifier.is_empty() {
26        return Err(Error::Config("empty sqlite identifier".to_owned()));
27    }
28    let mut chars = identifier.chars();
29    let Some(first) = chars.next() else {
30        return Err(Error::Config("empty sqlite identifier".to_owned()));
31    };
32    if !(first == '_' || first.is_ascii_alphabetic()) {
33        return Err(Error::Config(format!(
34            "sqlite identifier must start with letter or underscore: {identifier:?}"
35        )));
36    }
37    if !chars.all(|c| c == '_' || c.is_ascii_alphanumeric()) {
38        return Err(Error::Config(format!(
39            "sqlite identifier may only contain letters, digits, underscores: {identifier:?}"
40        )));
41    }
42    Ok(())
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48
49    #[test]
50    fn renders_simple_relation() {
51        let relation = SqliteRelationName::new("events").unwrap();
52        assert_eq!(relation.render(), "\"events\"");
53    }
54
55    #[test]
56    fn rejects_invalid_relation() {
57        assert!(SqliteRelationName::new("events;drop").is_err());
58        assert!(SqliteRelationName::new("9events").is_err());
59        assert!(SqliteRelationName::new("").is_err());
60    }
61}