1use rand::Rng;
2use serde::de::Error as DeError;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use std::fmt;
5use std::str::FromStr;
6use thiserror::Error;
7
8const PREFIX: &str = "pbx_";
9const LENGTH: usize = 8;
10const ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct PboxId(String);
15
16#[derive(Debug, Error, Clone, PartialEq, Eq)]
17pub enum PboxIdError {
18 #[error("public id must start with pbx_")]
19 InvalidPrefix,
20 #[error("public id must contain exactly 8 characters after pbx_")]
21 InvalidLength,
22 #[error("public id contains an invalid character: {0}")]
23 InvalidCharacter(char),
24}
25
26impl PboxId {
27 pub fn generate() -> Self {
28 let mut rng = rand::thread_rng();
29 let value: String = (0..LENGTH)
30 .map(|_| {
31 let index = rng.gen_range(0..ALPHABET.len());
32 ALPHABET[index] as char
33 })
34 .collect();
35 Self(format!("{PREFIX}{value}"))
36 }
37
38 pub fn parse(value: &str) -> Result<Self, PboxIdError> {
39 if !value.starts_with(PREFIX) {
40 return Err(PboxIdError::InvalidPrefix);
41 }
42 let suffix = &value[PREFIX.len()..];
43 if suffix.chars().count() != LENGTH {
44 return Err(PboxIdError::InvalidLength);
45 }
46 for character in suffix.chars() {
47 if !ALPHABET.contains(&(character as u8)) {
48 return Err(PboxIdError::InvalidCharacter(character));
49 }
50 }
51 Ok(Self(value.to_owned()))
52 }
53
54 pub fn as_str(&self) -> &str {
55 &self.0
56 }
57}
58
59impl fmt::Display for PboxId {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 formatter.write_str(&self.0)
62 }
63}
64
65impl FromStr for PboxId {
66 type Err = PboxIdError;
67
68 fn from_str(value: &str) -> Result<Self, Self::Err> {
69 Self::parse(value)
70 }
71}
72
73impl Serialize for PboxId {
74 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
75 where
76 S: Serializer,
77 {
78 serializer.serialize_str(self.as_str())
79 }
80}
81
82impl<'de> Deserialize<'de> for PboxId {
83 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84 where
85 D: Deserializer<'de>,
86 {
87 let value = String::deserialize(deserializer)?;
88 Self::parse(&value).map_err(D::Error::custom)
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn generated_ids_have_the_stable_shape() {
98 let id = PboxId::generate();
99 assert!(id.as_str().starts_with("pbx_"));
100 assert_eq!(id.as_str().len(), 12);
101 assert_eq!(PboxId::parse(id.as_str()).expect("generated id"), id);
102 }
103
104 #[test]
105 fn parser_rejects_ambiguous_or_malformed_values() {
106 for value in [
107 "box_t3yzd9y3",
108 "pbx_t3yzd9y",
109 "pbx_t3yzd9y3!",
110 "pbx_T3yzd9y3",
111 ] {
112 assert!(PboxId::parse(value).is_err(), "accepted {value}");
113 }
114 }
115}