1use nutype::nutype;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9#[derive(Debug, Clone, Error, PartialEq, Eq)]
11#[non_exhaustive]
12pub enum ValidationError {
13 #[error("hostname cannot be empty or whitespace")]
14 EmptyHostName,
15 #[error("server name cannot be empty or whitespace")]
16 EmptyServerName,
17 #[error("invalid hostname: {0}")]
18 InvalidHostName(String),
19 #[error("port cannot be 0")]
20 InvalidPort,
21 #[error("invalid message ID: {0}")]
22 InvalidMessageId(String),
23 #[error("config path cannot be empty")]
24 EmptyConfigPath,
25 #[error("username cannot be empty or whitespace")]
26 EmptyUsername,
27 #[error("password cannot be empty or whitespace")]
28 EmptyPassword,
29}
30
31#[nutype(
33 sanitize(trim),
34 validate(not_empty),
35 derive(
36 Debug,
37 Clone,
38 PartialEq,
39 Eq,
40 Hash,
41 Display,
42 AsRef,
43 Deref,
44 TryFrom,
45 Serialize,
46 Deserialize
47 )
48)]
49pub struct HostName(String);
50
51#[nutype(
53 sanitize(trim),
54 validate(not_empty),
55 derive(
56 Debug,
57 Clone,
58 PartialEq,
59 Eq,
60 Hash,
61 Display,
62 AsRef,
63 Deref,
64 TryFrom,
65 Serialize,
66 Deserialize
67 )
68)]
69pub struct ServerName(String);
70
71#[nutype(
73 sanitize(trim),
74 validate(not_empty),
75 derive(
76 Debug,
77 Clone,
78 PartialEq,
79 Eq,
80 Hash,
81 Display,
82 AsRef,
83 Deref,
84 TryFrom,
85 Serialize,
86 Deserialize
87 )
88)]
89pub struct Username(String);
90
91#[nutype(
93 sanitize(trim),
94 validate(not_empty),
95 derive(
96 Debug,
97 Clone,
98 PartialEq,
99 Eq,
100 Hash,
101 Display,
102 AsRef,
103 Deref,
104 TryFrom,
105 Serialize,
106 Deserialize
107 )
108)]
109pub struct Password(String);
110
111impl From<HostNameError> for ValidationError {
113 fn from(_: HostNameError) -> Self {
114 Self::EmptyHostName
115 }
116}
117
118impl From<ServerNameError> for ValidationError {
119 fn from(_: ServerNameError) -> Self {
120 Self::EmptyServerName
121 }
122}
123
124impl From<UsernameError> for ValidationError {
125 fn from(_: UsernameError) -> Self {
126 Self::EmptyUsername
127 }
128}
129
130impl From<PasswordError> for ValidationError {
131 fn from(_: PasswordError) -> Self {
132 Self::EmptyPassword
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Hash)]
138pub struct ConfigPath(PathBuf);
139
140impl ConfigPath {
141 pub fn new(path: impl AsRef<Path>) -> Result<Self, ValidationError> {
147 let path_ref = path.as_ref();
148 let path_str = path_ref.to_str().ok_or(ValidationError::EmptyConfigPath)?;
149 if path_str.trim().is_empty() {
150 return Err(ValidationError::EmptyConfigPath);
151 }
152 Ok(Self(path_ref.to_path_buf()))
153 }
154
155 #[must_use]
156 #[inline]
157 pub fn as_path(&self) -> &Path {
158 &self.0
159 }
160
161 #[must_use]
162 #[inline]
163 pub fn as_str(&self) -> &str {
164 self.0.to_str().unwrap_or("")
165 }
166}
167
168impl AsRef<Path> for ConfigPath {
169 #[inline]
170 fn as_ref(&self) -> &Path {
171 &self.0
172 }
173}
174
175impl fmt::Display for ConfigPath {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 write!(f, "{}", self.0.display())
178 }
179}
180
181impl std::str::FromStr for ConfigPath {
182 type Err = ValidationError;
183 fn from_str(s: &str) -> Result<Self, Self::Err> {
184 Self::new(s)
185 }
186}
187
188impl TryFrom<String> for ConfigPath {
189 type Error = ValidationError;
190 fn try_from(s: String) -> Result<Self, Self::Error> {
191 Self::new(s)
192 }
193}
194
195impl TryFrom<&str> for ConfigPath {
196 type Error = ValidationError;
197 fn try_from(s: &str) -> Result<Self, Self::Error> {
198 Self::new(s)
199 }
200}
201
202impl Serialize for ConfigPath {
203 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
204 where
205 S: serde::Serializer,
206 {
207 serializer.serialize_str(self.as_str())
208 }
209}
210
211impl<'de> Deserialize<'de> for ConfigPath {
212 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
213 where
214 D: serde::Deserializer<'de>,
215 {
216 let s = String::deserialize(deserializer)?;
217 Self::new(s).map_err(serde::de::Error::custom)
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use proptest::prelude::*;
225
226 fn non_empty_string() -> impl Strategy<Value = String> {
228 "[a-zA-Z0-9._@-]{1,100}"
229 }
230
231 fn path_string() -> impl Strategy<Value = String> {
232 "[a-zA-Z0-9._/-]{1,100}"
233 }
234
235 proptest! {
237 #[test]
238 fn hostname_serde_roundtrip(s in non_empty_string()) {
239 let hostname = HostName::try_new(s).unwrap();
240 let json = serde_json::to_string(&hostname).unwrap();
241 let deserialized: HostName = serde_json::from_str(&json).unwrap();
242 prop_assert_eq!(hostname, deserialized);
243 }
244 }
245
246 proptest! {
248 #[test]
249 fn server_name_serde_roundtrip(s in non_empty_string()) {
250 let server = ServerName::try_new(s).unwrap();
251 let json = serde_json::to_string(&server).unwrap();
252 let deserialized: ServerName = serde_json::from_str(&json).unwrap();
253 prop_assert_eq!(server, deserialized);
254 }
255 }
256
257 proptest! {
259 #[test]
260 fn username_serde_roundtrip(s in non_empty_string()) {
261 let username = Username::try_new(s).unwrap();
262 let json = serde_json::to_string(&username).unwrap();
263 let deserialized: Username = serde_json::from_str(&json).unwrap();
264 prop_assert_eq!(username, deserialized);
265 }
266 }
267
268 proptest! {
270 #[test]
271 fn password_serde_roundtrip(s in non_empty_string()) {
272 let password = Password::try_new(s).unwrap();
273 let json = serde_json::to_string(&password).unwrap();
274 let deserialized: Password = serde_json::from_str(&json).unwrap();
275 prop_assert_eq!(password, deserialized);
276 }
277 }
278
279 proptest! {
281 #[test]
282 fn config_path_non_empty_accepts(s in path_string()) {
283 let config = ConfigPath::try_from(s.clone()).unwrap();
284 prop_assert_eq!(config.as_str(), &s);
285 }
286
287 #[test]
288 fn config_path_as_path_roundtrip(s in path_string()) {
289 let config = ConfigPath::try_from(s.clone()).unwrap();
290 let path: &Path = config.as_ref();
291 prop_assert_eq!(path, Path::new(&s));
292 }
293
294 #[test]
295 fn config_path_serde_roundtrip(s in path_string()) {
296 let config = ConfigPath::try_from(s).unwrap();
297 let json = serde_json::to_string(&config).unwrap();
298 let deserialized: ConfigPath = serde_json::from_str(&json).unwrap();
299 prop_assert_eq!(config, deserialized);
300 }
301 }
302
303 #[test]
305 fn username_with_spaces_accepted() {
306 assert!(Username::try_new(" user ".to_string()).is_ok());
308 assert!(Username::try_new("user name".to_string()).is_ok());
309 }
310
311 #[test]
312 fn password_with_spaces_accepted() {
313 assert!(Password::try_new(" pass ".to_string()).is_ok());
314 assert!(Password::try_new("P@ssw0rd!".to_string()).is_ok());
315 assert!(Password::try_new("密码123".to_string()).is_ok());
316 }
317
318 #[test]
319 fn config_path_with_spaces_accepted() {
320 assert!(ConfigPath::try_from("my config.toml").is_ok());
321 assert!(ConfigPath::try_from("/absolute/path/config.toml").is_ok());
322 assert!(ConfigPath::try_from("./relative/config.toml").is_ok());
323 assert!(ConfigPath::try_from("../parent/config.toml").is_ok());
324 }
325
326 #[test]
328 fn hostname_deserialize_empty_fails() {
329 let json = "\"\"";
330 let result: Result<HostName, _> = serde_json::from_str(json);
331 assert!(result.is_err());
332 }
333
334 #[test]
335 fn config_path_deserialize_empty_fails() {
336 let json = "\"\"";
337 let result: Result<ConfigPath, _> = serde_json::from_str(json);
338 assert!(result.is_err());
339 }
340
341 #[test]
343 fn validation_error_messages() {
344 assert_eq!(
345 ValidationError::EmptyHostName.to_string(),
346 "hostname cannot be empty or whitespace"
347 );
348 assert_eq!(
349 ValidationError::EmptyServerName.to_string(),
350 "server name cannot be empty or whitespace"
351 );
352 assert_eq!(
353 ValidationError::EmptyUsername.to_string(),
354 "username cannot be empty or whitespace"
355 );
356 assert_eq!(
357 ValidationError::EmptyPassword.to_string(),
358 "password cannot be empty or whitespace"
359 );
360 assert_eq!(
361 ValidationError::EmptyConfigPath.to_string(),
362 "config path cannot be empty"
363 );
364 }
365
366 #[test]
367 fn validation_error_clone() {
368 let err = ValidationError::EmptyHostName;
369 let cloned = err.clone();
370 assert_eq!(err, cloned);
371 }
372}