1use std::fmt;
4use std::sync::Arc;
5
6#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
9pub struct EntityKind(Arc<str>);
10
11pub mod kinds {
13 pub const EMAIL: &str = "EMAIL";
15 pub const PHONE: &str = "PHONE";
17 pub const CARD: &str = "CARD";
19 pub const IBAN: &str = "IBAN";
21 pub const SECRET: &str = "SECRET";
23 pub const IP: &str = "IP";
25 pub const URL: &str = "URL";
27 pub const NATID: &str = "NATID";
29 pub const PERSON: &str = "PERSON";
31 pub const ORG: &str = "ORG";
33 pub const LOCATION: &str = "LOCATION";
35}
36
37#[derive(Debug, thiserror::Error)]
39#[error("invalid type name {0:?}: use 1 to 32 characters A-Z, 0-9 and _, starting with a letter")]
40pub struct InvalidKind(pub String);
41
42impl EntityKind {
43 pub fn new(name: &str) -> Result<Self, InvalidKind> {
46 let valid = (1..=32).contains(&name.len())
47 && name.starts_with(|c: char| c.is_ascii_uppercase())
48 && name.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_');
49 if valid {
50 Ok(EntityKind(name.into()))
51 } else {
52 Err(InvalidKind(name.to_owned()))
53 }
54 }
55
56 pub(crate) fn builtin(name: &'static str) -> Self {
57 EntityKind(name.into())
58 }
59
60 pub fn as_str(&self) -> &str {
62 &self.0
63 }
64}
65
66impl fmt::Display for EntityKind {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 f.write_str(&self.0)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn validates_names() {
78 assert!(EntityKind::new("EMAIL").is_ok());
79 assert!(EntityKind::new("NATIONAL_ID2").is_ok());
80 for bad in ["", "email", "1A", "A-B", "A B", &"A".repeat(33)] {
81 assert!(EntityKind::new(bad).is_err(), "{bad}");
82 }
83 }
84}