9 releases (stable)

1.3.2 Nov 24, 2022
1.3.1 Nov 11, 2022
1.1.0 Jun 3, 2022
1.0.0 May 19, 2022
0.1.0 Mar 20, 2022

#551 in Rust patterns

Download history 223/week @ 2024-06-14 279/week @ 2024-06-21 342/week @ 2024-06-28 315/week @ 2024-07-05 207/week @ 2024-07-12 236/week @ 2024-07-19 275/week @ 2024-07-26 374/week @ 2024-08-02 344/week @ 2024-08-09 279/week @ 2024-08-16 258/week @ 2024-08-23 187/week @ 2024-08-30 145/week @ 2024-09-06 257/week @ 2024-09-13 277/week @ 2024-09-20 240/week @ 2024-09-27

930 downloads per month
Used in 2 crates

MIT license

45KB
877 lines

Cubob

Some Rust helpers for structural output in display mode.

Name

This project has autogenerated name, thanks to This Word Does Not Exist project. The word definition can be checked here.

Purpose

Rust core library provides some nice output primitives as methods of core::fmt::Formatter: debug_list, debug_struct and so on. Moreover, it also provides an amazing derive macro for core::fmt::Debug, which perfectly performs all the routine of implementing usage of mentioned primitives. Unfortunately, there no display-mode analogs of those primitives, and there are no derive macro for core::fmt::Display itself. The last fact is stated in the documentation right with the explanation: it is so to encourage developers to implement core::fmt::Display accordingly to the related type purpose. I agree with that approach, but I don't believe, however, that this explanation is enough to avoid implementing any suitable primitives at all, whereas existing primitives are too debug-oriented (for example, they always print type name, or always print None values even when it isn't actually needed in display mode). This library/crate/repo is a little attempt to fullfill mentiond gap: it provides some tiny primitves for discussed cases (see examples).

Examples

Lets consider the next structure:

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

With standard std::fmt::Debug implementation via derive macro it will have the next output for {:?} format:

Point { x: 0, y: 0 }

And the next output with prettified ({:#?}) format:

Point { 
    x: 0, 
    y: 0,
}

Continuing the example, one can describe the next structure with related outputs (accordingly):

#[derive(Debug)]
struct Line {
    a: Point,
    b: Point,
}
Line { a: Point { x: 0, y: 0 }, b: Point { x: 1, y: 1 } }
Line { 
    a: Point { 
        x: 0, 
        y: 0, 
    }, 
    b: Point { 
        x: 1, 
        y: 1 ,
    } 
}

The same output will be reproduced if one implements it for display mode (actually, this is the way it is implemented for debug mode "inside" the derive macro):

impl Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.debug_struct("Point")
            .field("x", &self.x)
            .field("y", &self.y)
            .finish()
    }
}

impl Display for Line {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.debug_struct("Line")
            .field("a", &self.a)
            .field("b", &self.b)
            .finish()
    }
}

Such output in display mode can be too detailed (I personally found type information quite annoying). One can do slightly better using some tricks with std::fmt::Formatter methods:

impl Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.debug_map()
            .entry(&"x", &self.x)
            .entry(&"y", &self.y)
            .finish()
    }
}

impl Display for Line {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.debug_map()
            .entry(&"a", &self.a)
            .entry(&"b", &self.b)
            .finish()
    }
}

Here is the result (prettified to simplify reading):

{
    "a": Point {
        x: 0,
        y: 0,
    },
    "b": Point {
        x: 1,
        y: 1,
    },
}

Type name for Line has been removed, but stayed for Point - because due to call of debug_map (debug_ !) it uses debug mode for outputting all entries, and provided std::fmt::Display implementation isn't even used. Moreover, this approach puts " symbols around field names as they are treated like map keys, which is fair, but seems redundant in considered case. The next attempt will deal with both mentioned effect:

use std::format_args;

impl Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.debug_set()
            .entry(&format_args!("x: {:#}", self.x))
            .entry(&format_args!("y: {:#}", self.y))
            .finish()
    }
}

impl Display for Line {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        f.debug_set()
            .entry(&format_args!("a: {:#}", self.a))
            .entry(&format_args!("b: {:#}", self.b))
            .finish()
    }
}

This approach reaches our goal for prettified format:

{
    a: {
        x: 0,
        y: 0,
    },
    b: {
        x: 1,
        y: 1,
    },
}

But it has a problem for non-prettified one:

{a: {
    x: 0,
    y: 0,
}, b: {
    x: 1,
    y: 1,
}}

The reason is in format strings like "a: {:#}". It doesn't matter for Point due to scalar values of its fields, but it strikes back for Line, because its fields are structs themselves: they are always outputted prettified, because related format string specify that no matter what - even if the Line examplar itself is being outputted as non-prettified. To avoid those problems one should inject some variation in the output code:

impl Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        if f.alternate() {
            f.debug_set()
                .entry(&format_args!("x: {:#}", self.x))
                .entry(&format_args!("y: {:#}", self.y))
                .finish()
        } else {
            f.debug_set()
                .entry(&format_args!("x: {}", self.x))
                .entry(&format_args!("y: {}", self.y))
                .finish()
        }
    }
}

impl Display for Line {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        if f.alternate() {
            f.debug_set()
                .entry(&format_args!("a: {:#}", self.a))
                .entry(&format_args!("b: {:#}", self.b))
                .finish()
        } else {
            f.debug_set()
                .entry(&format_args!("a: {}", self.a))
                .entry(&format_args!("b: {}", self.b))
                .finish()
        }
    }
}

The result is:

Non-prettified: {a: {x: 0, y: 0}, b: {x: 1, y: 1}}
Prettified: {
    a: {
        x: 0,
        y: 0,
    },
    b: {
        x: 1,
        y: 1,
    },
}

It works! But the code became quite messy, Its no big deal for small structs and programs, but becomes one when a program have a lot of structs with a lot of fields. So here is a Cubob solution: some abstractions which help to achieve the same goals with simplier actions:

use cubob::display_struct;

impl Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        display_struct(
            f,
            &[
                (&"x", &self.x),
                (&"y", &self.y),
            ],
        )
    }
}

impl Display for Line {
    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
        display_struct(
            f,
            &[
                (&"a", &self.x),
                (&"b", &self.y),
            ],
        )
    }
}

This code produces the same behaviour as the previous, but lets to keep code simplier and clearer.

No runtime deps