nntp_proxy/types/
validated.rs1use serde::{Deserialize, Serialize};
4use std::fmt;
5use std::path::{Path, PathBuf};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Error, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum ValidationError {
12 #[error("hostname cannot be empty or whitespace")]
13 EmptyHostName,
14 #[error("server name cannot be empty or whitespace")]
15 EmptyServerName,
16 #[error("invalid hostname: {0}")]
17 InvalidHostName(String),
18 #[error("port cannot be 0")]
19 InvalidPort,
20 #[error("invalid message ID: {0}")]
21 InvalidMessageId(String),
22 #[error("config path cannot be empty")]
23 EmptyConfigPath,
24 #[error("username cannot be empty or whitespace")]
25 EmptyUsername,
26 #[error("password cannot be empty or whitespace")]
27 EmptyPassword,
28}
29
30macro_rules! validated_string {
32 ($(#[$meta:meta])* $vis:vis struct $name:ident($err:path);) => {
33 $(#[$meta])*
34 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
35 #[serde(transparent)]
36 $vis struct $name(String);
37
38 impl $name {
39 pub fn new(s: String) -> Result<Self, ValidationError> {
40 if s.trim().is_empty() {
41 Err($err)
42 } else {
43 Ok(Self(s))
44 }
45 }
46
47 #[must_use]
48 #[inline]
49 pub fn as_str(&self) -> &str {
50 &self.0
51 }
52 }
53
54 impl AsRef<str> for $name {
55 #[inline]
56 fn as_ref(&self) -> &str {
57 &self.0
58 }
59 }
60
61 impl std::ops::Deref for $name {
62 type Target = str;
63 #[inline]
64 fn deref(&self) -> &Self::Target {
65 &self.0
66 }
67 }
68
69 impl fmt::Display for $name {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(&self.0)
72 }
73 }
74
75 impl TryFrom<String> for $name {
76 type Error = ValidationError;
77 fn try_from(s: String) -> Result<Self, Self::Error> {
78 Self::new(s)
79 }
80 }
81
82 impl<'de> Deserialize<'de> for $name {
83 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
84 where
85 D: serde::Deserializer<'de>,
86 {
87 let s = String::deserialize(deserializer)?;
88 Self::new(s).map_err(serde::de::Error::custom)
89 }
90 }
91 };
92}
93
94validated_string! {
95 pub struct HostName(ValidationError::EmptyHostName);
97}
98
99validated_string! {
100 pub struct ServerName(ValidationError::EmptyServerName);
102}
103
104validated_string! {
105 pub struct Username(ValidationError::EmptyUsername);
107}
108
109validated_string! {
110 pub struct Password(ValidationError::EmptyPassword);
112}
113
114#[derive(Debug, Clone, PartialEq, Eq, Hash)]
116pub struct ConfigPath(PathBuf);
117
118impl ConfigPath {
119 pub fn new(path: impl AsRef<Path>) -> Result<Self, ValidationError> {
120 let path_ref = path.as_ref();
121 let path_str = path_ref.to_str().ok_or(ValidationError::EmptyConfigPath)?;
122 if path_str.trim().is_empty() {
123 return Err(ValidationError::EmptyConfigPath);
124 }
125 Ok(Self(path_ref.to_path_buf()))
126 }
127
128 #[must_use]
129 #[inline]
130 pub fn as_path(&self) -> &Path {
131 &self.0
132 }
133
134 #[must_use]
135 #[inline]
136 pub fn as_str(&self) -> &str {
137 self.0.to_str().unwrap_or("")
138 }
139}
140
141impl AsRef<Path> for ConfigPath {
142 #[inline]
143 fn as_ref(&self) -> &Path {
144 &self.0
145 }
146}
147
148impl fmt::Display for ConfigPath {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 write!(f, "{}", self.0.display())
151 }
152}
153
154impl std::str::FromStr for ConfigPath {
155 type Err = ValidationError;
156 fn from_str(s: &str) -> Result<Self, Self::Err> {
157 Self::new(s)
158 }
159}
160
161impl TryFrom<String> for ConfigPath {
162 type Error = ValidationError;
163 fn try_from(s: String) -> Result<Self, Self::Error> {
164 Self::new(s)
165 }
166}
167
168impl TryFrom<&str> for ConfigPath {
169 type Error = ValidationError;
170 fn try_from(s: &str) -> Result<Self, Self::Error> {
171 Self::new(s)
172 }
173}
174
175impl Serialize for ConfigPath {
176 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177 where
178 S: serde::Serializer,
179 {
180 serializer.serialize_str(self.as_str())
181 }
182}
183
184impl<'de> Deserialize<'de> for ConfigPath {
185 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
186 where
187 D: serde::Deserializer<'de>,
188 {
189 let s = String::deserialize(deserializer)?;
190 Self::new(s).map_err(serde::de::Error::custom)
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 #[test]
199 fn test_hostname_validation() {
200 assert!(HostName::new("example.com".to_string()).is_ok());
201 assert!(matches!(
202 HostName::new("".to_string()),
203 Err(ValidationError::EmptyHostName)
204 ));
205 assert!(matches!(
206 HostName::new(" ".to_string()),
207 Err(ValidationError::EmptyHostName)
208 ));
209 }
210
211 #[test]
212 fn test_server_name_validation() {
213 assert!(ServerName::new("backend-1".to_string()).is_ok());
214 assert!(matches!(
215 ServerName::new("".to_string()),
216 Err(ValidationError::EmptyServerName)
217 ));
218 assert!(matches!(
219 ServerName::new("\t".to_string()),
220 Err(ValidationError::EmptyServerName)
221 ));
222 }
223
224 #[test]
225 fn test_username_validation() {
226 assert!(Username::new("alice".to_string()).is_ok());
227 assert!(matches!(
228 Username::new("".to_string()),
229 Err(ValidationError::EmptyUsername)
230 ));
231 }
232
233 #[test]
234 fn test_password_validation() {
235 assert!(Password::new("secret".to_string()).is_ok());
236 assert!(matches!(
237 Password::new("".to_string()),
238 Err(ValidationError::EmptyPassword)
239 ));
240 }
241
242 #[test]
243 fn test_config_path_validation() {
244 assert!(ConfigPath::try_from("config.toml").is_ok());
245 assert!(matches!(
246 ConfigPath::try_from(""),
247 Err(ValidationError::EmptyConfigPath)
248 ));
249 }
250}