securefmt 0.1.5

Drop-in replacement for the Debug derive macro that hides fields marked as sensitive.
Documentation
use proc_macro2::{Ident, TokenStream};
use quote::{format_ident, quote};

use crate::errors::Result;
use crate::implementation::FmtBodySource;

pub fn debug(ident: &Ident, fmt_body_source: &impl FmtBodySource) -> Result<TokenStream> {
    let fmt_body = fmt_body_source.generate_fmt_body(&ident.to_string())?;
    let once_ident = format_ident!("WARN_ONCE_{}", ident.to_string().to_uppercase());
    let warning = warning(&once_ident);
    Ok(quote! {
        static #once_ident: std::sync::Once = std::sync::Once::new();

        impl core::fmt::Debug for #ident {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                #warning
                #fmt_body
            }
        }
    })
}

#[cfg(feature = "debug_mode")]
fn warning(once_ident: &Ident) -> TokenStream {
    quote! {
        #once_ident.call_once(|| {
            log::warn!("WARNING: securefmt debug_mode feature is active. Sensitive data may be leaked. It is strongly recommended to disable debug_mode in production releases.");
        });
    }
}

#[cfg(not(feature = "debug_mode"))]
fn warning(_: &Ident) -> TokenStream {
    TokenStream::new()
}

#[cfg(test)]
mod tests {
    use mockall::predicate;
    use pretty_assertions::assert_eq;
    use quote::{format_ident, quote};

    use crate::implementation::*;

    #[test]
    #[cfg_attr(feature = "debug_mode", ignore)]
    fn should_generate_debug_implementation_without_warning_when_debug_mode_is_inactive() {
        let ident = format_ident!("TestStruct");
        let mut formatting_data = MockFmtBodySource::new();
        formatting_data
            .expect_generate_fmt_body()
            .with(predicate::eq("TestStruct"))
            .returning(|_| Ok(quote! { FMTBODY }));

        assert_eq!(
            super::debug(&ident, &formatting_data)
                .expect("Should have succeeded")
                .to_string(),
            quote!(
                static WARN_ONCE_TESTSTRUCT: std::sync::Once = std::sync::Once::new();

                impl core::fmt::Debug for TestStruct {
                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                        FMTBODY
                    }
                }
            )
            .to_string()
        );
    }

    #[test]
    #[cfg(feature = "debug_mode")]
    fn should_generate_debug_implementation_with_warning_when_debug_mode_is_active() {
        let ident = format_ident!("TestStruct");
        let mut formatting_data = MockFmtBodySource::new();
        formatting_data
            .expect_generate_fmt_body()
            .with(predicate::eq("TestStruct"))
            .returning(|_| Ok(quote! { FMTBODY }));
        assert_eq!(
            super::debug(&ident, &formatting_data).expect("Should have succeeded").to_string(),
            quote!(
                static WARN_ONCE_TESTSTRUCT: std::sync::Once = std::sync::Once::new();

                impl core::fmt::Debug for TestStruct {
                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                        WARN_ONCE_TESTSTRUCT.call_once(|| {
                            log::warn!("WARNING: securefmt debug_mode feature is active. Sensitive data may be leaked. It is strongly recommended to disable debug_mode in production releases.");
                        });
                        FMTBODY
                    }
                }
            ).to_string()
        );
    }
}