siderite_validation/types/
mod.rs1use std::fmt;
4use std::ops::Deref;
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::FieldError;
9use crate::rules;
10
11mod decimal;
12mod float;
13mod int;
14mod ip;
15mod url;
16mod uuid;
17mod vec;
18
19pub use decimal::{Decimal, UnboundedDecimal};
20pub use float::{BoundedFloat, FloatBounds, NonNegative, Positive, UnitInterval};
21pub use int::{NegativeInt, NonNegativeInt};
22pub use ip::{IpAddress, Ipv4Address, Ipv6Address};
23pub use url::{HttpUrl, Url};
24pub use uuid::Uuid;
25pub use vec::ConstrainedVec;
26
27const REDACTED: &str = "**********";
28
29macro_rules! impl_wrapper {
34 (<$($gen:tt)+> $ty:ty => $inner:ty) => {
35 impl<$($gen)+> $ty {
36 pub fn into_inner(self) -> $inner {
38 self.0
39 }
40 }
41
42 impl<$($gen)+> AsRef<$inner> for $ty {
43 fn as_ref(&self) -> &$inner {
44 &self.0
45 }
46 }
47
48 impl<$($gen)+> ::std::ops::Deref for $ty {
49 type Target = $inner;
50
51 fn deref(&self) -> &Self::Target {
52 &self.0
53 }
54 }
55
56 impl<$($gen)+> ::std::fmt::Display for $ty
57 where
58 $inner: ::std::fmt::Display,
59 {
60 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
61 self.0.fmt(f)
62 }
63 }
64
65 impl<$($gen)+> From<$ty> for $inner {
66 fn from(value: $ty) -> Self {
67 value.0
68 }
69 }
70 };
71 ($ty:ty => $inner:ty) => {
72 impl $ty {
73 pub fn into_inner(self) -> $inner {
75 self.0
76 }
77 }
78
79 impl AsRef<$inner> for $ty {
80 fn as_ref(&self) -> &$inner {
81 &self.0
82 }
83 }
84
85 impl ::std::ops::Deref for $ty {
86 type Target = $inner;
87
88 fn deref(&self) -> &Self::Target {
89 &self.0
90 }
91 }
92
93 impl ::std::fmt::Display for $ty
94 where
95 $inner: ::std::fmt::Display,
96 {
97 fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
98 self.0.fmt(f)
99 }
100 }
101
102 impl From<$ty> for $inner {
103 fn from(value: $ty) -> Self {
104 value.0
105 }
106 }
107 };
108}
109pub(crate) use impl_wrapper;
110
111pub(super) fn prepare_as_string(
116 input: &mut serde_json::Value,
117 ctx: &mut crate::context::ValidationContext,
118) -> bool {
119 use crate::validate::Validate;
120 let before = ctx.error_count();
121 String::prepare(input, ctx);
122 ctx.error_count() == before
123}
124
125#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
127#[serde(try_from = "String", into = "String")]
128pub struct ConstrainedString<const MIN: usize, const MAX: usize>(String);
129
130impl<const MIN: usize, const MAX: usize> ConstrainedString<MIN, MAX> {
131 pub fn new(value: impl Into<String>) -> Result<Self, FieldError> {
133 let value = value.into();
134 rules::min_length(&value, MIN)?;
135 rules::max_length(&value, MAX)?;
136 Ok(Self(value))
137 }
138
139 pub fn into_inner(self) -> String {
141 self.0
142 }
143}
144
145impl<const MIN: usize, const MAX: usize> TryFrom<String> for ConstrainedString<MIN, MAX> {
146 type Error = FieldError;
147
148 fn try_from(value: String) -> Result<Self, Self::Error> {
149 Self::new(value)
150 }
151}
152
153impl<const MIN: usize, const MAX: usize> From<ConstrainedString<MIN, MAX>> for String {
154 fn from(value: ConstrainedString<MIN, MAX>) -> Self {
155 value.0
156 }
157}
158
159impl<const MIN: usize, const MAX: usize> AsRef<str> for ConstrainedString<MIN, MAX> {
160 fn as_ref(&self) -> &str {
161 &self.0
162 }
163}
164
165impl<const MIN: usize, const MAX: usize> Deref for ConstrainedString<MIN, MAX> {
166 type Target = str;
167
168 fn deref(&self) -> &Self::Target {
169 &self.0
170 }
171}
172
173impl<const MIN: usize, const MAX: usize> fmt::Display for ConstrainedString<MIN, MAX> {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 self.0.fmt(f)
176 }
177}
178
179#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
181#[serde(try_from = "i64", into = "i64")]
182pub struct BoundedI64<const MIN: i64, const MAX: i64>(i64);
183
184impl<const MIN: i64, const MAX: i64> BoundedI64<MIN, MAX> {
185 pub fn new(value: i64) -> Result<Self, FieldError> {
187 rules::ge(&value, &MIN)?;
188 rules::le(&value, &MAX)?;
189 Ok(Self(value))
190 }
191
192 pub fn into_inner(self) -> i64 {
194 self.0
195 }
196}
197
198impl<const MIN: i64, const MAX: i64> TryFrom<i64> for BoundedI64<MIN, MAX> {
199 type Error = FieldError;
200
201 fn try_from(value: i64) -> Result<Self, Self::Error> {
202 Self::new(value)
203 }
204}
205
206impl<const MIN: i64, const MAX: i64> From<BoundedI64<MIN, MAX>> for i64 {
207 fn from(value: BoundedI64<MIN, MAX>) -> Self {
208 value.0
209 }
210}
211
212impl<const MIN: i64, const MAX: i64> AsRef<i64> for BoundedI64<MIN, MAX> {
213 fn as_ref(&self) -> &i64 {
214 &self.0
215 }
216}
217
218impl<const MIN: i64, const MAX: i64> Deref for BoundedI64<MIN, MAX> {
219 type Target = i64;
220
221 fn deref(&self) -> &Self::Target {
222 &self.0
223 }
224}
225
226impl<const MIN: i64, const MAX: i64> fmt::Display for BoundedI64<MIN, MAX> {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 self.0.fmt(f)
229 }
230}
231
232#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
234#[serde(try_from = "i64", into = "i64")]
235pub struct PositiveInt(i64);
236
237impl PositiveInt {
238 pub fn new(value: i64) -> Result<Self, FieldError> {
240 rules::gt(&value, &0)?;
241 Ok(Self(value))
242 }
243
244 pub fn into_inner(self) -> i64 {
246 self.0
247 }
248}
249
250impl TryFrom<i64> for PositiveInt {
251 type Error = FieldError;
252
253 fn try_from(value: i64) -> Result<Self, Self::Error> {
254 Self::new(value)
255 }
256}
257
258impl From<PositiveInt> for i64 {
259 fn from(value: PositiveInt) -> Self {
260 value.0
261 }
262}
263
264impl AsRef<i64> for PositiveInt {
265 fn as_ref(&self) -> &i64 {
266 &self.0
267 }
268}
269
270impl Deref for PositiveInt {
271 type Target = i64;
272
273 fn deref(&self) -> &Self::Target {
274 &self.0
275 }
276}
277
278impl fmt::Display for PositiveInt {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 self.0.fmt(f)
281 }
282}
283
284#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
288#[serde(try_from = "String", into = "String")]
289pub struct Email(String);
290
291impl Email {
292 pub fn new(value: impl Into<String>) -> Result<Self, FieldError> {
294 let value = value.into();
295 rules::email(&value)?;
296 Ok(Self(value))
297 }
298
299 pub fn into_inner(self) -> String {
301 self.0
302 }
303}
304
305impl TryFrom<String> for Email {
306 type Error = FieldError;
307
308 fn try_from(value: String) -> Result<Self, Self::Error> {
309 Self::new(value)
310 }
311}
312
313impl From<Email> for String {
314 fn from(value: Email) -> Self {
315 value.0
316 }
317}
318
319impl AsRef<str> for Email {
320 fn as_ref(&self) -> &str {
321 &self.0
322 }
323}
324
325impl Deref for Email {
326 type Target = str;
327
328 fn deref(&self) -> &Self::Target {
329 &self.0
330 }
331}
332
333impl fmt::Display for Email {
334 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335 self.0.fmt(f)
336 }
337}
338
339#[derive(Clone, PartialEq, Eq, Deserialize)]
341#[serde(try_from = "String")]
342pub struct SecretString(String);
343
344impl SecretString {
345 pub fn new(value: impl Into<String>) -> Result<Self, FieldError> {
347 Ok(Self(value.into()))
348 }
349
350 pub fn into_inner(self) -> String {
352 self.0
353 }
354
355 pub fn expose_secret(&self) -> &str {
357 &self.0
358 }
359}
360
361impl TryFrom<String> for SecretString {
362 type Error = FieldError;
363
364 fn try_from(value: String) -> Result<Self, Self::Error> {
365 Self::new(value)
366 }
367}
368
369impl Serialize for SecretString {
370 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
371 where
372 S: serde::Serializer,
373 {
374 serializer.serialize_str(REDACTED)
375 }
376}
377
378impl fmt::Debug for SecretString {
379 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380 f.write_str(REDACTED)
381 }
382}
383
384impl fmt::Display for SecretString {
385 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386 f.write_str(REDACTED)
387 }
388}
389
390impl<const MIN: usize, const MAX: usize> crate::dump::Dump for ConstrainedString<MIN, MAX> {}
391impl<const MIN: i64, const MAX: i64> crate::dump::Dump for BoundedI64<MIN, MAX> {}
392impl crate::dump::Dump for PositiveInt {}
393impl crate::dump::Dump for Email {}
394impl crate::dump::Dump for SecretString {}
395
396#[cfg(test)]
397pub(super) fn prepare_codes<T: crate::validate::Validate>(
398 mut input: serde_json::Value,
399 mut ctx: crate::context::ValidationContext,
400) -> (serde_json::Value, Vec<String>) {
401 T::prepare(&mut input, &mut ctx);
402 let codes = ctx
403 .take_errors()
404 .errors
405 .into_iter()
406 .map(|e| e.code.into_owned())
407 .collect();
408 (input, codes)
409}
410
411#[cfg(test)]
412mod tests {
413 #![allow(clippy::unwrap_used)]
414
415 use super::*;
416
417 type Username = ConstrainedString<2, 8>;
418 type Percent = BoundedI64<0, 100>;
419
420 #[test]
421 fn constrained_string_success_and_char_length() {
422 let value = Username::new("éé").unwrap();
423 assert_eq!(value.as_ref(), "éé");
424 assert_eq!(value.into_inner(), "éé");
425 assert_eq!(Username::new("é").unwrap_err().code, "string_too_short");
426 assert_eq!(
427 Username::new("abcdefghij").unwrap_err().code,
428 "string_too_long"
429 );
430 }
431
432 #[test]
433 fn constrained_string_serde_validates() {
434 let value: Username = serde_json::from_str("\"abcd\"").unwrap();
435 assert_eq!(&*value, "abcd");
436 assert_eq!(serde_json::to_string(&value).unwrap(), "\"abcd\"");
437 assert!(serde_json::from_str::<Username>("\"x\"").is_err());
438 assert!(serde_json::from_str::<Username>("\"toolongname\"").is_err());
439 }
440
441 #[test]
442 fn bounded_i64_success_and_bounds() {
443 let value = Percent::new(50).unwrap();
444 assert_eq!(value.into_inner(), 50);
445 assert_eq!(*Percent::new(0).unwrap(), 0);
446 assert_eq!(*Percent::new(100).unwrap(), 100);
447 assert_eq!(Percent::new(-1).unwrap_err().code, "greater_than_equal");
448 assert_eq!(Percent::new(101).unwrap_err().code, "less_than_equal");
449 }
450
451 #[test]
452 fn bounded_i64_serde_validates() {
453 let value: Percent = serde_json::from_str("42").unwrap();
454 assert_eq!(i64::from(value), 42);
455 assert_eq!(serde_json::to_string(&value).unwrap(), "42");
456 assert!(serde_json::from_str::<Percent>("-1").is_err());
457 assert!(serde_json::from_str::<Percent>("101").is_err());
458 }
459
460 #[test]
461 fn positive_int_rejects_zero_and_negative() {
462 assert_eq!(PositiveInt::new(1).unwrap().into_inner(), 1);
463 assert_eq!(PositiveInt::new(0).unwrap_err().code, "greater_than");
464 assert_eq!(PositiveInt::new(-3).unwrap_err().code, "greater_than");
465 assert!(serde_json::from_str::<PositiveInt>("0").is_err());
466 let value: PositiveInt = serde_json::from_str("7").unwrap();
467 assert_eq!(serde_json::to_string(&value).unwrap(), "7");
468 }
469
470 #[test]
471 fn email_construction_and_serde() {
472 let email = Email::new("user@example.com").unwrap();
473 assert_eq!(email.as_ref(), "user@example.com");
474 assert_eq!(email.to_string(), "user@example.com");
475 assert_eq!(Email::new("nope").unwrap_err().code, "invalid_email");
476 let round: Email = serde_json::from_str("\"a@b.c\"").unwrap();
477 assert_eq!(&*round, "a@b.c");
478 assert_eq!(serde_json::to_string(&round).unwrap(), "\"a@b.c\"");
479 assert!(serde_json::from_str::<Email>("\"not-an-email\"").is_err());
480 }
481
482 #[test]
483 fn secret_string_redacts_everywhere() {
484 let secret = SecretString::new("hunter2").unwrap();
485 assert_eq!(secret.expose_secret(), "hunter2");
486 assert_eq!(format!("{secret}"), REDACTED);
487 assert_eq!(format!("{secret:?}"), REDACTED);
488 assert_eq!(serde_json::to_string(&secret).unwrap(), "\"**********\"");
489 let loaded: SecretString = serde_json::from_str("\"hunter2\"").unwrap();
490 assert_eq!(loaded.expose_secret(), "hunter2");
491 assert_eq!(loaded.into_inner(), "hunter2");
492 }
493}