eventuary_sqlite/
relation.rs1use 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}