4 releases
new 0.1.3 | May 22, 2025 |
---|---|
0.1.2 | May 22, 2025 |
0.1.1 | May 21, 2025 |
0.1.0 | May 19, 2025 |
#353 in Procedural macros
86 downloads per month
29KB
397 lines
formati
Enhanced Rust formatting macros with dotted notation and expression interpolation
formati
is a collection of procedural macros that extend Rust's standard formatting facilities to handle dotted notation:
formati::format!("User ID: {user.id} Name: {user.display_name()}");
Features
- Dotted notation: Access fields and elements with natural dot notation
- Expression evaluation: Run arbitrary expressions
- Argument deduplication: Simplifies repeated evaluation of the same arguments
- Fully backwards compatible: Works with all standard format specifiers (
{:?}
,{:.2}
, etc.) - Integration wrappers: Drop-in replacements for
std::io
(e.g.println!
), anyhow, tracing and log.
Installation
Add formati
to your Cargo.toml
:
[dependencies]
formati = "0.1"
Usage
Basic Formatting
use formati::format;
struct User {
id: u32,
role: String,
}
impl User {
fn display_name(&self) -> String {
format!("USER-{self.id}")
}
}
fn main() {
let coordinates = (42.5, 73.1);
let user = User { id: 101, role: String::from("Admin") };
let s = format!(
"Position: ({coordinates.0}, {coordinates.1})\n\
User: {user.display_name()} (ID: {user.id}, Role: {user.role})",
);
assert_eq!(
s,
"Position: (42.5, 73.1)\nUser: USER-101 (ID: 101, Role: Admin)",
);
}
Format Specifiers
use formati::format;
fn main() {
let coords = (10.12345, 20.67890);
let s = format!("Location: ({coords.0:.2}, {coords.1:.2})");
assert_eq!(
s,
"Location: (10.12, 20.68)",
);
}
print!
/ println!
Requires stdio
feature:
[dependencies]
formati = { version = "0.1", features = ["stdio"] }
use formati::{print, println};
fn main() {
let point = (5, 10);
print!("Starting at ({point.0}, {point.1})..."); // prints "Starting at (5, 10)..."
println!("Coordinates: ({point.0}, {point.1})"); // prints "Coordinates: (5, 10)\n"
}
Integration Wrappers
Anyhow
Requires anyhow
feature:
[dependencies]
formati = { version = "0.1", features = ["anyhow"] }
formati
-style versions of anyhow
macros:
use formati::{anyhow, bail};
#[derive(Debug)]
struct User {
id: u32,
name: String,
}
fn process(user: &User) -> anyhow::Result<()> {
if user.id == 0 {
bail!("Cannot process zero-id user {user.name} (ID {user.id})");
}
Ok(())
}
fn main() {
let user = User { id: 0, name: "Bob".into() };
if let Err(e) = process(&user) {
// Produces: "Cannot process zero-id user Bob (ID 0)"
eprintln!("{e}");
}
// Build an error directly
let err = anyhow!("Unexpected error for {user.name} with id {user.id}");
assert_eq!(err.to_string(), "Unexpected error for Bob with id 0");
}
Log
Requires log
feature:
(NOTE: the log
feature cannot be enabled together with tracing
)
[dependencies]
formati = { version = "0.1", features = ["log"] }
formati
-style versions of log
macros:
use formati::{debug, error, info, trace, warn}; // instead of log::{…}
use log::LevelFilter;
fn main() {
simple_logger::init_with_level(LevelFilter::Trace).unwrap();
let user = ("Alice", 42);
trace!("Starting auth flow for {user.0} ({user.1})…");
debug!("Loaded profile for {user.0}");
info!("User {user.0} logged in with ID {user.1}");
warn!("Suspicious activity detected for ID {user.1}");
error!("Failed to handle request for user {user.0}");
}
Tracing
Requires tracing
feature:
(NOTE: the tracing
feature cannot be enabled together with log
)
[dependencies]
formati = { version = "0.1", features = ["tracing"] }
formati
-style versions of tracing
macros:
use formati::{debug, error, info, trace}; // use in place of tracing::{debug, error, info, trace}
use tracing_subscriber::FmtSubscriber;
fn main() {
tracing::subscriber::set_global_default(
FmtSubscriber::builder().finish()
).unwrap();
let user = (String::from("Alice"), 101);
trace!(target: "auth", "Authenticating")
debug!(user_type = "admin", "Processing request for {user.0}");
info!("User {user.0} logged in with ID {user.1}");
warn!(data = (13, 37), "Bad data from ID {user.1}")
error!("Failed to process request for ID = {user.1}");
}
How It Works
The macros processes format strings at compile time to:
- Find placeholders with dotted notation (
{example.field}
) - Extract these expressions and deduplicate them
- Replace them with indexed placeholders
- Add the extracted expressions as arguments to the underlying format macro
This approach avoids evaluating the same expression multiple times and makes your format strings more readable.
Backwards compatibility
The macros are all backwards compatible and can be used as a drop-in replacement.
struct Point {
x: f32,
y: f32,
}
let point = Point { x: 3.0, y: 4.0 };
let info = format!(
"Point: ({point.x}, {point.y}), X-coord: {point.x}, Y-coord: {point.y}, ({},{})",
point.x,
point.y,
);
The format!
macro would expand to:
alloc::__export::must_use({
let res = alloc::fmt::format(alloc::__export::format_args!(
"Point: ({0}, {1}), X-coord: {0}, Y-coord: {1}, ({0}, {1})",
point.x,
point.y
));
res
})
Tests
Test formati::format!
macro:
cargo test
Test stdio
integration:
cargo test-stdio
Test anyhow
integration:
cargo test-anyhow
Test log
integration:
cargo test-log
Test tracing
integration:
cargo test-tracing
License
This project is licensed under the MIT License.
Dependencies
~195–620KB
~15K SLoC