Introduction
hooq is an attribute macro that can (when desired) insert methods in front of the ? operator1, return, and tail expressions.
use hooq::hooq;
#[hooq]
#[hooq::method(.map(|v| v * 2))]
fn double(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
let res = s.parse::<u32>()?;
Ok(res)
}
fn double_expanded(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
let res = s.parse::<u32>().map(|v| v * 2)?;
Ok(res)
}
#[test]
fn test() {
assert_eq!(double("21").unwrap(), double_expanded("21").unwrap());
}
fn main() {
println!("double_hooked: {}", double("21").unwrap());
println!("double: {}", double_expanded("21").unwrap());
}
It ships with rich presets so you can easily hook error logging and similar behavior onto Result values.
Why use hooq?
We will motivate hooq using the following source code containing a function that returns a Result type:
use std::error::Error;
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
// Loading APP_HOST
let host = std::env::var("APP_HOST")?;
// Loading APP_PORT
let port = std::env::var("APP_PORT")?;
// Convert to u16
let port: u16 = port.parse()?;
Ok(format!("{host}:{port}"))
}
fn main() -> Result<(), Box<dyn Error>> {
let host_and_port = load_host_and_port()?;
println!("Server is running on: {}", host_and_port);
// snip
Ok(())
}
#[test]
fn test_load_host_and_port() {
unsafe {
std::env::set_var("APP_HOST", "localhost");
std::env::set_var("APP_PORT", "8080");
}
let host_and_port = load_host_and_port().unwrap();
assert_eq!(host_and_port, "localhost:8080");
}
If you run it with all required environment variables set, it works without errors:
$ APP_HOST=127.0.0.1 APP_PORT=10 cargo run -q
Server is running on: 127.0.0.1:10
Now run it in a way that triggers an error:
$ APP_PORT=10 cargo run -q
Error: NotPresent
The contents of the Box<dyn Error> returned from main are printed; we (probably) have a missing environment variable.
But this error output is terrible:
- You cannot tell the context or what kind of error it is
- You cannot tell where the error occurred
Most likely the author of this Rust program did not want to build a formal application with fine‑grained error handling, but just a small casual CLI tool. Rust’s error output, however, can be rather unforgiving if you cut corners2.
Enter the hooq attribute macro!
use std::error::Error;
use hooq::hooq;
#[hooq]
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
// Loading APP_HOST
let host = std::env::var("APP_HOST")?;
// Loading APP_PORT
let port = std::env::var("APP_PORT")?;
// Convert to u16
let port: u16 = port.parse()?;
Ok(format!("{host}:{port}"))
}
#[hooq]
fn main() -> Result<(), Box<dyn Error>> {
let host_and_port = load_host_and_port()?;
println!("Server is running on: {}", host_and_port);
// snip
Ok(())
}
#[test]
fn test_load_host_and_port() {
unsafe {
std::env::set_var("APP_HOST", "localhost");
std::env::set_var("APP_PORT", "8080");
}
let host_and_port = load_host_and_port().unwrap();
assert_eq!(host_and_port, "localhost:8080");
}
Simply adding #[hooq] magically produces a pseudo stack trace for errors:
$ APP_PORT=10 cargo run -q
[mdbook-source-code/index/src/main.rs:8:41] NotPresent
8> std::env::var("APP_HOST")?
|
[mdbook-source-code/index/src/main.rs:21:45] NotPresent
21> load_host_and_port()?
|
Error: NotPresent
Apparently the APP_HOST environment variable was missing. We can also see how the error propagated (line 8, then line 21, etc.).
What exactly did the hooq macro do? That will be explained later.