From acdf8e31d7fb83e8b69d247537c2d3ac632d91b8 Mon Sep 17 00:00:00 2001 From: "Tobin C. Harding" Date: Thu, 26 Oct 2023 13:43:29 +1100 Subject: [PATCH] Add serde module Add a `serde` module and feature gate it behind the "serde" feature as is customary. Add an example showing how to use the new module to serialize struct fields as hex. Fix: #6 --- Cargo.toml | 8 ++++ contrib/test.sh | 3 +- examples/serde.rs | 33 ++++++++++++++++ src/lib.rs | 5 +++ src/serde.rs | 98 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 examples/serde.rs create mode 100644 src/serde.rs diff --git a/Cargo.toml b/Cargo.toml index 681f988..21c8378 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,12 @@ alloc = [] [dependencies] core2 = { version = "0.3.2", default_features = false, optional = true } +serde = { version = "1.0", default-features = false, optional = true } + [dev-dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" [[example]] name = "hexy" @@ -40,3 +44,7 @@ name = "wrap_array_display_hex_trait" [[example]] name = "wrap_array_fmt_traits" + +[[example]] +name = "serde" +required-features = ["std", "serde"] diff --git a/contrib/test.sh b/contrib/test.sh index 277fdc5..cb98ab5 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -2,7 +2,7 @@ set -ex -FEATURES="std alloc core2" +FEATURES="std alloc core2 serde" MSRV="1\.48\.0" cargo --version @@ -32,6 +32,7 @@ if [ "$DO_LINT" = true ] then cargo clippy --all-features --all-targets -- -D warnings cargo clippy --locked --example hexy -- -D warnings + cargo clippy --locked --example serde --features=serde -- -D warnings fi if [ "$DO_FEATURE_MATRIX" = true ]; then diff --git a/examples/serde.rs b/examples/serde.rs new file mode 100644 index 0000000..a37cfa4 --- /dev/null +++ b/examples/serde.rs @@ -0,0 +1,33 @@ +//! Demonstrate how to use the serde module with struct fields. + +#![allow(clippy::disallowed_names)] // Foo is a valid name. + +use hex_conservative as hex; +use serde::{Deserialize, Serialize}; + +fn main() { + let v = vec![0xde, 0xad, 0xbe, 0xef]; + + let foo = Foo { v: v.clone() }; + let bar = Bar { v: v.clone() }; + + let ser_foo = serde_json::to_string(&foo).expect("failed to serialize foo"); + let ser_bar = serde_json::to_string(&bar).expect("failed to serialize bar"); + + println!(); + println!("foo: {}", ser_foo); + println!("bar: {}", ser_bar); +} + +/// Abstracts over foo. +#[derive(Debug, Serialize, Deserialize)] +struct Foo { + #[serde(with = "hex")] + v: Vec, +} + +/// Abstracts over bar. +#[derive(Debug, Serialize, Deserialize)] +struct Bar { + v: Vec, +} diff --git a/src/lib.rs b/src/lib.rs index 1fc5573..2cbb88a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,6 +48,8 @@ pub mod display; mod error; mod iter; pub mod parse; +#[cfg(feature = "serde")] +pub mod serde; /// Reexports of the common crate types and traits. pub mod prelude { @@ -65,6 +67,9 @@ pub use self::{ iter::{BytesToHexIter, HexToBytesIter}, parse::{FromHex, HexToArrayError, HexToBytesError}, }; +#[cfg(feature = "serde")] +#[doc(inline)] +pub use crate::serde::{deserialize, serialize, serialize_lower, serialize_upper}; /// Possible case of hex. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 0000000..4be932a --- /dev/null +++ b/src/serde.rs @@ -0,0 +1,98 @@ +//! Hex encoding with `serde`. +//! +//! # Examples +//! +//! ``` +//! # #[cfg(feature = "std")] { +//! use hex_conservative as hex; +//! use serde::{Serialize, Deserialize}; +//! +//! #[derive(Debug, Serialize, Deserialize)] +//! struct Foo { +//! #[serde(with = "hex")] +//! bar: Vec, +//! } +//! # } +//! ``` + +use core::fmt; +use core::marker::PhantomData; + +use serde::de::{Error, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::prelude::*; + +/// Serializes `data` as a hex string using lowercase characters. +pub fn serialize(data: T, s: S) -> Result +where + S: Serializer, + T: Serialize + DisplayHex, +{ + serialize_lower(data, s) +} + +/// Serializes `data` as a hex string using lowercase characters. +pub fn serialize_lower(data: T, serializer: S) -> Result +where + S: Serializer, + T: Serialize + DisplayHex, +{ + // Don't do anything special when not human readable. + if !serializer.is_human_readable() { + serde::Serialize::serialize(&data, serializer) + } else { + serializer.collect_str(&format_args!("{:x}", data.as_hex())) + } +} + +/// Serializes `data` as hex string using uppercase characters. +pub fn serialize_upper(data: T, serializer: S) -> Result +where + S: Serializer, + T: Serialize + DisplayHex, +{ + // Don't do anything special when not human readable. + if !serializer.is_human_readable() { + serde::Serialize::serialize(&data, serializer) + } else { + serializer.collect_str(&format_args!("{:X}", data.as_hex())) + } +} + +/// Deserializes a hex string into raw bytes. +/// +/// Allows upper, lower, and mixed case characters (e.g. `a5b3c1`, `A5B3C1` and `A5b3C1`). +pub fn deserialize<'de, D, T>(d: D) -> Result +where + D: Deserializer<'de>, + T: Deserialize<'de> + FromHex, +{ + struct HexVisitor(PhantomData); + + impl<'de, T> Visitor<'de> for HexVisitor + where + T: FromHex, + { + type Value = T; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("an ASCII hex string") + } + + fn visit_str(self, data: &str) -> Result { + FromHex::from_hex(data).map_err(Error::custom) + } + + fn visit_borrowed_str(self, data: &'de str) -> Result { + FromHex::from_hex(data).map_err(Error::custom) + } + } + + // Don't do anything special when not human readable. + if !d.is_human_readable() { + serde::Deserialize::deserialize(d) + } else { + d.deserialize_map(HexVisitor(PhantomData)) + } +}