kellnr_common/
account_name.rs1use std::fmt;
4
5use sea_orm::Value;
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8use utoipa::ToSchema;
9
10pub const MAX_NAME_LEN: usize = 64;
12
13pub const MAX_TOKEN_NAME_LEN: usize = 128;
15
16#[derive(Debug, Error, PartialEq, Eq)]
17pub enum AccountNameError {
18 #[error("The name must not be empty")]
19 Empty,
20 #[error("The name must not be longer than {0} characters")]
21 TooLong(usize),
22 #[error("The name must not contain control characters")]
23 ControlCharacter,
24 #[error("The user name must not contain ':'")]
25 Colon,
26}
27
28pub fn validate_user_name(name: &str) -> Result<(), AccountNameError> {
31 validate(name, MAX_NAME_LEN)?;
32 if name.contains(':') {
33 return Err(AccountNameError::Colon);
34 }
35 Ok(())
36}
37
38pub fn validate_group_name(name: &str) -> Result<(), AccountNameError> {
40 validate(name, MAX_NAME_LEN)
41}
42
43pub fn validate_token_name(name: &str) -> Result<(), AccountNameError> {
45 validate(name, MAX_TOKEN_NAME_LEN)
46}
47
48macro_rules! account_name_type {
52 ($(#[$doc:meta])* $name:ident, $new_fn:ident, $validate:path) => {
53 $(#[$doc])*
54 #[derive(
55 Default,
56 Debug,
57 Clone,
58 PartialEq,
59 Eq,
60 PartialOrd,
61 Ord,
62 Hash,
63 Serialize,
64 Deserialize,
65 ToSchema,
66 )]
67 #[serde(try_from = "String", into = "String")]
68 pub struct $name(String);
69
70 impl $name {
71 pub fn $new_fn(name: impl Into<String>) -> Result<Self, AccountNameError> {
73 let name = name.into();
74 $validate(&name)?;
75 Ok(Self(name))
76 }
77
78 pub fn new(name: impl Into<String>) -> Result<Self, AccountNameError> {
81 let name = name.into();
82 if name.is_empty() {
83 return Err(AccountNameError::Empty);
84 }
85 Ok(Self(name))
86 }
87
88 pub fn from_unchecked(name: String) -> Self {
91 Self(name)
92 }
93
94 pub fn as_str(&self) -> &str {
95 &self.0
96 }
97
98 pub fn into_inner(self) -> String {
99 self.0
100 }
101 }
102
103 impl TryFrom<String> for $name {
104 type Error = AccountNameError;
105
106 fn try_from(name: String) -> Result<Self, Self::Error> {
107 Self::new(name)
108 }
109 }
110
111 impl TryFrom<&str> for $name {
112 type Error = AccountNameError;
113
114 fn try_from(name: &str) -> Result<Self, Self::Error> {
115 Self::new(name)
116 }
117 }
118
119 impl From<$name> for String {
120 fn from(name: $name) -> Self {
121 name.0
122 }
123 }
124
125 impl From<&$name> for Value {
126 fn from(name: &$name) -> Self {
127 Value::String(Some(name.0.clone()))
128 }
129 }
130
131 impl AsRef<str> for $name {
132 fn as_ref(&self) -> &str {
133 &self.0
134 }
135 }
136
137 impl PartialEq<str> for $name {
138 fn eq(&self, other: &str) -> bool {
139 self.0 == other
140 }
141 }
142
143 impl PartialEq<&str> for $name {
144 fn eq(&self, other: &&str) -> bool {
145 self.0 == *other
146 }
147 }
148
149 impl PartialEq<$name> for str {
150 fn eq(&self, other: &$name) -> bool {
151 self == other.0
152 }
153 }
154
155 impl PartialEq<$name> for &str {
156 fn eq(&self, other: &$name) -> bool {
157 *self == other.0
158 }
159 }
160
161 impl fmt::Display for $name {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 f.write_str(&self.0)
164 }
165 }
166 };
167}
168
169account_name_type!(
170 UserName,
172 for_new_user,
173 validate_user_name
174);
175
176account_name_type!(
177 GroupName,
179 for_new_group,
180 validate_group_name
181);
182
183fn validate(name: &str, max_len: usize) -> Result<(), AccountNameError> {
184 if name.trim().is_empty() {
185 return Err(AccountNameError::Empty);
186 }
187 if name.chars().count() > max_len {
188 return Err(AccountNameError::TooLong(max_len));
189 }
190 if name.chars().any(char::is_control) {
191 return Err(AccountNameError::ControlCharacter);
192 }
193 Ok(())
194}
195
196#[cfg(test)]
197mod tests {
198 use super::*;
199
200 #[test]
201 fn valid_names_are_accepted() {
202 assert_eq!(validate_user_name("alice.smith"), Ok(()));
203 assert_eq!(validate_user_name(&"a".repeat(MAX_NAME_LEN)), Ok(()));
204 assert_eq!(validate_group_name("dev team"), Ok(()));
205 assert_eq!(validate_token_name("CI token: main"), Ok(()));
206 }
207
208 #[test]
209 fn empty_and_whitespace_names_are_rejected() {
210 assert_eq!(validate_user_name(""), Err(AccountNameError::Empty));
211 assert_eq!(validate_group_name(" "), Err(AccountNameError::Empty));
212 assert_eq!(validate_token_name("\t"), Err(AccountNameError::Empty));
213 }
214
215 #[test]
216 fn long_names_are_rejected() {
217 assert_eq!(
218 validate_user_name(&"a".repeat(MAX_NAME_LEN + 1)),
219 Err(AccountNameError::TooLong(MAX_NAME_LEN))
220 );
221 assert_eq!(
222 validate_token_name(&"a".repeat(MAX_TOKEN_NAME_LEN + 1)),
223 Err(AccountNameError::TooLong(MAX_TOKEN_NAME_LEN))
224 );
225 }
226
227 #[test]
228 fn control_characters_are_rejected() {
229 assert_eq!(
230 validate_user_name("ali\nce"),
231 Err(AccountNameError::ControlCharacter)
232 );
233 }
234
235 #[test]
236 fn existing_names_are_taken_as_they_are() {
237 let name = "a:b".repeat(30);
238 assert_eq!(name, UserName::new(name.as_str()).unwrap().as_str());
239 assert_eq!(name, GroupName::new(name.as_str()).unwrap().as_str());
240 assert_eq!(Err(AccountNameError::Empty), UserName::new(""));
241 }
242
243 #[test]
244 fn new_names_are_validated() {
245 assert_eq!(Err(AccountNameError::Colon), UserName::for_new_user("a:b"));
246 assert_eq!(
247 Err(AccountNameError::TooLong(MAX_NAME_LEN)),
248 GroupName::for_new_group("a".repeat(MAX_NAME_LEN + 1))
249 );
250 assert_eq!(UserName::for_new_user("alice").unwrap(), "alice");
251 }
252
253 #[test]
254 fn names_are_strings_in_json() {
255 let name: UserName = serde_json::from_str(r#""a:b""#).unwrap();
256 assert_eq!(name, "a:b");
257 assert_eq!(r#""a:b""#, serde_json::to_string(&name).unwrap());
258 assert!(serde_json::from_str::<GroupName>(r#""""#).is_err());
259 }
260
261 #[test]
262 fn colon_is_rejected_in_user_names_only() {
263 assert_eq!(validate_user_name("a:b"), Err(AccountNameError::Colon));
264 assert_eq!(validate_group_name("a:b"), Ok(()));
265 }
266}