ascii_domain

Domains whose labels are only ASCII.
git clone https://git.philomathiclife.com/repos/ascii_domain
Log | Files | Refs | README

serde.rs (10103B)


      1 extern crate alloc;
      2 use crate::{
      3     char_set::{AllowedAscii, PRINTABLE_ASCII},
      4     dom::{Domain, DomainErr, Rfc1123Domain, Rfc1123Err},
      5 };
      6 use alloc::{borrow::ToOwned as _, string::String};
      7 use core::{
      8     fmt::{self, Formatter},
      9     marker::PhantomData,
     10 };
     11 use serde::{
     12     de::{self, Deserialize, Deserializer, Unexpected, Visitor},
     13     ser::{Serialize, Serializer},
     14 };
     15 /// The "default" `AllowedAscii` that is used for `Domain`.
     16 static DOMAIN_CHARS: &AllowedAscii<[u8; 92]> = &PRINTABLE_ASCII;
     17 impl<T: AsRef<[u8]>> Serialize for Domain<T> {
     18     /// Serializes `Domain` as a string.
     19     #[inline]
     20     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     21     where
     22         S: Serializer,
     23     {
     24         serializer.serialize_str(self.as_str())
     25     }
     26 }
     27 impl<T: AsRef<[u8]>> Serialize for Rfc1123Domain<T> {
     28     /// Serializes `Rfc1123Domain` as a string.
     29     #[inline]
     30     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
     31     where
     32         S: Serializer,
     33     {
     34         serializer.serialize_str(self.as_str())
     35     }
     36 }
     37 /// Serde [`Visitor`] that deserializes a string into a [`Domain`] based on [`Self::allowed_ascii`].
     38 ///
     39 /// Since `Domain`s rely on an [`AllowedAscii`], there cannot be a single deserializer. This visitor
     40 /// makes it slightly easier to implement [`Deserialize`] for `Domain` wrappers based on whatever `AllowedAscii`
     41 /// is desired.
     42 ///
     43 /// # Example
     44 ///
     45 /// ```
     46 /// use ascii_domain::{dom::Domain, char_set::ASCII_HYPHEN_DIGITS_LETTERS, serde::DomainVisitor};
     47 /// use serde::de::{Deserialize, Deserializer};
     48 /// struct DomainWrapper(Domain<String>);
     49 /// impl<'de> Deserialize<'de> for DomainWrapper {
     50 ///     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     51 ///     where
     52 ///         D: Deserializer<'de>,
     53 ///     {
     54 ///         deserializer.deserialize_string(DomainVisitor::<'_, _, String>::new(&ASCII_HYPHEN_DIGITS_LETTERS)).map(|dom| DomainWrapper(dom))
     55 ///     }
     56 /// }
     57 /// ```
     58 #[expect(
     59     clippy::partial_pub_fields,
     60     reason = "we don't expost PhantomData for obvious reasons, so this is fine"
     61 )]
     62 #[derive(Clone, Copy)]
     63 pub struct DomainVisitor<'a, T, T2> {
     64     /// Phantom.
     65     _x: PhantomData<fn() -> T2>,
     66     /// The character set the visitor will use when deserializing a string into a `Domain`.
     67     pub allowed_ascii: &'a AllowedAscii<T>,
     68 }
     69 /// Converts `DomainErr` to a Serde `de::Error`.
     70 fn dom_err_to_serde<E: de::Error>(value: DomainErr) -> E {
     71     match value {
     72         DomainErr::Empty => E::invalid_length(
     73             0,
     74             &"a valid domain with length inclusively between 1 and 253",
     75         ),
     76         DomainErr::RootDomain => {
     77             E::invalid_length(0, &"a valid domain with at least one non-root label")
     78         }
     79         DomainErr::LenExceeds253(len) => E::invalid_length(
     80             len,
     81             &"a valid domain with length inclusively between 1 and 253",
     82         ),
     83         DomainErr::LabelLenExceeds63 => E::invalid_length(
     84             64,
     85             &"a valid domain containing labels of length inclusively between 1 and 63",
     86         ),
     87         DomainErr::EmptyLabel => E::invalid_length(
     88             0,
     89             &"a valid domain containing labels of length inclusively between 1 and 63",
     90         ),
     91         DomainErr::InvalidByte(byt) => E::invalid_value(
     92             Unexpected::Unsigned(u64::from(byt)),
     93             &"a valid domain containing only the supplied ASCII subset",
     94         ),
     95     }
     96 }
     97 impl<'a, T, T2> DomainVisitor<'a, T, T2> {
     98     /// Returns `DomainVisitor` with [`Self::allowed_ascii`] set to `allowed_ascii`.
     99     ///
    100     /// # Example
    101     ///
    102     /// ```
    103     /// use ascii_domain::{char_set::ASCII_HYPHEN_DIGITS_LETTERS, serde::DomainVisitor};
    104     /// assert!(DomainVisitor::<'_, _, String>::new(&ASCII_HYPHEN_DIGITS_LETTERS).allowed_ascii.len() == 63);
    105     /// ```
    106     #[inline]
    107     pub const fn new<'b: 'a>(allowed_ascii: &'b AllowedAscii<T>) -> Self {
    108         Self {
    109             _x: PhantomData,
    110             allowed_ascii,
    111         }
    112     }
    113 }
    114 impl<'de: 'a, 'a, T: AsRef<[u8]>> Visitor<'de> for DomainVisitor<'_, T, &'a str> {
    115     type Value = Domain<&'a str>;
    116     #[inline]
    117     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    118         formatter.write_str("Domain")
    119     }
    120     #[inline]
    121     fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
    122     where
    123         E: de::Error,
    124     {
    125         Self::Value::try_from_bytes(v, self.allowed_ascii).map_err(|err| dom_err_to_serde::<E>(err))
    126     }
    127 }
    128 impl<T: AsRef<[u8]>> Visitor<'_> for DomainVisitor<'_, T, String> {
    129     type Value = Domain<String>;
    130     #[inline]
    131     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    132         formatter.write_str("Domain")
    133     }
    134     #[inline]
    135     fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
    136     where
    137         E: de::Error,
    138     {
    139         Self::Value::try_from_bytes(v, self.allowed_ascii).map_err(|err| dom_err_to_serde::<E>(err))
    140     }
    141     #[inline]
    142     fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    143     where
    144         E: de::Error,
    145     {
    146         self.visit_string(v.to_owned())
    147     }
    148 }
    149 /// Deserializes `String`s into a `Domain` based on [`PRINTABLE_ASCII`].
    150 impl<'de> Deserialize<'de> for Domain<String> {
    151     #[inline]
    152     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    153     where
    154         D: Deserializer<'de>,
    155     {
    156         deserializer.deserialize_string(DomainVisitor::<'_, _, String>::new(DOMAIN_CHARS))
    157     }
    158 }
    159 /// Deserializes `str`s into a `Domain` based on [`PRINTABLE_ASCII`].
    160 impl<'de: 'a, 'a> Deserialize<'de> for Domain<&'a str> {
    161     #[inline]
    162     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    163     where
    164         D: Deserializer<'de>,
    165     {
    166         deserializer.deserialize_str(DomainVisitor::<'_, _, &str>::new(DOMAIN_CHARS))
    167     }
    168 }
    169 /// Converts `Rfc1123Err` to a Serde `de::Error`.
    170 fn rfc_err_to_serde<E: de::Error>(value: Rfc1123Err) -> E {
    171     match value {
    172         Rfc1123Err::DomainErr(err) => dom_err_to_serde(err),
    173         Rfc1123Err::LabelStartsWithAHyphen | Rfc1123Err::LabelEndsWithAHyphen => E::invalid_value(
    174             Unexpected::Str("-"),
    175             &"a valid domain conforming to RFC 1123 which requires all labels to not begin or end with a '-'",
    176         ),
    177         Rfc1123Err::InvalidTld => E::invalid_value(
    178             Unexpected::Str(
    179                 "tld that is not all letters nor begins with 'xn--' and has length of at least five",
    180             ),
    181             &"a valid domain conforming to RFC 1123 which requires the last label (i.e., TLD) to either be all letters or have length of at least five and begins with 'xn--'",
    182         ),
    183     }
    184 }
    185 /// Serde [`Visitor`] that deserializes a string into an [`Rfc1123Domain`].
    186 struct Rfc1123Visitor<T>(PhantomData<fn() -> T>);
    187 impl<'de: 'a, 'a> Visitor<'de> for Rfc1123Visitor<&'a str> {
    188     type Value = Rfc1123Domain<&'a str>;
    189     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    190         formatter.write_str("Rfc1123Domain")
    191     }
    192     fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
    193     where
    194         E: de::Error,
    195     {
    196         Self::Value::try_from_bytes(v).map_err(|err| rfc_err_to_serde(err))
    197     }
    198 }
    199 impl Visitor<'_> for Rfc1123Visitor<String> {
    200     type Value = Rfc1123Domain<String>;
    201     fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
    202         formatter.write_str("Rfc1123Domain")
    203     }
    204     fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
    205     where
    206         E: de::Error,
    207     {
    208         Self::Value::try_from_bytes(v).map_err(|err| rfc_err_to_serde(err))
    209     }
    210     fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
    211     where
    212         E: de::Error,
    213     {
    214         self.visit_string(v.to_owned())
    215     }
    216 }
    217 impl<'de> Deserialize<'de> for Rfc1123Domain<String> {
    218     #[inline]
    219     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    220     where
    221         D: Deserializer<'de>,
    222     {
    223         deserializer.deserialize_string(Rfc1123Visitor::<String>(PhantomData))
    224     }
    225 }
    226 impl<'de: 'a, 'a> Deserialize<'de> for Rfc1123Domain<&'a str> {
    227     #[inline]
    228     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    229     where
    230         D: Deserializer<'de>,
    231     {
    232         deserializer.deserialize_str(Rfc1123Visitor::<&'a str>(PhantomData))
    233     }
    234 }
    235 #[cfg(test)]
    236 mod tests {
    237     extern crate alloc;
    238     use crate::{
    239         char_set::ASCII_HYPHEN_DIGITS_LETTERS,
    240         dom::{Domain, Rfc1123Domain},
    241     };
    242     use alloc::string::String;
    243     use serde_json;
    244     #[test]
    245     fn test_serde() {
    246         assert!(
    247             serde_json::from_str::<Domain<&str>>(r#""example.com""#)
    248                 .map_or(false, |dom| dom.into_iter().count() == 2)
    249         );
    250         assert!(
    251             serde_json::from_str::<Domain<String>>(r#""c\"om""#)
    252                 .map_or(false, |dom| dom.into_iter().count() == 1)
    253         );
    254         // Can't borrow since input needs to be de-escaped.
    255         assert!(
    256             serde_json::from_str::<Domain<&str>>(r#""c\"om""#)
    257                 .map_or_else(|err| err.is_data() && err.column() == 7, |_| false)
    258         );
    259         assert!(
    260             serde_json::to_string(
    261                 &Domain::try_from_bytes("example.com", &ASCII_HYPHEN_DIGITS_LETTERS).unwrap()
    262             )
    263             .map_or(false, |output| output == r#""example.com""#)
    264         );
    265         assert!(
    266             serde_json::to_string(
    267                 &Domain::try_from_bytes(b"example.com", &ASCII_HYPHEN_DIGITS_LETTERS).unwrap()
    268             )
    269             .map_or(false, |output| output == r#""example.com""#)
    270         );
    271         assert!(
    272             serde_json::from_str::<Rfc1123Domain<&str>>(r#""example.com""#)
    273                 .map_or(false, |dom| dom.into_iter().count() == 2)
    274         );
    275         assert!(
    276             serde_json::from_str::<Rfc1123Domain<String>>(r#""c\u006fm""#)
    277                 .map_or(false, |dom| dom.tld().as_str() == "com")
    278         );
    279         // Can't borrow since input needs to be de-escaped.
    280         assert!(
    281             serde_json::from_str::<Rfc1123Domain<&str>>(r#""c\u006fm""#)
    282                 .map_or_else(|err| err.is_data() && err.column() == 10, |_| false)
    283         );
    284     }
    285 }