1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
9#[non_exhaustive]
10pub struct ValidationError(pub String);
11
12impl fmt::Display for ValidationError {
13 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14 f.write_str(&self.0)
15 }
16}
17
18impl std::error::Error for ValidationError {}
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(transparent)]
28#[non_exhaustive]
29pub struct Id(String);
30
31#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
38#[serde(transparent)]
39#[non_exhaustive]
40pub struct UTCDate(String);
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(transparent)]
55#[non_exhaustive]
56pub struct Date(String);
57
58#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
65#[serde(transparent)]
66#[non_exhaustive]
67pub struct State(String);
68
69macro_rules! impl_string_newtype {
73 ($T:ident) => {
74 impl fmt::Display for $T {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(&self.0)
77 }
78 }
79 impl From<String> for $T {
80 fn from(s: String) -> Self {
81 Self(s)
82 }
83 }
84 impl From<&str> for $T {
85 fn from(s: &str) -> Self {
86 Self(s.to_owned())
87 }
88 }
89 impl AsRef<str> for $T {
90 fn as_ref(&self) -> &str {
91 &self.0
92 }
93 }
94 impl PartialEq<str> for $T {
95 fn eq(&self, other: &str) -> bool {
96 self.0 == other
97 }
98 }
99 impl PartialEq<&str> for $T {
100 fn eq(&self, other: &&str) -> bool {
101 self.0 == *other
102 }
103 }
104 impl std::borrow::Borrow<str> for $T {
105 fn borrow(&self) -> &str {
106 &self.0
107 }
108 }
109 impl $T {
110 pub fn into_inner(self) -> String {
112 self.0
113 }
114 }
115 };
116}
117
118impl_string_newtype!(Id);
119impl_string_newtype!(UTCDate);
120impl_string_newtype!(Date);
121impl_string_newtype!(State);
122
123fn validate_id(s: &str) -> Result<(), ValidationError> {
139 if s.is_empty() {
140 return Err(ValidationError("Id must not be empty".into()));
141 }
142 if s.len() > 255 {
143 return Err(ValidationError(format!(
144 "Id exceeds 255 bytes (got {})",
145 s.len()
146 )));
147 }
148 for ch in s.chars() {
149 let b = ch as u32;
150 if !(0x21..=0x7E).contains(&b) || b == 0x22 {
152 return Err(ValidationError(format!(
153 "Id contains invalid character {:?} (U+{b:04X})",
154 ch
155 )));
156 }
157 }
158 Ok(())
159}
160
161fn validate_utcdate(s: &str) -> Result<(), ValidationError> {
166 if s.len() != 20 {
167 return Err(ValidationError(format!(
168 "UTCDate must be exactly 20 characters (YYYY-MM-DDTHH:MM:SSZ), got {:?}",
169 s
170 )));
171 }
172 let b = s.as_bytes();
173 if b[4] != b'-'
175 || b[7] != b'-'
176 || b[10] != b'T'
177 || b[13] != b':'
178 || b[16] != b':'
179 || b[19] != b'Z'
180 {
181 return Err(ValidationError(format!(
182 "UTCDate has wrong structure, expected YYYY-MM-DDTHH:MM:SSZ, got {:?}",
183 s
184 )));
185 }
186 for &pos in &[0, 1, 2, 3, 5, 6, 8, 9, 11, 12, 14, 15, 17, 18] {
189 if !b[pos].is_ascii_digit() {
190 return Err(ValidationError(format!(
191 "UTCDate position {} is not a digit in {:?}",
192 pos, s
193 )));
194 }
195 }
196 Ok(())
197}
198
199fn validate_state(s: &str) -> Result<(), ValidationError> {
204 if s.is_empty() {
205 return Err(ValidationError("State must not be empty".into()));
206 }
207 Ok(())
208}
209
210impl Id {
211 pub fn new_validated(s: impl Into<String>) -> Result<Self, ValidationError> {
220 let s = s.into();
221 validate_id(&s)?;
222 Ok(Self(s))
223 }
224}
225
226impl UTCDate {
227 pub fn new_validated(s: impl Into<String>) -> Result<Self, ValidationError> {
234 let s = s.into();
235 validate_utcdate(&s)?;
236 Ok(Self(s))
237 }
238}
239
240impl State {
241 pub fn new_validated(s: impl Into<String>) -> Result<Self, ValidationError> {
248 let s = s.into();
249 validate_state(&s)?;
250 Ok(Self(s))
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
260 fn id_serializes_as_plain_string() {
261 let id = Id("abc123".to_owned());
262 let json = serde_json::to_string(&id).expect("serialize Id");
263 assert_eq!(json, "\"abc123\"");
264 }
265
266 #[test]
268 fn id_deserializes_from_plain_string() {
269 let id: Id = serde_json::from_str("\"abc123\"").expect("deserialize Id");
270 assert_eq!(id.as_ref(), "abc123");
271 }
272
273 #[test]
275 fn utcdate_serializes_as_plain_string() {
276 let d = UTCDate("2014-10-30T06:12:00Z".to_owned());
277 let json = serde_json::to_string(&d).expect("serialize UTCDate");
278 assert_eq!(json, "\"2014-10-30T06:12:00Z\"");
279 }
280
281 #[test]
283 fn state_serializes_as_plain_string() {
284 let s = State("75128aab4b1b".to_owned());
285 let json = serde_json::to_string(&s).expect("serialize State");
286 assert_eq!(json, "\"75128aab4b1b\"");
287 }
288
289 #[test]
291 fn id_from_str() {
292 let id = Id::from("hello");
293 assert_eq!(id.as_ref(), "hello");
294 }
295
296 #[test]
298 fn id_display() {
299 let id = Id("display-test".to_owned());
300 assert_eq!(id.to_string(), "display-test");
301 }
302
303 #[test]
305 fn id_as_ref_str() {
306 let id = Id("ref-test".to_owned());
307 assert_eq!(id.as_ref(), "ref-test");
308 }
309
310 #[test]
312 fn state_round_trip() {
313 let s = State("75128aab4b1b".to_owned());
314 let json = serde_json::to_string(&s).expect("serialize");
315 let s2: State = serde_json::from_str(&json).expect("deserialize");
316 assert_eq!(s, s2);
317 }
318
319 #[test]
321 fn date_accepts_non_utc_offset() {
322 let d = Date("2014-10-30T14:12:00+08:00".to_owned());
323 let json = serde_json::to_string(&d).expect("serialize Date");
324 assert_eq!(json, "\"2014-10-30T14:12:00+08:00\"");
325 let d2: Date = serde_json::from_str(&json).expect("deserialize Date");
326 assert_eq!(d, d2);
327 }
328
329 #[test]
336 fn id_new_validated_empty_fails() {
337 let err = Id::new_validated("").unwrap_err();
338 assert!(err.0.contains("empty"), "error must mention 'empty': {err}");
339 }
340
341 #[test]
343 fn id_new_validated_space_fails() {
344 let err = Id::new_validated("has space").unwrap_err();
345 assert!(err.0.contains("invalid character"), "{err}");
346 }
347
348 #[test]
350 fn id_new_validated_dquote_fails() {
351 let err = Id::new_validated("has\"quote").unwrap_err();
352 assert!(err.0.contains("invalid character"), "{err}");
353 }
354
355 #[test]
357 fn id_new_validated_control_char_fails() {
358 let err = Id::new_validated("has\x01ctrl").unwrap_err();
359 assert!(err.0.contains("invalid character"), "{err}");
360 }
361
362 #[test]
364 fn id_new_validated_too_long_fails() {
365 let long = "a".repeat(256);
366 assert!(Id::new_validated(long).is_err());
367 }
368
369 #[test]
371 fn id_new_validated_valid_succeeds() {
372 let id = Id::new_validated("abc123-_ABC").expect("valid Id must succeed");
373 assert_eq!(id.as_ref(), "abc123-_ABC");
374 }
375
376 #[test]
378 fn id_new_validated_max_length_succeeds() {
379 let id255 = "a".repeat(255);
380 Id::new_validated(id255).expect("255-byte Id must succeed");
381 }
382
383 #[test]
385 fn utcdate_new_validated_valid_succeeds() {
386 let d = UTCDate::new_validated("2014-10-30T06:12:00Z").expect("valid UTCDate must succeed");
387 assert_eq!(d.as_ref(), "2014-10-30T06:12:00Z");
388 }
389
390 #[test]
392 fn utcdate_new_validated_no_z_fails() {
393 assert!(UTCDate::new_validated("2014-10-30T06:12:00+00:00").is_err());
394 }
395
396 #[test]
398 fn utcdate_new_validated_empty_fails() {
399 assert!(UTCDate::new_validated("").is_err());
400 }
401
402 #[test]
404 fn utcdate_new_validated_wrong_length_fails() {
405 assert!(UTCDate::new_validated("2014-10-30").is_err());
406 assert!(UTCDate::new_validated("2014-10-30T06:12:00.000Z").is_err());
408 }
409
410 #[test]
412 fn utcdate_new_validated_non_digit_fails() {
413 assert!(UTCDate::new_validated("XXXX-10-30T06:12:00Z").is_err());
414 }
415
416 #[test]
418 fn state_new_validated_empty_fails() {
419 let err = State::new_validated("").unwrap_err();
420 assert!(err.0.contains("empty"), "{err}");
421 }
422
423 #[test]
425 fn state_new_validated_valid_succeeds() {
426 let s = State::new_validated("75128aab4b1b").expect("valid State must succeed");
427 assert_eq!(s.as_ref(), "75128aab4b1b");
428 }
429
430 #[test]
432 fn validation_error_implements_error() {
433 let e = Id::new_validated("").unwrap_err();
434 let _: &dyn std::error::Error = &e;
435 assert!(!e.to_string().is_empty(), "error message must not be empty");
436 assert_eq!(format!("{e}"), e.0, "Display must show the inner message");
437 }
438}