commit f3b6ea7df4cce7a861a7a77fa574a07b10b64ea5
parent e64e9a57bf7ee0554ed1bfe21d49a1ed0a6124f3
Author: Zack Newman <zack@philomathiclife.com>
Date: Mon, 5 Feb 2024 22:34:44 -0700
add serde support
Diffstat:
5 files changed, 263 insertions(+), 8 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,12 +9,28 @@ license = "MIT OR Apache-2.0"
name = "ascii_domain"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/ascii_domain/"
-version = "0.3.0"
+version = "0.3.1"
[lib]
name = "ascii_domain"
path = "src/lib.rs"
+[dependencies]
+serde = { version = "1.0.196", default-features = false, features = ["alloc"], optional = true }
+
+[dev-dependencies]
+serde_json = { version = "1.0.113", default-features = false, features = ["alloc"] }
+
+[build-dependencies]
+rustc_version = "0.4.0"
+
+[features]
+serde = ["dep:serde"]
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
[badges]
maintenance = { status = "actively-developed" }
diff --git a/build.rs b/build.rs
@@ -0,0 +1,12 @@
+use rustc_version::{version_meta, Channel};
+
+fn main() {
+ // Set cfg flags depending on release channel
+ let channel = match version_meta().unwrap().channel {
+ Channel::Stable => "CHANNEL_STABLE",
+ Channel::Beta => "CHANNEL_BETA",
+ Channel::Nightly => "CHANNEL_NIGHTLY",
+ Channel::Dev => "CHANNEL_DEV",
+ };
+ println!("cargo:rustc-cfg={}", channel)
+}
diff --git a/src/dom.rs b/src/dom.rs
@@ -989,9 +989,7 @@ impl Display for Rfc1123Err {
}
}
impl Error for Rfc1123Err {}
-/// **TL;DR**
-///
-/// Wrapper type around a [`Domain`] that enforces conformance to
+/// **TL;DR** Wrapper type around a [`Domain`] that enforces conformance to
/// [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#page-13).
///
/// * Each [`Label`] must only contain ASCII digits, letters, or hyphen.
@@ -1000,10 +998,10 @@ impl Error for Rfc1123Err {}
/// ---
/// Unsurprisingly, RFC 1123 is not super precise as it uses "host name" to mean label and also domain:
/// "Host software MUST handle host names \[labels\] of up to 63 characters and SHOULD handle host
-/// names \[domains\] of up to 255 characters". It also states that only "one aspect of host name syntax is hereby
-/// changed" from [RFC 952](https://www.rfc-editor.org/rfc/rfc952): "the restriction on the first character
-/// is relaxed to allow either a letter or a digit". Despite that, it goes on to mention other restrictions
-/// not mentioned in RFC 952: "the highest-level component label will be alphabetic". It is therefore
+/// names \[domains\] of up to 255 characters". It also states that only "one aspect of host name \[label\]
+/// syntax is hereby changed" from [RFC 952](https://www.rfc-editor.org/rfc/rfc952): "the restriction on the
+/// first character is relaxed to allow either a letter or a digit". Despite that, it goes on to mention other
+/// restrictions not mentioned in RFC 952: "the highest-level component label will be alphabetic". It is therefore
/// important to understand how this type interprets that RFC and why it does so.
///
/// The primary issue with RFC 1123 is the unjustified comment about the TLD being alphabetic. It is given
@@ -1091,6 +1089,7 @@ impl<T: AsRef<[u8]>> Rfc1123Domain<T> {
/// Note that due to the most relaxed interpretation of RFC 1123 mentioned in [`Rfc1123Domain`], it is possible
/// for the domain to be an IPv4 address unlike the strictest, strict, literal, and possibly relaxed
/// interpretations.
+ ///
/// # Example
///
/// ```
diff --git a/src/lib.rs b/src/lib.rs
@@ -13,6 +13,7 @@
//! all octets are allowed; but conforming to [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123) or
//! [RFC 5891](https://datatracker.ietf.org/doc/html/rfc5891) requires stricter formats and a reduced character
//! set.
+#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))]
#![deny(
unsafe_code,
unused,
@@ -44,3 +45,7 @@ pub mod char_set;
/// [`char_set::AllowedAscii`]. Also contains [`dom::Rfc1123Domain`] which is a `Domain` that conforms to
/// [RFC 1123](https://www.rfc-editor.org/rfc/rfc1123#page-13).
pub mod dom;
+/// Contains a Serde [`Visitor`](https://docs.rs/serde/latest/serde/de/trait.Visitor.html) that can be used to help
+/// deserialize [`dom::Domain`] wrappers.
+#[cfg(feature = "serde")]
+pub mod serde;
diff --git a/src/serde.rs b/src/serde.rs
@@ -0,0 +1,223 @@
+#[cfg(feature = "serde")]
+use crate::{
+ char_set::{AllowedAscii, ASCII_HYPHEN_DIGITS_LETTERS, PRINTABLE_ASCII},
+ dom::{Domain, Rfc1123Domain},
+};
+#[cfg(feature = "serde")]
+use core::fmt;
+#[cfg(feature = "serde")]
+use core::marker::PhantomData;
+#[cfg(feature = "serde")]
+use serde::{
+ de::{self, Deserialize, Deserializer, Error, Unexpected, Visitor},
+ ser::{Serialize, Serializer},
+};
+#[cfg(feature = "serde")]
+impl<T: AsRef<[u8]>> Serialize for Domain<T> {
+ /// Serializes `Domain` as a string.
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+#[cfg(feature = "serde")]
+impl<T: AsRef<[u8]>> Serialize for Rfc1123Domain<T> {
+ /// Serializes `Rfc1123Domain` as a string.
+ #[inline]
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(self.as_str())
+ }
+}
+/// Serde [`Visitor`] that deserializes a string into a [`Domain`] based on [`Self::allowed_ascii`].
+///
+/// Since `Domain`s rely on an [`AllowedAscii`], there cannot be a single deserializer. This visitor
+/// makes it slightly easier to implement [`Deserialize`] for `Domain` wrappers based on whatever `AllowedAscii`
+/// is desired.
+///
+/// # Example
+///
+/// ```
+/// use ascii_domain::{dom::Domain, char_set::ASCII_HYPHEN_DIGITS_LETTERS, serde::DomainVisitor};
+/// use serde::de::{Deserialize, Deserializer};
+/// struct DomainWrapper(Domain<String>);
+/// impl<'de> Deserialize<'de> for DomainWrapper {
+/// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+/// where
+/// D: Deserializer<'de>,
+/// {
+/// deserializer.deserialize_string(DomainVisitor::<'_, _, String>::new(&ASCII_HYPHEN_DIGITS_LETTERS)).map(|dom| DomainWrapper(dom))
+/// }
+/// }
+/// ```
+#[allow(clippy::partial_pub_fields)]
+#[cfg(feature = "serde")]
+#[derive(Clone, Copy)]
+pub struct DomainVisitor<'a, T, T2> {
+ /// Phantom.
+ _x: PhantomData<fn() -> T2>,
+ /// The character set the visitor will use when deserializing a string into a `Domain`.
+ pub allowed_ascii: &'a AllowedAscii<T>,
+}
+impl<'a, T, T2> DomainVisitor<'a, T, T2> {
+ /// Returns `DomainVisitor` with [`Self::allowed_ascii`] set to `allowed_ascii`.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use ascii_domain::{char_set::ASCII_HYPHEN_DIGITS_LETTERS, serde::DomainVisitor};
+ /// assert!(DomainVisitor::<'_, _, String>::new(&ASCII_HYPHEN_DIGITS_LETTERS).allowed_ascii.len() == 63);
+ /// ```
+ #[inline]
+ pub const fn new<'b: 'a>(allowed_ascii: &'b AllowedAscii<T>) -> Self {
+ Self {
+ _x: PhantomData,
+ allowed_ascii,
+ }
+ }
+}
+#[cfg(feature = "serde")]
+impl<'de: 'b, 'a, 'b, T: AsRef<[u8]>> Visitor<'de> for DomainVisitor<'a, T, &'b str> {
+ type Value = Domain<&'b str>;
+ #[inline]
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("Domain")
+ }
+ #[inline]
+ fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ Domain::try_from_bytes(v, self.allowed_ascii)
+ .map_err(|err| E::invalid_value(Unexpected::Str(err.to_string().as_str()), &"a Domain"))
+ }
+}
+#[cfg(feature = "serde")]
+impl<'de, 'a, T: AsRef<[u8]>> Visitor<'de> for DomainVisitor<'a, T, String> {
+ type Value = Domain<String>;
+ #[inline]
+ fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ formatter.write_str("Domain")
+ }
+ #[inline]
+ fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ Domain::try_from_bytes(v, self.allowed_ascii)
+ .map_err(|err| E::invalid_value(Unexpected::Str(err.to_string().as_str()), &"a Domain"))
+ }
+ #[inline]
+ fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
+ where
+ E: de::Error,
+ {
+ self.visit_string(v.to_owned())
+ }
+}
+/// Deserializes `String`s into a `Domain` based on [`PRINTABLE_ASCII`].
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Domain<String> {
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_string(DomainVisitor::<'_, _, String>::new(&PRINTABLE_ASCII))
+ }
+}
+/// Deserializes `str`s into a `Domain` based on [`PRINTABLE_ASCII`].
+#[cfg(feature = "serde")]
+impl<'de: 'a, 'a> Deserialize<'de> for Domain<&'a str> {
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer.deserialize_string(DomainVisitor::<'_, _, &str>::new(&PRINTABLE_ASCII))
+ }
+}
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Rfc1123Domain<String> {
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_string(DomainVisitor::<'_, _, String>::new(
+ &ASCII_HYPHEN_DIGITS_LETTERS,
+ ))
+ .and_then(|dom| {
+ Self::try_from(dom).map_err(|err| {
+ Error::invalid_value(
+ Unexpected::Str(err.to_string().as_str()),
+ &"an Rfc1123Domain",
+ )
+ })
+ })
+ }
+}
+#[cfg(feature = "serde")]
+impl<'de: 'a, 'a> Deserialize<'de> for Rfc1123Domain<&'a str> {
+ #[inline]
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ deserializer
+ .deserialize_string(DomainVisitor::<'_, _, &str>::new(
+ &ASCII_HYPHEN_DIGITS_LETTERS,
+ ))
+ .and_then(|dom| {
+ Self::try_from(dom).map_err(|err| {
+ Error::invalid_value(
+ Unexpected::Str(err.to_string().as_str()),
+ &"an Rfc1123Domain",
+ )
+ })
+ })
+ }
+}
+#[cfg(all(test, feature = "serde"))]
+mod tests {
+ use crate::{
+ char_set::ASCII_HYPHEN_DIGITS_LETTERS,
+ dom::{Domain, Rfc1123Domain},
+ };
+ use serde_json;
+ #[test]
+ fn test_serde() {
+ assert!(serde_json::from_str::<Domain<&str>>(r#""example.com""#)
+ .map_or(false, |dom| dom.label_count().get() == 2));
+ assert!(serde_json::from_str::<Domain<String>>(r#""c\"om""#)
+ .map_or(false, |dom| dom.label_count().get() == 1));
+ // Can't borrow since input needs to be de-escaped.
+ assert!(serde_json::from_str::<Domain<&str>>(r#""c\"om""#)
+ .map_or_else(|err| err.is_data() && err.column() == 7, |_| false));
+ assert!(serde_json::to_string(
+ &Domain::try_from_bytes("example.com", &ASCII_HYPHEN_DIGITS_LETTERS).unwrap()
+ )
+ .map_or(false, |output| output == r#""example.com""#));
+ assert!(serde_json::to_string(
+ &Domain::try_from_bytes(b"example.com", &ASCII_HYPHEN_DIGITS_LETTERS).unwrap()
+ )
+ .map_or(false, |output| output == r#""example.com""#));
+ assert!(
+ serde_json::from_str::<Rfc1123Domain<&str>>(r#""example.com""#)
+ .map_or(false, |dom| dom.label_count().get() == 2)
+ );
+ assert!(
+ serde_json::from_str::<Rfc1123Domain<String>>(r#""c\u006fm""#)
+ .map_or(false, |dom| dom.tld().as_str() == "com")
+ );
+ // Can't borrow since input needs to be de-escaped.
+ assert!(serde_json::from_str::<Rfc1123Domain<&str>>(r#""c\u006fm""#)
+ .map_or_else(|err| err.is_data() && err.column() == 10, |_| false));
+ }
+}