Debug implementation same as #[derive(Debug)] but with a tiny customisation?

I have an enum for which I derived Debug:

#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Reply {
    AddRcpt {
        rcpt: CString,
    },
    AddRcptExt {
        rcpt: CString,
        args: Option<CString>,
    },
    OptNeg {
        version: Version,
        actions: Actions,
        opts: ProtoOpts,
        macros: HashMap<Stage, CString>,
    },
    Accept,
    ReplaceBody { chunk: Vec<u8> },
    // ...
}

I am almost happy with the Debug implementation, except for the ReplaceBody { chunk: Vec<u8> } variant. I would like to format this byte vector like a byte literal (b"") instead of a list ([]).

How would I write a manual Debug implementation that is exactly like the generated one, except for this tiny adaptation?

You can match on the enum and then invoke the debug_struct() method on Formatter for pretty-printing the debug representation.

However, you could achieve the same effect with less boilerplace code by wrapping the Vec<u8> in a newtype wrapper that implements the correct format, and then keeping the #[derive(Debug)] instead of writing a fully manual impl.

Here is one possible implementation:

#[derive(Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DebugBytes(pub Vec<u8>);

impl Debug for DebugBytes {
    fn fmt(&self, fmt: &mut Formatter) -> Result {
        fmt.write_str("b\"")?;
        for &byte in &self.0 {
            write!(fmt, "\\x{:02x}", byte)?;
        }
        fmt.write_str("\"")
    }
}
2 Likes

If you use rust analyzer, you can use it to “convert to manual `impl std::fmt::Debug for Reply`”. This generates code like this

impl std::fmt::Debug for Reply {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::AddRcpt { rcpt } => f.debug_struct("AddRcpt").field("rcpt", rcpt).finish(),
            Self::AddRcptExt { rcpt, args } => f.debug_struct("AddRcptExt").field("rcpt", rcpt).field("args", args).finish(),
            Self::OptNeg { version, actions, opts, macros } => f.debug_struct("OptNeg").field("version", version).field("actions", actions).field("opts", opts).field("macros", macros).finish(),
            Self::Accept => write!(f, "Accept"),
            Self::ReplaceBody { chunk } => f.debug_struct("ReplaceBody").field("chunk", chunk).finish(),
        }
    }
}

which you can then customize.

1 Like

Yeah, in this case the boilerplate is massive (basically, redoing all the work of #[derive(Debug)] through std::fmt::Formatter) ... the newtype may be a good alternative. Thank you.

You can also implement From, Deref, AsRef, Borrow, etc. for the wrapper type so that it's easier to use in a generic context. Then you won't need to perform manual wrapping and unwrapping all the time.

In my case, the simplest solution is to replace Vec<u8> with bytes::Bytes, which has the right Debug representation. If I can live with exposing Bytes in my API then problem solved.

Thanks everybody!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.