Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a way to customize Debug output without implementing the Debug trait directly?

Tags:

rust

Consider the following code:

#[derive(Debug)]
struct Test {
    int: u8,
    bits: u8,
    hex: u8
}

fn main() {
    let t = Test {
        int: 1,
        bits: 2,
        hex: 3
    };
    println!("{:#?}", t);
}

When run, it outputs the following:

Test {
    int: 1,
    bits: 2,
    hex: 3
}

That's cool that we can dump a struct with very little effort, but some of my data structures contain bit masks or other data not easily read in base 10. For the bits field, it would be much easier to read it they were output like 0b00000010.

Is there a simple way to get output like this, without implementing the Debug trait directly on the struct (a Debug trait on another type would be fine)?

Test {
    int: 1,
    bits: 0b00000010,
    hex: 0x03
}

I'm fine with using different types if I have to, but I'm hoping to keep the debug trait on the struct itself as simple as #[derive(Debug)].

like image 588
Alexander O'Mara Avatar asked Oct 14 '17 21:10

Alexander O'Mara


1 Answers

Is there a simple way to get output like this, without implementing the Debug trait directly on the struct (a Debug trait on another type would be fine)

Yes. #[derive(Debug)] implements Debug by just calling Debug::debug on each of the members in turn. Follow the instructions in How to implement a custom 'fmt::Debug' trait? for your newtype wrapper:

use std::fmt;

#[derive(Debug)]
struct Test {
    int: u8,
    bits: Bits,
    hex: u8,
}

struct Bits(u8);

impl fmt::Debug for Bits {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "0b{:08b}", self.0)
    }
}

In a different direction, it would not be strange to have a method on your primary struct that returns another struct "for display purposes", akin to std::path::Display. This allows you to move complicated display logic to a separate type while allowing your original struct to not have newtypes that might get in your way:

use std::fmt;

#[derive(Debug)]
struct Test {
    int: u8,
    bits: u8,
    hex: u8,
}

impl Test {
    fn pretty_debug(&self) -> PrettyDebug<'_> {
        PrettyDebug {
            int: &self.int,
            bits: Bits(&self.bits),
            hex: &self.hex,
        }
    }
}

#[derive(Debug)]
struct PrettyDebug<'a> {
    int: &'a u8,
    bits: Bits<'a>,
    hex: &'a u8,
}

struct Bits<'a>(&'a u8);

impl fmt::Debug for Bits<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "0b{:08b}", self.0)
    }
}

It's a little silly to have a reference to a u8, but references are the most generic solution here — choose appropriate data types for your case.

You could also implement Debug directly for your PrettyDebug type:

use std::fmt;

#[derive(Debug)]
struct Test {
    int: u8,
    bits: u8,
    hex: u8,
}

impl Test {
    fn pretty_debug(&self) -> PrettyDebug<'_> {
        PrettyDebug(self)
    }
}

struct PrettyDebug<'a>(&'a Test);

impl fmt::Debug for PrettyDebug<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("Test")
            .field("int", &self.0.int)
            .field("bits", &format_args!("0b{:08b}", self.0.bits))
            .field("hex", &format_args!("0x{:02x}", self.0.hex))
            .finish()
    }
}
like image 90
Shepmaster Avatar answered Sep 28 '22 22:09

Shepmaster