format-bytes 0.4.0

A macro to format bytestrings
Documentation
# Format-bytes

This crate exposes procedural macros that allow you to format bytestrings.
For more background on why you would want to do that,
[read this article](https://octobus.net/blog/2020-06-05-not-everything-is-utf8.html).

## Usage

Add this to your `Cargo.toml`:

```toml
[dependencies]
format-bytes = "0.4"
```

Requires Rust 1.85 or later. See the [changelog](https://foss.heptapod.net/octobus/rust/format-bytes/-/blob/branch/default/CHANGELOG.md).

Then use the macro like so:

```rust
use format_bytes::format_bytes;

fn main() {
    assert_eq!(
        format_bytes!(b"look at those {} bytes", &[0u8, 1, 2]),
        b"look at those \x00\x01\x02 bytes"
    );

    let path = b"caf\xe9";
    assert_eq!(
        format_bytes!("{{{path}}} is {len} bytes long", len = path.len()),
        b"{caf\xe9} is 4 bytes long"
    );
}
```

See more examples of how it works on the documentation of
[`format_bytes!` itself][`format_bytes!`].

Arguments can be of any type that implements [`DisplayBytes`]: bytestrings,
integers and floats, [`Utf8`] for [`std::fmt::Display`] types, [`join()`] for
sequences, and references and smart pointers to those.

### Writing to a stream: `write_bytes!`

[`write_bytes!`] takes a `&mut` to the stream to write to, followed by the same
arguments as [`format_bytes!`]:

```rust
use format_bytes::write_bytes;
use std::io;

fn write_entry(out: &mut dyn io::Write, path: &[u8]) -> io::Result<()> {
    write_bytes!(out, b"{}: {} bytes\n", path, path.len())
}

fn main() {
    let mut out = vec![];
    write_entry(&mut out, b"caf\xe9").unwrap();
    assert_eq!(out, b"caf\xe9: 4 bytes\n");
}
```

The stream can be any [`std::io::Write`], or any [`Sink`].

### Formatting later: `message!`

[`message!`] takes the same arguments as [`format_bytes!`], but returns a
[`Message`] that keeps the format string and the arguments, to be written
later, for example by an error handler. Arguments are moved into the message,
so they must be `'static`, [`Send`] and [`Sync`].

```rust
use format_bytes::{Message, format_bytes, message};

fn unknown_revision(rev: u32) -> Message {
    message!("unknown revision {rev}")
}

fn main() {
    let msg = unknown_revision(42);
    assert_eq!(format_bytes!("abort: {msg}"), b"abort: unknown revision 42");
    assert_eq!(msg.template().source, "unknown revision {rev}");
}
```

A [`Message`] is itself a [`DisplayBytes`] argument. [`Message::write_to`]
writes it to a [`Sink`], and [`Message::template`] gives the format string,
parsed at compile time.

### Custom output: `Sink`

A [`Sink`] receives the format string separately from each argument and its
name ([`ArgName`]). Every [`std::io::Write`] is a [`Sink`] that writes both
as-is. Other sinks can, for example, tell user data apart from the message
around it:

```rust
use format_bytes::{ArgName, DisplayBytes, Sink, write_bytes};
use std::io;

/// Quotes arguments
struct Quoting(Vec<u8>);

impl Sink for Quoting {
    fn literal(&mut self, text: &str) -> io::Result<()> {
        self.0.literal(text)
    }

    fn arg(&mut self, _: ArgName, value: &dyn DisplayBytes) -> io::Result<()> {
        write_bytes!(&mut self.0, "'{}'", value)
    }
}

fn main() {
    let mut sink = Quoting(vec![]);
    write_bytes!(&mut sink, "cannot open {path}", path = b"caf\xe9").unwrap();
    assert_eq!(sink.0, b"cannot open 'caf\xe9'");
}
```

A sink that cares about types for structured output can call
[`DisplayBytes::visit`] with a [`Visit`], which implements methods for
integers, UTF-8 text, bytes, a [`join()`] sequence or a nested [`Message`].

## Missing features

* [`format!`]-like explicit indices (`{0}`) and format specs (`{:?}`, padding):
if the need manifests itself, they might appear
* ``#[no_std]`` support


## Why not 1.0?

Not until the macro gets a bit of mileage (it
will be used in [Mercurial](https://www.mercurial-scm.org)).

[`ArgName`]: https://docs.rs/format-bytes/latest/format_bytes/enum.ArgName.html
[`DisplayBytes`]: https://docs.rs/format-bytes/latest/format_bytes/trait.DisplayBytes.html
[`DisplayBytes::visit`]: https://docs.rs/format-bytes/latest/format_bytes/trait.DisplayBytes.html#method.visit
[`Message`]: https://docs.rs/format-bytes/latest/format_bytes/struct.Message.html
[`Message::template`]: https://docs.rs/format-bytes/latest/format_bytes/struct.Message.html#method.template
[`Message::write_to`]: https://docs.rs/format-bytes/latest/format_bytes/struct.Message.html#method.write_to
[`Send`]: https://doc.rust-lang.org/std/marker/trait.Send.html
[`Sink`]: https://docs.rs/format-bytes/latest/format_bytes/trait.Sink.html
[`Sync`]: https://doc.rust-lang.org/std/marker/trait.Sync.html
[`Utf8`]: https://docs.rs/format-bytes/latest/format_bytes/struct.Utf8.html
[`Visit`]: https://docs.rs/format-bytes/latest/format_bytes/trait.Visit.html
[`format!`]: https://doc.rust-lang.org/std/macro.format.html
[`format_bytes!`]: https://docs.rs/format-bytes/latest/format_bytes/macro.format_bytes.html
[`join()`]: https://docs.rs/format-bytes/latest/format_bytes/fn.join.html
[`message!`]: https://docs.rs/format-bytes/latest/format_bytes/macro.message.html
[`std::fmt::Display`]: https://doc.rust-lang.org/std/fmt/trait.Display.html
[`std::io::Write`]: https://doc.rust-lang.org/std/io/trait.Write.html
[`write_bytes!`]: https://docs.rs/format-bytes/latest/format_bytes/macro.write_bytes.html