Skip to main content

siderite_validation/types/
mod.rs

1//! Constrained newtypes that validate on construction and on deserialize.
2
3use 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
29/// Shared `into_inner` / `AsRef` / `Deref` / `Display` / `From` for wrapper newtypes.
30///
31/// `TryFrom` is left to each type because the input of `new` is not always
32/// the inner stored type (string-parsed wrappers store a parsed value).
33macro_rules! impl_wrapper {
34    (<$($gen:tt)+> $ty:ty => $inner:ty) => {
35        impl<$($gen)+> $ty {
36            /// Unwrap the inner value.
37            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            /// Unwrap the inner value.
74            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
111/// Run [`String`]'s prepare (type check + model string transforms).
112///
113/// Returns `true` when no new error was recorded, so callers can apply a
114/// constraint check to the (possibly rewritten) string slot.
115pub(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/// UTF-8 string whose character length is in `MIN..=MAX`.
126#[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    /// Validate `value` and wrap it.
132    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    /// Unwrap the inner string.
140    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/// Signed 64-bit integer whose value is in `MIN..=MAX`.
180#[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    /// Validate `value` and wrap it.
186    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    /// Unwrap the inner integer.
193    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/// Strictly positive `i64` (`value > 0`).
233#[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    /// Validate `value > 0` and wrap it.
239    pub fn new(value: i64) -> Result<Self, FieldError> {
240        rules::gt(&value, &0)?;
241        Ok(Self(value))
242    }
243
244    /// Unwrap the inner integer.
245    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/// Email address validated by the pragmatic [`rules::email`] check.
285///
286/// Construction does **not** implement full RFC 5322.
287#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
288#[serde(try_from = "String", into = "String")]
289pub struct Email(String);
290
291impl Email {
292    /// Validate `value` as an email and wrap it.
293    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    /// Unwrap the inner string.
300    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/// Opaque string that redacts its contents in `Debug`, `Display`, and `Serialize`.
340#[derive(Clone, PartialEq, Eq, Deserialize)]
341#[serde(try_from = "String")]
342pub struct SecretString(String);
343
344impl SecretString {
345    /// Wrap `value` without further validation.
346    pub fn new(value: impl Into<String>) -> Result<Self, FieldError> {
347        Ok(Self(value.into()))
348    }
349
350    /// Unwrap the inner string.
351    pub fn into_inner(self) -> String {
352        self.0
353    }
354
355    /// Borrow the secret in plaintext.
356    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}