const-struct-version 0.2.0

A trait plus proc-macro to generate a hash based on the fields of a struct. Useful for intelligently expiring a cache when the stored object changes.
Documentation
#![doc = include_str!("../README.md")]
#![deny(clippy::all, clippy::pedantic)]
#![allow(clippy::uninlined_format_args, clippy::needless_doctest_main)]

use std::any::type_name;

use sha1::Digest;

#[doc(hidden)]
pub mod __private {
    pub use sha1;

    pub fn execute_if_serde_enabled(hasher: &mut sha1::Sha1, f: impl FnOnce(&mut sha1::Sha1)) {
        if cfg!(feature = "serde-attributes") {
            f(hasher);
        }
    }
}

pub trait StructVersion {
    fn version() -> String;
}

#[cfg(feature = "derive")]
pub use const_struct_version_derive::StructVersion;

macro_rules! impl_struct_version {
    ($($t:ty),*) => {
        $(
            impl StructVersion for $t {
                fn version() -> String {
                    let mut hasher = sha1::Sha1::new();
                    hasher.update(type_name::<$t>());
                    format!("{:x}", hasher.finalize())
                }
            }
        )*
    };
}

impl_struct_version!(
    (),
    bool,
    char,
    u8,
    i8,
    u16,
    i16,
    u32,
    i32,
    u64,
    i64,
    u128,
    i128,
    f32,
    f64,
    String,
    std::time::Duration,
    std::time::Instant
);

#[cfg_attr(test, mutants::skip)]
impl<T: StructVersion> StructVersion for Vec<T> {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("Vec");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}
impl<T: StructVersion> StructVersion for Option<T> {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("Option");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}
impl<T: StructVersion> StructVersion for Box<T> {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("Box");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}
impl<T: StructVersion> StructVersion for std::rc::Rc<T> {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("Rc");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}
impl<T: StructVersion> StructVersion for std::sync::Arc<T> {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("Arc");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}

impl<K, V, S> StructVersion for std::collections::HashMap<K, V, S>
where
    K: StructVersion,
    V: StructVersion,
    S: std::hash::BuildHasher,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("HashMap");
        hasher.update(type_name::<K>());
        hasher.update(type_name::<V>());
        format!("{:x}", hasher.finalize())
    }
}

impl<K, V> StructVersion for std::collections::BTreeMap<K, V>
where
    K: StructVersion,
    V: StructVersion,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("BTreeMap");
        hasher.update(type_name::<K>());
        hasher.update(type_name::<V>());
        format!("{:x}", hasher.finalize())
    }
}

impl<K, S> StructVersion for std::collections::HashSet<K, S>
where
    K: StructVersion,
    S: std::hash::BuildHasher,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("HashSet");
        hasher.update(type_name::<K>());
        format!("{:x}", hasher.finalize())
    }
}

impl<K> StructVersion for std::collections::BTreeSet<K>
where
    K: StructVersion,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("BTreeSet");
        hasher.update(type_name::<K>());
        format!("{:x}", hasher.finalize())
    }
}

impl<T> StructVersion for std::collections::LinkedList<T>
where
    T: StructVersion,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("LinkedList");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}

impl<T> StructVersion for std::collections::VecDeque<T>
where
    T: StructVersion,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("VecDeque");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}

impl<T> StructVersion for std::collections::BinaryHeap<T>
where
    T: StructVersion,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("BinaryHeap");
        hasher.update(type_name::<T>());
        format!("{:x}", hasher.finalize())
    }
}

#[cfg(feature = "chrono")]
impl<Tz: chrono::TimeZone> StructVersion for chrono::DateTime<Tz> {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("DateTime");
        hasher.update(type_name::<Tz>());
        format!("{:x}", hasher.finalize())
    }
}

#[cfg(feature = "url")]
impl StructVersion for url::Url {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("Url");
        format!("{:x}", hasher.finalize())
    }
}

#[cfg(feature = "uuid")]
impl StructVersion for uuid::Uuid {
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("Uuid");
        format!("{:x}", hasher.finalize())
    }
}

#[cfg(feature = "indexmap")]
impl<K, V, S> StructVersion for indexmap::IndexMap<K, V, S>
where
    K: StructVersion,
    V: StructVersion,
    S: std::hash::BuildHasher,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("IndexMap");
        hasher.update(type_name::<K>());
        hasher.update(type_name::<V>());
        format!("{:x}", hasher.finalize())
    }
}

#[cfg(feature = "indexmap")]
impl<K> StructVersion for indexmap::IndexSet<K>
where
    K: StructVersion,
{
    fn version() -> String {
        let mut hasher = sha1::Sha1::new();
        hasher.update("IndexSet");
        hasher.update(type_name::<K>());
        format!("{:x}", hasher.finalize())
    }
}

#[cfg(test)]
mod tests {
    macro_rules! test {
        ($name:ident, $t:ty) => {
            #[test]
            fn $name() {
                let version = <$t as super::StructVersion>::version();
                insta::assert_snapshot!(version);
            }
        };
    }

    test!(test_bool, bool);
    test!(test_char, char);
    test!(test_u8, u8);
    test!(test_i8, i8);
    test!(test_u16, u16);
    test!(test_i16, i16);
    test!(test_u32, u32);
    test!(test_i32, i32);
    test!(test_u64, u64);
    test!(test_i64, i64);
    test!(test_u128, u128);
    test!(test_i128, i128);
    test!(test_f32, f32);
    test!(test_f64, f64);
    test!(test_string, String);
    test!(test_duration, std::time::Duration);
    test!(test_instant, std::time::Instant);
    test!(test_vec_u32, Vec<u32>);
    test!(test_option_u32, Option<u32>);
    test!(test_box_u32, Box<u32>);
    test!(test_rc_u32, std::rc::Rc<u32>);
    test!(test_arc_u32, std::sync::Arc<u32>);
    test!(test_hash_map_u32_u32, std::collections::HashMap<u32, u32>);
    test!(test_b_tree_map_u32_u32, std::collections::BTreeMap<u32, u32>);
    test!(test_hash_set_u32, std::collections::HashSet<u32>);
    test!(test_b_tree_set_u32, std::collections::BTreeSet<u32>);
    test!(test_linked_list_u32, std::collections::LinkedList<u32>);
    test!(test_vec_deque_u32, std::collections::VecDeque<u32>);
    test!(test_binary_heap_u32, std::collections::BinaryHeap<u32>);
    #[cfg(feature = "chrono")]
    test!(test_date_time_local, chrono::DateTime<chrono::Local>);
    #[cfg(feature = "url")]
    test!(test_url, url::Url);
    #[cfg(feature = "uuid")]
    test!(test_uuid, uuid::Uuid);
    #[cfg(feature = "indexmap")]
    test!(test_index_map_u32_u32, indexmap::IndexMap<u32, u32>);
    #[cfg(feature = "indexmap")]
    test!(test_index_set_u32, indexmap::IndexSet<u32>);
}