1use std::{borrow::Borrow, ops::Deref};
5use crate::_prelude::*;
7
8macro_rules! def_id {
9 ($name:ident, $doc:literal, $kind:literal) => {
10 #[doc = $doc]
11 #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12 #[serde(try_from = "String", into = "String")]
13 pub struct $name(String);
14 impl $name {
15 pub fn new(value: impl AsRef<str>) -> Result<Self, IdentifierError> {
17 let view = value.as_ref();
18
19 validate_view($kind, view)?;
20
21 Ok(Self(view.to_owned()))
22 }
23 }
24 impl Deref for $name {
25 type Target = str;
26
27 fn deref(&self) -> &Self::Target {
28 &self.0
29 }
30 }
31 impl AsRef<str> for $name {
32 fn as_ref(&self) -> &str {
33 &self.0
34 }
35 }
36 impl From<$name> for String {
37 fn from(value: $name) -> Self {
38 value.0
39 }
40 }
41 impl TryFrom<String> for $name {
42 type Error = IdentifierError;
43
44 fn try_from(value: String) -> Result<Self, Self::Error> {
45 validate_view($kind, &value)?;
46
47 Ok(Self(value))
48 }
49 }
50 impl Borrow<str> for $name {
51 fn borrow(&self) -> &str {
52 &self.0
53 }
54 }
55 impl Debug for $name {
56 fn fmt(&self, f: &mut Formatter) -> FmtResult {
57 write!(f, concat!($kind, "({})"), self.0)
58 }
59 }
60 impl Display for $name {
61 fn fmt(&self, f: &mut Formatter) -> FmtResult {
62 f.write_str(&self.0)
63 }
64 }
65 impl FromStr for $name {
66 type Err = IdentifierError;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 Self::new(s)
70 }
71 }
72 };
73}
74
75const IDENTIFIER_MAX_LEN: usize = 128;
76
77#[derive(Clone, Debug, PartialEq, Eq, Serialize, ThisError)]
79pub enum IdentifierError {
80 #[error("{kind} identifier cannot be empty.")]
82 Empty {
83 kind: &'static str,
85 },
86 #[error("{kind} identifier contains whitespace.")]
88 ContainsWhitespace {
89 kind: &'static str,
91 },
92 #[error("{kind} identifier exceeds {max} characters.")]
94 TooLong {
95 kind: &'static str,
97 max: usize,
99 },
100}
101
102def_id! { TenantId, "Unique identifier for a broker tenant.", "Tenant" }
103def_id! { PrincipalId, "Unique identifier for a broker principal.", "Principal" }
104def_id! { ProviderId, "Identifier for an OAuth provider descriptor.", "Provider" }
105
106fn validate_view(kind: &'static str, view: &str) -> Result<(), IdentifierError> {
107 if view.is_empty() {
108 return Err(IdentifierError::Empty { kind });
109 }
110 if view.chars().any(char::is_whitespace) {
111 return Err(IdentifierError::ContainsWhitespace { kind });
112 }
113 if view.len() > IDENTIFIER_MAX_LEN {
114 return Err(IdentifierError::TooLong { kind, max: IDENTIFIER_MAX_LEN });
115 }
116
117 Ok(())
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
124
125 #[test]
126 fn identifiers_trim_and_validate() {
127 assert!(TenantId::new(" tenant-123").is_err(), "Leading whitespace must be rejected.");
128 assert!(TenantId::new("tenant-123 ").is_err(), "Trailing whitespace must be rejected.");
129
130 let tenant =
131 TenantId::new("tenant-123").expect("Tenant fixture should be considered valid.");
132
133 assert_eq!(tenant.as_ref(), "tenant-123");
134 assert!(PrincipalId::new("").is_err());
135 assert!(ProviderId::new("with space").is_err());
136 }
137
138 #[test]
139 fn serde_round_trip_enforces_validation() {
140 let payload = "\"tenant-42\"";
141 let tenant: TenantId =
142 serde_json::from_str(payload).expect("Tenant should deserialize successfully.");
143
144 assert_eq!(tenant.as_ref(), "tenant-42");
145 assert!(serde_json::from_str::<TenantId>("\"with space\"").is_err());
146 assert!(serde_json::from_str::<TenantId>("\" tenant-42\"").is_err());
147 }
148
149 #[test]
150 fn unicode_whitespace_and_length_limits() {
151 let nbsp = format!("tenant{}id", '\u{00A0}');
152
153 assert!(TenantId::new( ).is_err());
154
155 let exact = "a".repeat(IDENTIFIER_MAX_LEN);
156
157 TenantId::new(&exact).expect("Exact length should succeed.");
158
159 let too_long = "a".repeat(IDENTIFIER_MAX_LEN + 1);
160
161 assert!(TenantId::new(&too_long).is_err());
162 }
163
164 #[test]
165 fn borrow_supports_fast_lookup() {
166 let map: HashMap<TenantId, u8> = HashMap::from_iter([(
167 TenantId::new("tenant-123").expect("Tenant used for lookup should be valid."),
168 7_u8,
169 )]);
170
171 assert_eq!(map.get("tenant-123"), Some(&7));
172 }
173}