nntp_proxy/types/
validated.rs1use serde::{Deserialize, Serialize};
4use std::fmt;
5use thiserror::Error;
6
7#[derive(Debug, Clone, Error, PartialEq, Eq)]
9#[non_exhaustive]
10pub enum ValidationError {
11 #[error("hostname cannot be empty or whitespace")]
12 EmptyHostName,
13
14 #[error("server name cannot be empty or whitespace")]
15 EmptyServerName,
16
17 #[error("invalid hostname: {0}")]
18 InvalidHostName(String),
19
20 #[error("port cannot be 0")]
21 InvalidPort,
22
23 #[error("invalid message ID: {0}")]
24 InvalidMessageId(String),
25}
26
27macro_rules! validated_string {
55 (
56 $(#[$meta:meta])*
57 $vis:vis struct $name:ident(String) {
58 validation: |$s_param:ident| $validation:expr,
59 }
60 ) => {
61 $(#[$meta])*
62 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
63 #[serde(transparent)]
64 $vis struct $name(String);
65
66 impl $name {
67 #[doc = concat!("Create a new ", stringify!($name), " after validation")]
68 pub fn new($s_param: String) -> Result<Self, ValidationError> {
69 $validation?;
70 Ok(Self($s_param))
71 }
72
73 #[doc = concat!("Get the ", stringify!($name), " as a string slice")]
74 #[must_use]
75 #[inline]
76 pub fn as_str(&self) -> &str {
77 &self.0
78 }
79 }
80
81 impl AsRef<str> for $name {
82 #[inline]
83 fn as_ref(&self) -> &str {
84 &self.0
85 }
86 }
87
88 impl std::ops::Deref for $name {
89 type Target = str;
90
91 #[inline]
92 fn deref(&self) -> &Self::Target {
93 &self.0
94 }
95 }
96
97 impl fmt::Display for $name {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 write!(f, "{}", self.0)
100 }
101 }
102
103 impl TryFrom<String> for $name {
104 type Error = ValidationError;
105
106 fn try_from($s_param: String) -> Result<Self, Self::Error> {
107 Self::new($s_param)
108 }
109 }
110
111 impl<'de> Deserialize<'de> for $name {
112 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
113 where
114 D: serde::Deserializer<'de>,
115 {
116 let s = String::deserialize(deserializer)?;
117 Self::new(s).map_err(serde::de::Error::custom)
118 }
119 }
120 };
121}
122
123validated_string! {
126 #[doc(alias = "host")]
143 #[doc(alias = "domain")]
144 pub struct HostName(String) {
145 validation: |s| {
146 if s.trim().is_empty() {
147 Err(ValidationError::EmptyHostName)
148 } else {
149 Ok(())
150 }
151 },
152 }
153}
154
155validated_string! {
156 pub struct ServerName(String) {
158 validation: |s| {
159 if s.trim().is_empty() {
160 Err(ValidationError::EmptyServerName)
161 } else {
162 Ok(())
163 }
164 },
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
174 fn test_hostname_valid() {
175 let host = HostName::new("example.com".to_string()).unwrap();
176 assert_eq!(host.as_str(), "example.com");
177 }
178
179 #[test]
180 fn test_hostname_valid_ip() {
181 let host = HostName::new("192.168.1.1".to_string()).unwrap();
182 assert_eq!(host.as_str(), "192.168.1.1");
183 }
184
185 #[test]
186 fn test_hostname_valid_localhost() {
187 let host = HostName::new("localhost".to_string()).unwrap();
188 assert_eq!(host.as_str(), "localhost");
189 }
190
191 #[test]
192 fn test_hostname_valid_with_subdomain() {
193 let host = HostName::new("news.example.com".to_string()).unwrap();
194 assert_eq!(host.as_str(), "news.example.com");
195 }
196
197 #[test]
198 fn test_hostname_valid_with_port_notation() {
199 let host = HostName::new("example.com:119".to_string()).unwrap();
203 assert_eq!(host.as_str(), "example.com:119");
204 }
205
206 #[test]
207 fn test_hostname_empty_rejected() {
208 let result = HostName::new("".to_string());
209 assert!(matches!(result, Err(ValidationError::EmptyHostName)));
210 }
211
212 #[test]
213 fn test_hostname_whitespace_rejected() {
214 let result = HostName::new(" ".to_string());
215 assert!(matches!(result, Err(ValidationError::EmptyHostName)));
216 }
217
218 #[test]
219 fn test_hostname_tabs_rejected() {
220 let result = HostName::new("\t\t".to_string());
221 assert!(matches!(result, Err(ValidationError::EmptyHostName)));
222 }
223
224 #[test]
225 fn test_hostname_newlines_rejected() {
226 let result = HostName::new("\n\n".to_string());
227 assert!(matches!(result, Err(ValidationError::EmptyHostName)));
228 }
229
230 #[test]
231 fn test_hostname_mixed_whitespace_rejected() {
232 let result = HostName::new(" \t\n ".to_string());
233 assert!(matches!(result, Err(ValidationError::EmptyHostName)));
234 }
235
236 #[test]
237 fn test_hostname_display() {
238 let host = HostName::new("example.com".to_string()).unwrap();
239 assert_eq!(format!("{}", host), "example.com");
240 }
241
242 #[test]
243 fn test_hostname_as_ref() {
244 let host = HostName::new("example.com".to_string()).unwrap();
245 let s: &str = host.as_ref();
246 assert_eq!(s, "example.com");
247 }
248
249 #[test]
250 fn test_hostname_try_from() {
251 let result: Result<HostName, _> = "example.com".to_string().try_into();
252 assert!(result.is_ok());
253 assert_eq!(result.unwrap().as_str(), "example.com");
254 }
255
256 #[test]
257 fn test_hostname_try_from_empty() {
258 let result: Result<HostName, _> = "".to_string().try_into();
259 assert!(result.is_err());
260 }
261
262 #[test]
263 fn test_hostname_clone() {
264 let host1 = HostName::new("example.com".to_string()).unwrap();
265 let host2 = host1.clone();
266 assert_eq!(host1, host2);
267 }
268
269 #[test]
270 fn test_hostname_equality() {
271 let host1 = HostName::new("example.com".to_string()).unwrap();
272 let host2 = HostName::new("example.com".to_string()).unwrap();
273 let host3 = HostName::new("other.com".to_string()).unwrap();
274 assert_eq!(host1, host2);
275 assert_ne!(host1, host3);
276 }
277
278 #[test]
279 fn test_hostname_serde() {
280 let host = HostName::new("test.com".to_string()).unwrap();
281 let json = serde_json::to_string(&host).unwrap();
282 assert_eq!(json, "\"test.com\"");
283
284 let deserialized: HostName = serde_json::from_str(&json).unwrap();
285 assert_eq!(deserialized, host);
286 }
287
288 #[test]
289 fn test_hostname_serde_invalid() {
290 let json = "\"\"";
291 let result: Result<HostName, _> = serde_json::from_str(json);
292 assert!(result.is_err());
293 }
294
295 #[test]
296 fn test_hostname_serde_whitespace_rejected() {
297 let json = "\" \"";
298 let result: Result<HostName, _> = serde_json::from_str(json);
299 assert!(result.is_err());
300 }
301
302 #[test]
304 fn test_server_name_valid() {
305 let name = ServerName::new("backend-1".to_string()).unwrap();
306 assert_eq!(name.as_str(), "backend-1");
307 }
308
309 #[test]
310 fn test_server_name_valid_simple() {
311 let name = ServerName::new("server1".to_string()).unwrap();
312 assert_eq!(name.as_str(), "server1");
313 }
314
315 #[test]
316 fn test_server_name_valid_descriptive() {
317 let name = ServerName::new("Primary News Server".to_string()).unwrap();
318 assert_eq!(name.as_str(), "Primary News Server");
319 }
320
321 #[test]
322 fn test_server_name_valid_with_symbols() {
323 let name = ServerName::new("server_1-prod".to_string()).unwrap();
324 assert_eq!(name.as_str(), "server_1-prod");
325 }
326
327 #[test]
328 fn test_server_name_empty_rejected() {
329 let result = ServerName::new("".to_string());
330 assert!(matches!(result, Err(ValidationError::EmptyServerName)));
331 }
332
333 #[test]
334 fn test_server_name_whitespace_rejected() {
335 let result = ServerName::new(" ".to_string());
336 assert!(matches!(result, Err(ValidationError::EmptyServerName)));
337 }
338
339 #[test]
340 fn test_server_name_tabs_rejected() {
341 let result = ServerName::new("\t".to_string());
342 assert!(matches!(result, Err(ValidationError::EmptyServerName)));
343 }
344
345 #[test]
346 fn test_server_name_display() {
347 let name = ServerName::new("backend-1".to_string()).unwrap();
348 assert_eq!(format!("{}", name), "backend-1");
349 }
350
351 #[test]
352 fn test_server_name_as_ref() {
353 let name = ServerName::new("backend-1".to_string()).unwrap();
354 let s: &str = name.as_ref();
355 assert_eq!(s, "backend-1");
356 }
357
358 #[test]
359 fn test_server_name_try_from() {
360 let result: Result<ServerName, _> = "backend-1".to_string().try_into();
361 assert!(result.is_ok());
362 assert_eq!(result.unwrap().as_str(), "backend-1");
363 }
364
365 #[test]
366 fn test_server_name_try_from_empty() {
367 let result: Result<ServerName, _> = "".to_string().try_into();
368 assert!(result.is_err());
369 }
370
371 #[test]
372 fn test_server_name_clone() {
373 let name1 = ServerName::new("backend-1".to_string()).unwrap();
374 let name2 = name1.clone();
375 assert_eq!(name1, name2);
376 }
377
378 #[test]
379 fn test_server_name_equality() {
380 let name1 = ServerName::new("backend-1".to_string()).unwrap();
381 let name2 = ServerName::new("backend-1".to_string()).unwrap();
382 let name3 = ServerName::new("backend-2".to_string()).unwrap();
383 assert_eq!(name1, name2);
384 assert_ne!(name1, name3);
385 }
386
387 #[test]
388 fn test_server_name_serde() {
389 let name = ServerName::new("backend-1".to_string()).unwrap();
390 let json = serde_json::to_string(&name).unwrap();
391 assert_eq!(json, "\"backend-1\"");
392
393 let deserialized: ServerName = serde_json::from_str(&json).unwrap();
394 assert_eq!(deserialized, name);
395 }
396
397 #[test]
398 fn test_server_name_serde_invalid() {
399 let json = "\"\"";
400 let result: Result<ServerName, _> = serde_json::from_str(json);
401 assert!(result.is_err());
402 }
403
404 #[test]
406 fn test_validation_error_display_hostname() {
407 let error = ValidationError::EmptyHostName;
408 assert_eq!(
409 format!("{}", error),
410 "hostname cannot be empty or whitespace"
411 );
412 }
413
414 #[test]
415 fn test_validation_error_display_servername() {
416 let error = ValidationError::EmptyServerName;
417 assert_eq!(
418 format!("{}", error),
419 "server name cannot be empty or whitespace"
420 );
421 }
422
423 #[test]
424 fn test_validation_error_display_invalid_hostname() {
425 let error = ValidationError::InvalidHostName("bad-host".to_string());
426 assert!(format!("{}", error).contains("bad-host"));
427 }
428
429 #[test]
430 fn test_validation_error_equality() {
431 let error1 = ValidationError::EmptyHostName;
432 let error2 = ValidationError::EmptyHostName;
433 let error3 = ValidationError::EmptyServerName;
434 assert_eq!(error1, error2);
435 assert_ne!(error1, error3);
436 }
437
438 #[test]
439 fn test_validation_error_clone() {
440 let error1 = ValidationError::EmptyHostName;
441 let error2 = error1.clone();
442 assert_eq!(error1, error2);
443 }
444
445 #[test]
447 fn test_hostname_and_servername_different_types() {
448 let host = HostName::new("example.com".to_string()).unwrap();
449 let name = ServerName::new("example.com".to_string()).unwrap();
450 assert_eq!(host.as_str(), name.as_str());
452 }
453
454 #[test]
455 fn test_multiple_validations() {
456 let host1 = HostName::new("host1.com".to_string()).unwrap();
458 let host2 = HostName::new("host2.com".to_string()).unwrap();
459 let name1 = ServerName::new("server1".to_string()).unwrap();
460 let name2 = ServerName::new("server2".to_string()).unwrap();
461
462 assert_ne!(host1, host2);
463 assert_ne!(name1, name2);
464 }
465
466 #[test]
467 fn test_serde_roundtrip_hostname() {
468 let original = HostName::new("test.example.com".to_string()).unwrap();
469 let json = serde_json::to_string(&original).unwrap();
470 let deserialized: HostName = serde_json::from_str(&json).unwrap();
471 assert_eq!(original, deserialized);
472 }
473
474 #[test]
475 fn test_serde_roundtrip_servername() {
476 let original = ServerName::new("production-server-01".to_string()).unwrap();
477 let json = serde_json::to_string(&original).unwrap();
478 let deserialized: ServerName = serde_json::from_str(&json).unwrap();
479 assert_eq!(original, deserialized);
480 }
481}