From 0e1b91bace04f80979ad825440774af98ea6298e Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 20 Aug 2024 16:24:44 +0200 Subject: [PATCH 01/15] Add `as_` module Signed-off-by: Denis Varlakov --- Cargo.lock | 4 +- udigest/src/as_.rs | 272 +++++++++++++++++++++++++++++++++++++++++++++ udigest/src/lib.rs | 7 +- 3 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 udigest/src/as_.rs diff --git a/Cargo.lock b/Cargo.lock index 4fff404..8b2213e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "udigest" -version = "0.2.0-rc4" +version = "0.2.0" dependencies = [ "blake2", "digest", @@ -163,7 +163,7 @@ dependencies = [ [[package]] name = "udigest-derive" -version = "0.2.0-rc3" +version = "0.2.0" dependencies = [ "proc-macro2", "quote", diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs new file mode 100644 index 0000000..ae0f76e --- /dev/null +++ b/udigest/src/as_.rs @@ -0,0 +1,272 @@ +//! Provides utilities for custom digesting rules + +use crate::{encoding, Buffer, Digestable}; + +/// Custom rule for digesting an instance of `T` +pub trait DigestAs { + /// Digests `value` + fn digest_as(value: &T, encoder: encoding::EncodeValue); +} + +impl DigestAs<&T> for &U +where + U: DigestAs, +{ + fn digest_as(value: &&T, encoder: encoding::EncodeValue) { + U::digest_as(*value, encoder) + } +} + +/// Stores `T`, digests it using `DigestAs` implementation of `U` +pub struct As { + value: T, + _rule: core::marker::PhantomData, +} + +impl As { + /// Constructor + pub const fn new(value: T) -> Self { + Self { + value, + _rule: core::marker::PhantomData, + } + } + + /// Returns stored value + pub fn into_inner(self) -> T { + self.value + } +} + +impl Digestable for As +where + U: DigestAs, +{ + fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { + U::digest_as(&self.value, encoder) + } +} + +impl core::cmp::PartialEq for As { + fn eq(&self, other: &Self) -> bool { + self.value.eq(&other.value) + } +} +impl core::cmp::Eq for As {} +impl core::cmp::PartialOrd for As { + fn partial_cmp(&self, other: &Self) -> Option { + self.value.partial_cmp(&other.value) + } +} +impl core::cmp::Ord for As { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.value.cmp(&other.value) + } +} + +/// Digests any type `T` via its own implementation of [`Digestable`] trait +pub struct Same; + +impl DigestAs for Same +where + T: Digestable, +{ + fn digest_as(value: &T, encoder: encoding::EncodeValue) { + value.unambiguously_encode(encoder) + } +} + +/// Digests any bytestring that implements `impl AsRef<[u8]>` +pub type Bytes = crate::Bytes<()>; + +impl DigestAs for Bytes +where + T: AsRef<[u8]> + ?Sized, +{ + fn digest_as(value: &T, encoder: encoding::EncodeValue) { + encoder.encode_leaf_value(value.as_ref()) + } +} + +impl DigestAs> for Option +where + U: DigestAs, +{ + fn digest_as(value: &Option, encoder: encoding::EncodeValue) { + value + .as_ref() + .map(As::<&T, &U>::new) + .unambiguously_encode(encoder) + } +} + +impl DigestAs> for Result +where + T2: DigestAs, + E2: DigestAs, +{ + fn digest_as(value: &Result, encoder: encoding::EncodeValue) { + value + .as_ref() + .map(As::<&T1, &T2>::new) + .map_err(As::<&E1, &E2>::new) + .unambiguously_encode(encoder) + } +} + +impl DigestAs<[T]> for [U] +where + U: DigestAs, +{ + fn digest_as(value: &[T], encoder: encoding::EncodeValue) { + crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) + } +} + +impl DigestAs<[T; N]> for [U; N] +where + U: DigestAs, +{ + fn digest_as(value: &[T; N], encoder: encoding::EncodeValue) { + crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) + } +} + +#[cfg(feature = "alloc")] +impl DigestAs> for alloc::vec::Vec +where + U: DigestAs, +{ + fn digest_as(value: &alloc::vec::Vec, encoder: encoding::EncodeValue) { + crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) + } +} + +impl DigestAs> for alloc::collections::LinkedList +where + U: DigestAs, +{ + fn digest_as( + value: &alloc::collections::LinkedList, + encoder: encoding::EncodeValue, + ) { + crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) + } +} +impl DigestAs> for alloc::collections::VecDeque +where + U: DigestAs, +{ + fn digest_as( + value: &alloc::collections::VecDeque, + encoder: encoding::EncodeValue, + ) { + crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) + } +} +impl DigestAs> for alloc::collections::BTreeSet +where + U: DigestAs, +{ + fn digest_as( + value: &alloc::collections::BTreeSet, + encoder: encoding::EncodeValue, + ) { + crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) + } +} + +#[cfg(feature = "alloc")] +impl DigestAs> + for alloc::collections::BTreeMap +where + K2: DigestAs, + V2: DigestAs, +{ + fn digest_as( + value: &alloc::collections::BTreeMap, + encoder: encoding::EncodeValue, + ) { + crate::unambiguously_encode_iter( + encoder, + value + .iter() + .map(|(key, value)| (As::<&K1, &K2>::new(key), As::<&V1, &V2>::new(value))), + ) + } +} + +/// Digests `HashMap` by transforming it into `BTreeMap` +#[cfg(feature = "std")] +impl DigestAs> + for alloc::collections::BTreeMap +where + K2: DigestAs, + V2: DigestAs, + K1: core::cmp::Ord, +{ + fn digest_as( + value: &std::collections::HashMap, + encoder: encoding::EncodeValue, + ) { + let ordered_map = value + .iter() + .map(|(key, value)| (As::<&K1, &K2>::new(key), As::<&V1, &V2>::new(value))) + .collect::>(); + + // ordered map has deterministic order, so we can reproducibly hash it + ordered_map.unambiguously_encode(encoder) + } +} + +#[cfg(feature = "alloc")] +impl DigestAs> for alloc::boxed::Box +where + U: DigestAs, +{ + fn digest_as(value: &alloc::boxed::Box, encoder: encoding::EncodeValue) { + U::digest_as(value, encoder) + } +} + +#[cfg(feature = "alloc")] +impl DigestAs> for alloc::rc::Rc +where + U: DigestAs, +{ + fn digest_as(value: &alloc::rc::Rc, encoder: encoding::EncodeValue) { + U::digest_as(value, encoder) + } +} + +#[cfg(feature = "alloc")] +impl DigestAs> for alloc::sync::Arc +where + U: DigestAs, +{ + fn digest_as(value: &alloc::sync::Arc, encoder: encoding::EncodeValue) { + U::digest_as(value, encoder) + } +} + +#[cfg(feature = "alloc")] +impl<'a, T, U> DigestAs> for alloc::borrow::Cow<'a, U> +where + U: DigestAs + alloc::borrow::ToOwned, + T: alloc::borrow::ToOwned + ?Sized + 'a, +{ + fn digest_as(value: &alloc::borrow::Cow<'a, T>, encoder: encoding::EncodeValue) { + U::digest_as(value.as_ref(), encoder) + } +} + +#[cfg(feature = "alloc")] +impl<'a, T, U> DigestAs> for &'a U +where + U: DigestAs, + T: alloc::borrow::ToOwned + ?Sized + 'a, +{ + fn digest_as(value: &alloc::borrow::Cow<'a, T>, encoder: encoding::EncodeValue) { + U::digest_as(value.as_ref(), encoder) + } +} diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 7622c3a..2ec0cf8 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -56,6 +56,8 @@ #[cfg(feature = "alloc")] extern crate alloc; +#[cfg(feature = "std")] +extern crate std; pub use encoding::Buffer; @@ -204,6 +206,9 @@ pub mod encoding; #[cfg(feature = "inline-struct")] pub mod inline_struct; +pub mod as_; +pub use as_::DigestAs; + /// Digests a structured `value` using fixed-output hash function (like sha2-256) #[cfg(feature = "digest")] pub fn hash(value: &impl Digestable) -> digest::Output { @@ -307,7 +312,7 @@ pub struct Bytes(pub T); impl + ?Sized> Digestable for Bytes { fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { - self.0.as_ref().unambiguously_encode(encoder) + encoder.encode_leaf_value(self.0.as_ref()) } } From 28d39863ff7ee6fe86fa746fd9e7f55e821f8909 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 20 Aug 2024 16:32:54 +0200 Subject: [PATCH 02/15] Deny warnings in CI Signed-off-by: Denis Varlakov --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f80990b..e47a61b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -7,6 +7,7 @@ on: env: CARGO_TERM_COLOR: always CARGO_NET_GIT_FETCH_WITH_CLI: true + RUSTFLAGS: -D warnings jobs: check-and-test: From 1b4c1b9f6061a2c773251389d227f5f7f6ed7e83 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Tue, 20 Aug 2024 16:35:31 +0200 Subject: [PATCH 03/15] Fix warns in derive Signed-off-by: Denis Varlakov --- udigest-derive/src/attrs.rs | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/udigest-derive/src/attrs.rs b/udigest-derive/src/attrs.rs index 1c6ad51..4856a0b 100644 --- a/udigest-derive/src/attrs.rs +++ b/udigest-derive/src/attrs.rs @@ -59,7 +59,7 @@ impl syn::parse::Parse for Attr { pub struct Root { pub root: kw::root, - pub eq: syn::Token![=], + pub _eq: syn::Token![=], pub path: RootPath, } @@ -68,50 +68,50 @@ pub type RootPath = syn::punctuated::Punctuated; impl syn::parse::Parse for Root { fn parse(input: syn::parse::ParseStream) -> syn::Result { let root = input.parse::()?; - let eq = input.parse::()?; + let _eq = input.parse::()?; let path = RootPath::parse_separated_nonempty_with(input, syn::Ident::parse_any)?; - Ok(Self { root, eq, path }) + Ok(Self { root, _eq, path }) } } pub struct Tag { pub tag: kw::tag, - pub eq: syn::Token![=], + pub _eq: syn::Token![=], pub value: syn::Expr, } impl syn::parse::Parse for Tag { fn parse(input: syn::parse::ParseStream) -> syn::Result { let tag = input.parse()?; - let eq = input.parse()?; + let _eq = input.parse()?; let value = input.parse()?; - Ok(Self { tag, eq, value }) + Ok(Self { tag, _eq, value }) } } pub struct AsBytes { pub as_bytes: kw::as_bytes, - pub eq: Option, + pub _eq: Option, pub value: Option, } impl syn::parse::Parse for AsBytes { fn parse(input: syn::parse::ParseStream) -> syn::Result { let as_bytes = input.parse()?; - let mut eq = None; + let mut _eq = None; let mut value = None; let lookahead = input.lookahead1(); if lookahead.peek(syn::Token![=]) { - eq = Some(input.parse()?); + _eq = Some(input.parse()?); value = Some(input.parse()?); } Ok(Self { as_bytes, - eq, + _eq, value, }) } @@ -119,17 +119,17 @@ impl syn::parse::Parse for AsBytes { pub struct Bound { pub bound: kw::bound, - pub eq: syn::Token![=], + pub _eq: syn::Token![=], pub value: syn::LitStr, } impl syn::parse::Parse for Bound { fn parse(input: syn::parse::ParseStream) -> syn::Result { let bound = input.parse()?; - let eq = input.parse()?; + let _eq = input.parse()?; let value = input.parse()?; - Ok(Self { bound, eq, value }) + Ok(Self { bound, _eq, value }) } } @@ -146,31 +146,31 @@ impl syn::parse::Parse for Skip { pub struct Rename { pub rename: kw::rename, - pub eq: syn::Token![=], + pub _eq: syn::Token![=], pub value: syn::Expr, } impl syn::parse::Parse for Rename { fn parse(input: syn::parse::ParseStream) -> syn::Result { let rename = input.parse()?; - let eq = input.parse()?; + let _eq = input.parse()?; let value = input.parse()?; - Ok(Self { rename, eq, value }) + Ok(Self { rename, _eq, value }) } } pub struct With { pub with: kw::with, - pub eq: syn::Token![=], + pub _eq: syn::Token![=], pub value: syn::Expr, } impl syn::parse::Parse for With { fn parse(input: syn::parse::ParseStream) -> syn::Result { let with = input.parse()?; - let eq = input.parse()?; + let _eq = input.parse()?; let value = input.parse()?; - Ok(Self { with, eq, value }) + Ok(Self { with, _eq, value }) } } From 5dab7c70f0c5acd7d5f17b063b17c91a02276125 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 21 Aug 2024 15:54:28 +0200 Subject: [PATCH 04/15] proc-macro: add support of `as` attribute Signed-off-by: Denis Varlakov --- udigest-derive/src/attrs.rs | 19 +++ udigest-derive/src/lib.rs | 260 +++++++++++++++++++++++++++++++----- udigest/Cargo.toml | 5 + udigest/src/as_.rs | 3 +- udigest/src/lib.rs | 2 +- udigest/tests/common/mod.rs | 16 +++ udigest/tests/derive.rs | 10 ++ udigest/tests/digest_as.rs | 100 ++++++++++++++ 8 files changed, 378 insertions(+), 37 deletions(-) create mode 100644 udigest/tests/common/mod.rs create mode 100644 udigest/tests/digest_as.rs diff --git a/udigest-derive/src/attrs.rs b/udigest-derive/src/attrs.rs index 4856a0b..ae98b3e 100644 --- a/udigest-derive/src/attrs.rs +++ b/udigest-derive/src/attrs.rs @@ -18,6 +18,7 @@ pub enum Attr { Skip(Skip), Rename(Rename), With(With), + As(As), } impl Attr { @@ -30,6 +31,7 @@ impl Attr { Attr::Skip(attr) => attr.skip.span, Attr::Rename(attr) => attr.rename.span, Attr::With(attr) => attr.with.span, + Attr::As(attr) => attr.as_.span, } } } @@ -51,6 +53,8 @@ impl syn::parse::Parse for Attr { Rename::parse(input).map(Attr::Rename) } else if lookahead.peek(kw::with) { With::parse(input).map(Attr::With) + } else if lookahead.peek(syn::Token![as]) { + As::parse(input).map(Attr::As) } else { Err(lookahead.error()) } @@ -174,3 +178,18 @@ impl syn::parse::Parse for With { Ok(Self { with, _eq, value }) } } + +pub struct As { + pub as_: syn::Token![as], + pub _eq: syn::Token![=], + pub value: syn::Type, +} + +impl syn::parse::Parse for As { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let as_ = input.parse()?; + let _eq = input.parse()?; + let value = input.parse()?; + Ok(Self { as_, _eq, value }) + } +} diff --git a/udigest-derive/src/lib.rs b/udigest-derive/src/lib.rs index fdd120e..09daf34 100644 --- a/udigest-derive/src/lib.rs +++ b/udigest-derive/src/lib.rs @@ -75,7 +75,7 @@ fn process_enum( }, fields: (0..) .zip(v.fields.iter()) - .map(|(i, f)| process_field(i, f)) + .map(|(i, f)| process_field(&attrs.get_root_path(), i, f)) .collect::>>()?, }) }) @@ -92,13 +92,34 @@ fn process_struct( ) -> Result { let struct_fields = (0..) .zip(s.fields.iter()) - .map(|(i, f)| process_field(i, f)) + .map(|(i, f)| process_field(&container_attrs.get_root_path(), i, f)) .collect::>>()?; generate_impl_for_struct(container_attrs, name, generics, &struct_fields) } -fn process_field(index: u32, field: &syn::Field) -> Result { +fn process_field(root_path: &attrs::RootPath, index: u32, field: &syn::Field) -> Result { + // same_ty = ::as_::Same + let same_ty = { + let mut root = root_path.clone(); + root.extend([ + syn::Ident::new("as_", root_path.span()), + syn::Ident::new("Same", root_path.span()), + ]); + syn::Type::Path(syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments: root + .into_iter() + .map(|ident| syn::PathSegment { + ident, + arguments: syn::PathArguments::None, + }) + .collect(), + }, + }) + }; let mut field_attrs = FieldAttrs::default(); let mem = field @@ -118,52 +139,212 @@ fn process_field(index: u32, field: &syn::Field) -> Result { continue; }; match attr { - attrs::Attr::AsBytes(_) if field_attrs.with.is_some() => { - return Err(Error::new( - attr.kw_span(), - "attributes `with` and `as_bytes` cannot be used together", - )); + attrs::Attr::AsBytes(_) if field_attrs.as_bytes.is_some() => { + return Err(Error::new(attr.kw_span(), "attribute is duplicated")) + } + attrs::Attr::With(_) if field_attrs.with.is_some() => { + return Err(Error::new(attr.kw_span(), "attribute is duplicated")) } - attrs::Attr::With(_) if field_attrs.as_bytes.is_some() => { + attrs::Attr::Skip(_) if field_attrs.skip.is_some() => { + return Err(Error::new(attr.kw_span(), "attribute is duplicated")); + } + attrs::Attr::Rename(_) if field_attrs.rename.is_some() => { + return Err(Error::new(attr.kw_span(), "attribute is duplicated")) + } + attrs::Attr::As(_) if field_attrs.as_.is_some() => { + return Err(Error::new(attr.kw_span(), "attribute is duplicated")) + } + attrs::Attr::AsBytes(_) + | attrs::Attr::With(_) + | attrs::Attr::As(_) + | attrs::Attr::Skip(_) + if count_trues([ + field_attrs.as_bytes.is_some(), + field_attrs.with.is_some(), + field_attrs.as_.is_some(), + field_attrs.skip.is_some(), + ]) > 0 => + { return Err(Error::new( attr.kw_span(), - "attributes `with` and `as_bytes` cannot be used together", + "attributes `with`, `as_bytes`, `as` and 'skip` cannot be used together", )); } - attrs::Attr::AsBytes(_) if field_attrs.as_bytes.is_some() => { - return Err(Error::new(attr.kw_span(), "attribute is duplicated")) - } attrs::Attr::AsBytes(attr) => { field_attrs.as_bytes = Some(attr); } - attrs::Attr::With(_) if field_attrs.with.is_some() => { - return Err(Error::new(attr.kw_span(), "attribute is duplicated")) - } attrs::Attr::With(attr) => { field_attrs.with = Some(attr); } - attrs::Attr::Skip(_) if field_attrs.skip.is_some() => { - return Err(Error::new(attr.kw_span(), "attribute is duplicated")); - } attrs::Attr::Skip(attr) => { field_attrs.skip = Some(attr); } - attrs::Attr::Rename(_) if field_attrs.rename.is_some() => { - return Err(Error::new(attr.kw_span(), "attribute is duplicated")) - } attrs::Attr::Rename(attr) => { field_attrs.rename = Some(attr); } + attrs::Attr::As(mut attr) => { + attr.value = type_replace_infer(attr.value, same_ty.clone())?; + field_attrs.as_ = Some(attr); + } _ => return Err(Error::new(attr.kw_span(), "attribute is not allowed here")), } } Ok(Field { + span: field.ty.span(), attrs: field_attrs, mem, + ty: field.ty.clone(), }) } +fn count_trues(i: impl IntoIterator) -> usize { + i.into_iter().filter(|x| *x).count() +} + +/// Traverses the type and replaces `_` with `infer_ty` +/// +/// E.g. `Option<_>` becomes `Option<{infer_ty}>`. +/// +/// Returns an error if provided type is not supported. It supports any types that +/// can be found as the type of field in the struct. For instance, `impl Trait` is +/// not supported. +/// +/// The function only traverses some types such as: path type (e.g. `std::result::Result`), +/// arrays, slices, tuples, references, pointers. It does not traverse anything else, +/// like function pointers or trait objects. E.g. `fn(_) -> u32` or `Box` are +/// not modified by the function. +fn type_replace_infer(ty: syn::Type, infer_ty: syn::Type) -> Result { + match ty { + syn::Type::Infer(_) => Ok(infer_ty), + + syn::Type::Array(ty) => Ok(syn::Type::Array(syn::TypeArray { + bracket_token: ty.bracket_token, + elem: Box::new(type_replace_infer(*ty.elem, infer_ty)?), + semi_token: ty.semi_token, + len: ty.len, + })), + syn::Type::Group(ty) => Ok(syn::Type::Group(syn::TypeGroup { + group_token: ty.group_token, + elem: Box::new(type_replace_infer(*ty.elem, infer_ty)?), + })), + syn::Type::Paren(ty) => Ok(syn::Type::Paren(syn::TypeParen { + paren_token: ty.paren_token, + elem: Box::new(type_replace_infer(*ty.elem, infer_ty)?), + })), + syn::Type::Path(ty) => Ok(syn::Type::Path(syn::TypePath { + qself: ty.qself, + path: syn::Path { + leading_colon: ty.path.leading_colon, + // Traverse each segment of the path, e.g.: + // + // std::result::Result + // 1 --| 2 ----| 3 --------| + segments: ty + .path + .segments + .into_pairs() + .map(|pair| { + let (seg, sep) = pair.into_tuple(); + let args = match seg.arguments { + syn::PathArguments::None => syn::PathArguments::None, + syn::PathArguments::Parenthesized(x) => { + return Err(Error::new(x.span(), "not allowed in this context")) + } + // Result + // ^----^ angle-bracketed arguments + syn::PathArguments::AngleBracketed(args) => { + syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { + colon2_token: args.colon2_token, + lt_token: args.lt_token, + // traverse each path argument + args: args + .args + .into_pairs() + .map(|pair| { + let (arg, comma) = pair.into_tuple(); + let arg = match arg { + // type argument => need to traverse + syn::GenericArgument::Type(ty) => { + syn::GenericArgument::Type( + type_replace_infer( + ty, + infer_ty.clone(), + )?, + ) + } + // other arguments we do not care about, like lifetimes + _ => arg, + }; + Ok(syn::punctuated::Pair::new(arg, comma)) + }) + .collect::>()?, + gt_token: args.gt_token, + }, + ) + } + }; + + Ok(syn::punctuated::Pair::new( + syn::PathSegment { + ident: seg.ident, + arguments: args, + }, + sep, + )) + }) + .collect::>()?, + }, + })), + syn::Type::Ptr(ty) => Ok(syn::Type::Ptr(syn::TypePtr { + star_token: ty.star_token, + const_token: ty.const_token, + mutability: ty.mutability, + elem: Box::new(type_replace_infer(*ty.elem, infer_ty)?), + })), + syn::Type::Reference(ty) => Ok(syn::Type::Reference(syn::TypeReference { + and_token: ty.and_token, + lifetime: ty.lifetime, + mutability: ty.mutability, + elem: Box::new(type_replace_infer(*ty.elem, infer_ty)?), + })), + syn::Type::Slice(ty) => Ok(syn::Type::Slice(syn::TypeSlice { + bracket_token: ty.bracket_token, + elem: Box::new(type_replace_infer(*ty.elem, infer_ty)?), + })), + syn::Type::Tuple(ty) => Ok(syn::Type::Tuple(syn::TypeTuple { + paren_token: ty.paren_token, + // Traverse each type in the tuple + elems: ty + .elems + .into_pairs() + .map(|pair| { + let (ty, comma) = pair.into_tuple(); + let ty = type_replace_infer(ty, infer_ty.clone())?; + Ok(syn::punctuated::Pair::new(ty, comma)) + }) + .collect::>()?, + })), + + // Following types are not traversed + syn::Type::BareFn(_) + | syn::Type::Macro(_) + | syn::Type::Never(_) + | syn::Type::TraitObject(_) + | syn::Type::Verbatim(_) => Ok(ty), + + // Following types are not supported + syn::Type::ImplTrait(_) => Err(Error::new( + ty.span(), + "`impl Trait` is not supported in this context", + )), + + // This might happen if Rust gets a new type in the future + _ => Err(Error::new(ty.span(), "unknown type")), + } +} + fn generate_impl_for_enum( attrs: &ContainerAttrs, enum_name: &syn::Ident, @@ -215,8 +396,9 @@ fn generate_impl_for_enum( &root_path, &encoder_var, &f.attrs, - f.mem.span(), + f.span, &f.stringify_field_name(), + &f.ty, &binding, ) }); @@ -280,9 +462,10 @@ fn generate_impl_for_struct( &root_path, &encoder_var, &f.attrs, - f.mem.span(), + f.span, &f.stringify_field_name(), - "e_spanned! {f.mem.span() => &self.#mem}, + &f.ty, + "e_spanned! {f.ty.span() => &self.#mem}, ) }); @@ -338,14 +521,14 @@ fn make_where_clause( let generated_predicates = match &attrs.bound { Some(bound) => { - let overriden_where_clause: proc_macro2::TokenStream = bound + let overridden_where_clause: proc_macro2::TokenStream = bound .value .value() .parse() .map_err(|err| Error::new(bound.value.span(), err))?; let predicates = syn::parse::Parser::parse2( syn::punctuated::Punctuated::::parse_terminated, - overriden_where_clause + overridden_where_clause ) .map_err(|err| Error::new(bound.value.span(), err))?; let predicates = predicates.iter(); @@ -379,6 +562,7 @@ fn encode_field( field_attrs: &FieldAttrs, field_span: proc_macro2::Span, field_name: &str, + field_type: &syn::Type, field_ref: &impl quote::ToTokens, ) -> proc_macro2::TokenStream { if field_attrs.skip.is_some() { @@ -390,11 +574,8 @@ fn encode_field( Some(attrs::Rename { rename, value, .. }) => quote_spanned! { rename.span => #value }, }; - match (&field_attrs.as_bytes, &field_attrs.with) { - (Some(_), Some(_)) => { - unreachable!("it should have been validated that `with` and `as_bytes` are not used in the same time") - } - (Some(attr), None) => match &attr.value { + match (&field_attrs.as_bytes, &field_attrs.with, &field_attrs.as_) { + (Some(attr), None, None) => match &attr.value { Some(func) => quote_spanned! {field_span => { let field_encoder = #encoder_var.add_field(#field_name); let field_bytes = #func(#field_ref); @@ -407,15 +588,23 @@ fn encode_field( field_encoder.encode_leaf_value(field_bytes); }), }, - (None, Some(attrs::With { value: func, .. })) => quote_spanned! {field_span => { + (None, Some(attrs::With { value: func, .. }), None) => quote_spanned! {field_span => { let field_encoder = #encoder_var.add_field(#field_name); #[allow(clippy::needless_borrow, clippy::needless_borrows_for_generic_args)] #func(#field_ref, field_encoder); }}, - (None, None) => quote_spanned! {field_span => { + (None, None, Some(attrs::As { value: ty, .. })) => quote_spanned! {field_span => { + let field_encoder = #encoder_var.add_field(#field_name); + #[allow(clippy::needless_borrow, clippy::needless_borrows_for_generic_args)] + <#ty as #root_path::DigestAs<#field_type>>::digest_as(#field_ref, field_encoder) + }}, + (None, None, None) => quote_spanned! {field_span => { let field_encoder = #encoder_var.add_field(#field_name); #root_path::Digestable::unambiguously_encode(#field_ref, field_encoder); }}, + _ => { + unreachable!("it should have been validated that `with`, `as_bytes`, `as` are not used in the same time") + } } } @@ -445,11 +634,14 @@ struct FieldAttrs { skip: Option, rename: Option, with: Option, + as_: Option, } struct Field { + span: proc_macro2::Span, attrs: FieldAttrs, mem: syn::Member, + ty: syn::Type, } impl Field { diff --git a/udigest/Cargo.toml b/udigest/Cargo.toml index 0fdaaef..8d946ef 100644 --- a/udigest/Cargo.toml +++ b/udigest/Cargo.toml @@ -49,6 +49,11 @@ required-features = ["derive", "digest"] name = "inline_struct" required-features = ["derive", "inline-struct"] +[[test]] +name = "digest_as" +required-features = ["derive", "inline-struct"] + [[example]] name = "derivation" required-features = ["std", "derive", "digest"] + diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index ae0f76e..d9d6c96 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -76,8 +76,7 @@ where } } -/// Digests any bytestring that implements `impl AsRef<[u8]>` -pub type Bytes = crate::Bytes<()>; +pub use crate::Bytes; impl DigestAs for Bytes where diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 2ec0cf8..55ab79e 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -308,7 +308,7 @@ impl Digestable for &T { /// Wrapper for a bytestring /// /// Wraps any bytestring that `impl AsRef<[u8]>` and provides [`Digestable`] trait implementation -pub struct Bytes(pub T); +pub struct Bytes(pub T); impl + ?Sized> Digestable for Bytes { fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { diff --git a/udigest/tests/common/mod.rs b/udigest/tests/common/mod.rs new file mode 100644 index 0000000..60e662a --- /dev/null +++ b/udigest/tests/common/mod.rs @@ -0,0 +1,16 @@ +/// A buffer based on `Vec`. Writing to the buffer +/// appends data to the vector +pub struct VecBuf(pub Vec); + +impl udigest::encoding::Buffer for VecBuf { + fn write(&mut self, bytes: &[u8]) { + self.0.extend_from_slice(bytes) + } +} + +/// Encodes digestable data into bytes +pub fn encode_to_vec(x: &impl udigest::Digestable) -> Vec { + let mut buffer = VecBuf(vec![]); + x.unambiguously_encode(udigest::encoding::EncodeValue::new(&mut buffer)); + buffer.0 +} diff --git a/udigest/tests/derive.rs b/udigest/tests/derive.rs index 0f6e2d4..3a024c0 100644 --- a/udigest/tests/derive.rs +++ b/udigest/tests/derive.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + #[derive(udigest::Digestable)] #[udigest(tag = concat!("udigest.example", ".v1"))] pub struct DigestableExample { @@ -102,3 +104,11 @@ pub struct StructWithRefs<'a> { #[udigest(with = encoding::encode_bar)] field4: &'a Bar, } + +#[derive(udigest::Digestable)] +pub struct OptionalBytes { + #[udigest(as = Option)] + field1: Option>, + #[udigest(as = std::collections::BTreeMap<_, udigest::Bytes>)] + hash_map: std::collections::HashMap>, +} diff --git a/udigest/tests/digest_as.rs b/udigest/tests/digest_as.rs new file mode 100644 index 0000000..91d8762 --- /dev/null +++ b/udigest/tests/digest_as.rs @@ -0,0 +1,100 @@ +mod common; + +#[test] +fn list_of_bytestrings() { + #[derive(udigest::Digestable)] + struct Post { + title: String, + content: String, + #[udigest(as = Vec)] + images: Vec>, + } + + impl Post { + fn digest_expected(&self) -> impl udigest::Digestable + '_ { + udigest::inline_struct!({ + title: &self.title, + content: &self.content, + images: self.images + .clone() + .into_iter() + .map(udigest::Bytes) + .collect::>(), + }) + } + } + + let post = Post { + title: "My first post!".into(), + content: "This is the first post I've ever written!".into(), + images: vec![b"some image".to_vec()], + }; + + let expected = common::encode_to_vec(&post.digest_expected()); + let actual = common::encode_to_vec(&post); + + assert_eq!(hex::encode(expected), hex::encode(actual)); +} + +#[test] +fn hash_map() { + #[derive(udigest::Digestable)] + struct Attributes( + #[udigest(as = std::collections::BTreeMap<_, udigest::Bytes>)] + std::collections::HashMap>, + ); + + #[derive(udigest::Digestable)] + struct EncodingExpected(std::collections::BTreeMap>>); + + impl Attributes { + fn digest_expected(&self) -> impl udigest::Digestable + '_ { + let encoding = self + .0 + .iter() + .map(|(k, v)| (k.clone(), udigest::Bytes(v.clone()))) + .collect::>(); + EncodingExpected(encoding) + } + } + + let attrs = Attributes(FromIterator::from_iter([ + ("some_attr".to_string(), b"value1".to_vec()), + ("attr".to_string(), b"value2".to_vec()), + ("some_other_attr".to_string(), b"value3".to_vec()), + ])); + + let expected = common::encode_to_vec(&attrs.digest_expected()); + let actual = common::encode_to_vec(&attrs); + + assert_eq!(hex::encode(expected), hex::encode(actual)); +} + +#[test] +fn option() { + #[derive(udigest::Digestable)] + struct Person { + nickname: String, + #[udigest(as = Option)] + avatar: Option>, + } + + impl Person { + fn digest_expected(&self) -> impl udigest::Digestable + '_ { + udigest::inline_struct!({ + nickname: &self.nickname, + avatar: self.avatar.as_ref().map(udigest::Bytes) + }) + } + } + + let person = Person { + nickname: "c00l_name".to_string(), + avatar: Some(b"image".to_vec()), + }; + + let expected = common::encode_to_vec(&person.digest_expected()); + let actual = common::encode_to_vec(&person); + + assert_eq!(hex::encode(expected), hex::encode(actual)); +} From ea101596af4ccc8a9e276a91405d95092c5162e5 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 21 Aug 2024 16:02:17 +0200 Subject: [PATCH 05/15] Add one more test Signed-off-by: Denis Varlakov --- udigest/tests/digest_as.rs | 32 ++++++++++++++++++++++++++++++++ udigest/tests/encoding.rs | 10 ++-------- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/udigest/tests/digest_as.rs b/udigest/tests/digest_as.rs index 91d8762..7f61f33 100644 --- a/udigest/tests/digest_as.rs +++ b/udigest/tests/digest_as.rs @@ -98,3 +98,35 @@ fn option() { assert_eq!(hex::encode(expected), hex::encode(actual)); } + +#[test] +fn array() { + #[derive(udigest::Digestable)] + struct Foo { + #[udigest(as = [udigest::Bytes; 5])] + bar: [Vec; 5], + } + + impl Foo { + fn digest_expected(&self) -> impl udigest::Digestable + '_ { + udigest::inline_struct!({ + bar: self.bar.each_ref().map(udigest::Bytes), + }) + } + } + + let foo = Foo { + bar: [ + b"abcd".to_vec(), + b"aaaa".to_vec(), + b"..-..---.-..-".to_vec(), + b"bbbbbb".to_vec(), + b"finally".to_vec(), + ], + }; + + let expected = common::encode_to_vec(&foo.digest_expected()); + let actual = common::encode_to_vec(&foo); + + assert_eq!(hex::encode(expected), hex::encode(actual)); +} diff --git a/udigest/tests/encoding.rs b/udigest/tests/encoding.rs index b64f33c..27e4047 100644 --- a/udigest/tests/encoding.rs +++ b/udigest/tests/encoding.rs @@ -1,14 +1,8 @@ use udigest::encoding::*; -/// A buffer based on `Vec`. Writing to the buffer -/// appends data to the vector -pub struct VecBuf(Vec); +use common::VecBuf; -impl udigest::encoding::Buffer for VecBuf { - fn write(&mut self, bytes: &[u8]) { - self.0.extend_from_slice(bytes) - } -} +mod common; macro_rules! concat_bytes_into_vec { ($($bytes:expr),*$(,)?) => {{ From f0534e9fbfa2eae532c62e2655caf3fb38de3b7e Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 21 Aug 2024 16:20:48 +0200 Subject: [PATCH 06/15] Fix compilation Signed-off-by: Denis Varlakov --- udigest/tests/common/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/udigest/tests/common/mod.rs b/udigest/tests/common/mod.rs index 60e662a..dddebea 100644 --- a/udigest/tests/common/mod.rs +++ b/udigest/tests/common/mod.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + /// A buffer based on `Vec`. Writing to the buffer /// appends data to the vector pub struct VecBuf(pub Vec); From 6273d6534f6a6f5a504f194fad16d2646e514b42 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 21 Aug 2024 16:27:15 +0200 Subject: [PATCH 07/15] Add docs Signed-off-by: Denis Varlakov --- udigest/src/as_.rs | 12 ++++++++++++ udigest/src/lib.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index d9d6c96..d48ec76 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -1,4 +1,16 @@ //! Provides utilities for custom digesting rules +//! +//! It's supposed to be used in a pair with derive proc macro and `as` attribute. +//! For instance, it can be used to digest a hash map "as a btree map": +//! ```rust +//! #[derive(udigest::Digestable)] +//! pub struct Attributes( +//! #[udigest(as = std::collections::BTreeMap<_, udigest::Bytes>)] +//! std::collections::HashMap>, +//! ); +//! ``` +//! +//! See more examples in [macro@Digestable] macro docs. use crate::{encoding, Buffer, Digestable}; diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 55ab79e..ab24b75 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -185,6 +185,47 @@ pub use encoding::Buffer; /// todo!() /// } /// ``` +/// * `#[udigest(as = ...)]` \ +/// Tells to encode the field as another type `U`. Proc macro will use +/// [`>::digest_as`](DigestAs) to encode this field. +/// +/// It is similar to `#[udigest(with = ...)]` attribute described above which allows to specify +/// a function which will be used to encode a field value. `#[udigest(as = ...)]` is designed for +/// more complex use-cases. For instance, it can be used to digest a hash map: +/// +/// ```rust +/// #[derive(udigest::Digestable)] +/// pub struct Attributes( +/// #[udigest(as = std::collections::BTreeMap<_, udigest::Bytes>)] +/// std::collections::HashMap>, +/// ); +/// ``` +/// +/// When structure is digested, the hash map is converted into btree map. `_` in `BTreeMap<_, udigest::Bytes>` +/// says that the key should be kept as it is: `String` string will be digested. +/// `udigest::Bytes` indicates that the value `Vec` should be digested as bytes, not as +/// list of u8 which would be a default behavior. +/// +/// Similarly, if we have a field of type `Option>` and we want it to be encoded as +/// bytes, we cannot use `#[udigest(as_bytes)]` as the field does not implement `AsRef<[u8]>`: +/// +/// ```compile_fail +/// #[derive(udigest::Digestable)] +/// pub struct Data( +/// #[udigest(as_bytes)] +/// Option>, +/// ); +/// ``` +/// +/// We can use `as` attribute instead: +/// ```rust +/// #[derive(udigest::Digestable)] +/// pub struct Data( +/// #[udigest(as = Option)] +/// Option>, +/// ); +/// ``` +/// /// * `#[udigest(rename = "...")]` \ /// Specifies another name to use for the field. As field name gets mixed into the hash, /// changing the field name will change the hash. Sometimes, it may be required to change From 0b203fb6797920b68e18c49457fae32a8449f971 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 21 Aug 2024 16:43:28 +0200 Subject: [PATCH 08/15] Update CI, fix no-alloc compilation Signed-off-by: Denis Varlakov --- .github/workflows/rust.yml | 4 ++-- udigest/src/as_.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e47a61b..22a6a30 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -21,9 +21,9 @@ jobs: with: cache-on-failure: "true" - name: Check - run: cargo check -p udigest --features "${{ matrix.features }}" + run: cargo check -p udigest --no-default-features --features "${{ matrix.features }}" - name: Run tests - run: cargo test -p udigest --lib --tests --features "${{ matrix.features }}" + run: cargo test -p udigest --lib --tests --no-default-features --features "${{ matrix.features }}" test-all-features: runs-on: ubuntu-latest steps: diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index d48ec76..3cedbc2 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -152,7 +152,7 @@ where crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) } } - +#[cfg(feature = "alloc")] impl DigestAs> for alloc::collections::LinkedList where U: DigestAs, @@ -164,6 +164,7 @@ where crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) } } +#[cfg(feature = "alloc")] impl DigestAs> for alloc::collections::VecDeque where U: DigestAs, @@ -175,6 +176,7 @@ where crate::unambiguously_encode_iter(encoder, value.iter().map(As::<&T, &U>::new)) } } +#[cfg(feature = "alloc")] impl DigestAs> for alloc::collections::BTreeSet where U: DigestAs, From 3ed5e22a0d628a69d7a6f2ec5ad3403200435232 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Wed, 21 Aug 2024 17:26:42 +0200 Subject: [PATCH 09/15] Bump versions, update changelog Signed-off-by: Denis Varlakov --- udigest-derive/CHANGELOG.md | 5 +++++ udigest-derive/Cargo.toml | 2 +- udigest/CHANGELOG.md | 5 +++++ udigest/Cargo.toml | 4 ++-- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/udigest-derive/CHANGELOG.md b/udigest-derive/CHANGELOG.md index 70ccf66..680c5d2 100644 --- a/udigest-derive/CHANGELOG.md +++ b/udigest-derive/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.3.0 +* Add `#[udigest(as = ...)]` attribute support [#12] + +[#12]: https://github.com/dfns/udigest/pull/12 + ## v0.2.0 * Fix proc macro causing clippy warnings in certain cases [#6] diff --git a/udigest-derive/Cargo.toml b/udigest-derive/Cargo.toml index e414cc9..c185fb4 100644 --- a/udigest-derive/Cargo.toml +++ b/udigest-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "udigest-derive" -version = "0.2.0" +version = "0.3.0" edition = "2021" description = "Proc macro for `udigest` crate" license = "MIT OR Apache-2.0" diff --git a/udigest/CHANGELOG.md b/udigest/CHANGELOG.md index d935753..42b1d4c 100644 --- a/udigest/CHANGELOG.md +++ b/udigest/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.3.0 +* Add `#[udigest(as = ...)]` attribute and `DeriveAs` trait [#12] + +[#12]: https://github.com/dfns/udigest/pull/12 + ## v0.2.0 * Breaking change: remove `udigest::Tag` [#4] * Breaking change: rename `udigest::udigest` function to `udigest::hash` [#4] diff --git a/udigest/Cargo.toml b/udigest/Cargo.toml index 8d946ef..ccb7033 100644 --- a/udigest/Cargo.toml +++ b/udigest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "udigest" -version = "0.2.0" +version = "0.2.1" edition = "2021" license = "MIT OR Apache-2.0" description = "Unambiguously digest structured data" @@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] digest = { version = "0.10", default-features = false, optional = true } -udigest-derive = { version = "0.2", path = "../udigest-derive", optional = true } +udigest-derive = { version = "0.3", path = "../udigest-derive", optional = true } [dev-dependencies] hex = "0.4" From 9c53bd9cf907ac329cc885646287921c52552f62 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 22 Aug 2024 16:58:40 +0200 Subject: [PATCH 10/15] Rename generics Signed-off-by: Denis Varlakov --- Cargo.lock | 4 ++-- udigest/src/as_.rs | 38 +++++++++++++++++++------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b2213e..d32ae57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "udigest" -version = "0.2.0" +version = "0.2.1" dependencies = [ "blake2", "digest", @@ -163,7 +163,7 @@ dependencies = [ [[package]] name = "udigest-derive" -version = "0.2.0" +version = "0.3.0" dependencies = [ "proc-macro2", "quote", diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index 3cedbc2..2911975 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -111,16 +111,16 @@ where } } -impl DigestAs> for Result +impl DigestAs> for Result where - T2: DigestAs, - E2: DigestAs, + TAs: DigestAs, + EAs: DigestAs, { - fn digest_as(value: &Result, encoder: encoding::EncodeValue) { + fn digest_as(value: &Result, encoder: encoding::EncodeValue) { value .as_ref() - .map(As::<&T1, &T2>::new) - .map_err(As::<&E1, &E2>::new) + .map(As::<&T, &TAs>::new) + .map_err(As::<&E, &EAs>::new) .unambiguously_encode(encoder) } } @@ -190,41 +190,41 @@ where } #[cfg(feature = "alloc")] -impl DigestAs> - for alloc::collections::BTreeMap +impl DigestAs> + for alloc::collections::BTreeMap where - K2: DigestAs, - V2: DigestAs, + KAs: DigestAs, + VAs: DigestAs, { fn digest_as( - value: &alloc::collections::BTreeMap, + value: &alloc::collections::BTreeMap, encoder: encoding::EncodeValue, ) { crate::unambiguously_encode_iter( encoder, value .iter() - .map(|(key, value)| (As::<&K1, &K2>::new(key), As::<&V1, &V2>::new(value))), + .map(|(key, value)| (As::<&K, &KAs>::new(key), As::<&V, &VAs>::new(value))), ) } } /// Digests `HashMap` by transforming it into `BTreeMap` #[cfg(feature = "std")] -impl DigestAs> - for alloc::collections::BTreeMap +impl DigestAs> + for alloc::collections::BTreeMap where - K2: DigestAs, - V2: DigestAs, - K1: core::cmp::Ord, + KAs: DigestAs, + VAs: DigestAs, + K: core::cmp::Ord, { fn digest_as( - value: &std::collections::HashMap, + value: &std::collections::HashMap, encoder: encoding::EncodeValue, ) { let ordered_map = value .iter() - .map(|(key, value)| (As::<&K1, &K2>::new(key), As::<&V1, &V2>::new(value))) + .map(|(key, value)| (As::<&K, &KAs>::new(key), As::<&V, &VAs>::new(value))) .collect::>(); // ordered map has deterministic order, so we can reproducibly hash it From cf054b8b6212f71d2126981356386adcd34d8a52 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 22 Aug 2024 17:01:00 +0200 Subject: [PATCH 11/15] Add digest as for HashSet Signed-off-by: Denis Varlakov --- udigest/src/as_.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index 2911975..15be504 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -209,6 +209,27 @@ where } } +/// Digests `HashSet` by transforming it into `BTreeSet` +#[cfg(feature = "std")] +impl DigestAs> for alloc::collections::BTreeSet +where + U: DigestAs, + T: core::cmp::Ord, +{ + fn digest_as( + value: &std::collections::HashSet, + encoder: encoding::EncodeValue, + ) { + let ordered_map = value + .iter() + .map(|x| As::<&T, &U>::new(x)) + .collect::>(); + + // ordered set has deterministic order, so we can reproducibly hash it + ordered_map.unambiguously_encode(encoder) + } +} + /// Digests `HashMap` by transforming it into `BTreeMap` #[cfg(feature = "std")] impl DigestAs> From 14a58eb6420d377d419b3970e688f425bad3b518 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 22 Aug 2024 17:26:30 +0200 Subject: [PATCH 12/15] Impl DigestAs for tuples Signed-off-by: Denis Varlakov --- udigest/src/as_.rs | 39 +++++++++++++++++++++++++++++++++++++++ udigest/src/lib.rs | 29 ++++++++++++++++++----------- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index 15be504..dd5d16d 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -304,3 +304,42 @@ where U::digest_as(value.as_ref(), encoder) } } + +macro_rules! impl_for_tuples { + ($($t:ident, $as:ident),*) => { + impl<$($t, $as),*> DigestAs<($($t,)*)> for ($($as,)*) + where + $($as: DigestAs<$t>),* + { + fn digest_as(value: &($($t,)*), encoder: encoding::EncodeValue) { + #[allow(non_snake_case)] + let ($($t,)*) = value; + ( + $(As::<&$t, &$as>::new($t),)* + ).unambiguously_encode(encoder) + } + } + }; +} + +#[rustfmt::skip] +mod tuples { + use super::*; + + impl_for_tuples!(T, U); + impl_for_tuples!(T0, As0, T1, As1); + impl_for_tuples!(T0, As0, T1, As1, T2, As2); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8, T9, As9); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8, T9, As9, T10, As10); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8, T9, As9, T10, As10, T11, As11); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8, T9, As9, T10, As10, T11, As11, T12, As12); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8, T9, As9, T10, As10, T11, As11, T12, As12, T13, As13); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8, T9, As9, T10, As10, T11, As11, T12, As12, T13, As13, T14, As14); + impl_for_tuples!(T0, As0, T1, As1, T2, As2, T3, As3, T4, As4, T5, As5, T6, As6, T7, As7, T8, As8, T9, As9, T10, As10, T11, As11, T12, As12, T13, As13, T14, As14, T15, As15); +} diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index ab24b75..99866d7 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -480,10 +480,10 @@ impl Digestable for Result { macro_rules! digestable_tuple { ($($letter:ident),+) => { - impl<$($letter: Digestable),+> Digestable for ($($letter),+) { + impl<$($letter: Digestable),+> Digestable for ($($letter,)+) { fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { #[allow(non_snake_case)] - let ($($letter),+) = self; + let ($($letter,)+) = self; let mut list = encoder.encode_list(); $( let item_encoder = list.add_item(); @@ -494,16 +494,23 @@ macro_rules! digestable_tuple { }; } -macro_rules! digestable_tuples { - ($letter:ident) => {}; - ($letter:ident, $($others:ident),+) => { - digestable_tuple!($letter, $($others),+); - digestable_tuples!($($others),+); - } -} - // We support tuples with up to 16 elements -digestable_tuples!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); +digestable_tuple!(A); +digestable_tuple!(A, B); +digestable_tuple!(A, B, C); +digestable_tuple!(A, B, C, D); +digestable_tuple!(A, B, C, D, E); +digestable_tuple!(A, B, C, D, E, F); +digestable_tuple!(A, B, C, D, E, F, G); +digestable_tuple!(A, B, C, D, E, F, G, H); +digestable_tuple!(A, B, C, D, E, F, G, H, I); +digestable_tuple!(A, B, C, D, E, F, G, H, I, J); +digestable_tuple!(A, B, C, D, E, F, G, H, I, J, K); +digestable_tuple!(A, B, C, D, E, F, G, H, I, J, K, L); +digestable_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M); +digestable_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N); +digestable_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O); +digestable_tuple!(A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P); fn unambiguously_encode_iter( encoder: encoding::EncodeValue, From 18d760fd41143c520da6ba86982d4704c1369689 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 22 Aug 2024 17:29:40 +0200 Subject: [PATCH 13/15] Update docs Signed-off-by: Denis Varlakov --- udigest/src/as_.rs | 14 +++++++------- udigest/src/lib.rs | 4 +++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index dd5d16d..a5ba085 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -2,13 +2,13 @@ //! //! It's supposed to be used in a pair with derive proc macro and `as` attribute. //! For instance, it can be used to digest a hash map "as a btree map": -//! ```rust -//! #[derive(udigest::Digestable)] -//! pub struct Attributes( -//! #[udigest(as = std::collections::BTreeMap<_, udigest::Bytes>)] -//! std::collections::HashMap>, -//! ); -//! ``` +//! ```rust +//! #[derive(udigest::Digestable)] +//! pub struct Attributes( +//! #[udigest(as = std::collections::BTreeMap<_, udigest::Bytes>)] +//! std::collections::HashMap>, +//! ); +//! ``` //! //! See more examples in [macro@Digestable] macro docs. diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 99866d7..122677e 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -202,7 +202,9 @@ pub use encoding::Buffer; /// ``` /// /// When structure is digested, the hash map is converted into btree map. `_` in `BTreeMap<_, udigest::Bytes>` -/// says that the key should be kept as it is: `String` string will be digested. +/// says that the key should be kept as it is: `String` string will be digested. The macro +/// replaces underscores (also called "infer types") with [`udigest::as_::Same`], which +/// indicates that the same digestion rules should be used as for the original type. /// `udigest::Bytes` indicates that the value `Vec` should be digested as bytes, not as /// list of u8 which would be a default behavior. /// From a7d8e6dfe05af799729cfafafcad5abbe05cf105 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 22 Aug 2024 17:31:21 +0200 Subject: [PATCH 14/15] clippy fix Signed-off-by: Denis Varlakov --- udigest/src/as_.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/udigest/src/as_.rs b/udigest/src/as_.rs index a5ba085..79a061a 100644 --- a/udigest/src/as_.rs +++ b/udigest/src/as_.rs @@ -222,7 +222,7 @@ where ) { let ordered_map = value .iter() - .map(|x| As::<&T, &U>::new(x)) + .map(As::<&T, &U>::new) .collect::>(); // ordered set has deterministic order, so we can reproducibly hash it From dc6242fb6964deba9a792307da548dd1e68b19c5 Mon Sep 17 00:00:00 2001 From: Denis Varlakov Date: Thu, 22 Aug 2024 17:32:45 +0200 Subject: [PATCH 15/15] Fix docs Signed-off-by: Denis Varlakov --- udigest/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 122677e..ab7c7e8 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -203,7 +203,7 @@ pub use encoding::Buffer; /// /// When structure is digested, the hash map is converted into btree map. `_` in `BTreeMap<_, udigest::Bytes>` /// says that the key should be kept as it is: `String` string will be digested. The macro -/// replaces underscores (also called "infer types") with [`udigest::as_::Same`], which +/// replaces underscores (also called "infer types") with [`Same`](crate::as_::Same), which /// indicates that the same digestion rules should be used as for the original type. /// `udigest::Bytes` indicates that the value `Vec` should be digested as bytes, not as /// list of u8 which would be a default behavior.