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.
-
The former
trymacro. ↩ -
“Couldn’t we just use
unwrapat this scale?” I hate perceptive brats like you… Even so, not being able to useResultcomfortably would be inconvenient, so the hooq macro remains meaningful. ↩
Documentation Links
A collection of documentation links related to hooq.
- hooq Reference (this document)
- hooq Reference (日本語版) (this Japanese edition translated)
- GitHub
- docs.rs
- crates.io
Tutorial
This chapter introduces the basics of using hooq across three lessons plus a wrap‑up page.
#[hooq]to capture error lines- Customize hooks with
#[hooq::method(...)] - Create / use presets via flavors
- Summary
We assume the hooq crate has already been added.
Adding hooq
Add via cargo:
cargo add hooq
Or specify the latest version in Cargo.toml (see crates.io).
Some features exist, but commonly used ones are in the default feature set. See Features.
#[hooq] to Capture Error Lines
We continue with the example from Introduction to explain what the hooq macro does to source code and how to use it.
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");
}
Adding the #[hooq] attribute macro to a function inserts the following default hooq method inspect_err in front of each ?, and after the value of each return and tail expression (only when the function’s return type matches a hook target such as Result).
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
Items like $path are hooq meta variables. See Meta Variables for full details; those used in the default method are:
| Meta | Literal Kind | Description |
|---|---|---|
$path | string | Relative path from crate root to the file |
$line | integer | Line where the hook is inserted |
$col | integer | Column where the hook is inserted |
$source | tokens of target expression | The original expression being hooked (for logging) |
The function load_host_and_port expands roughly like this (formatting may differ). Using cargo expand will show similar output:
---
source: mdbook-source-code/index/tests/test.rs
expression: "format!(\"{stdout}\")"
---
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use std::error::Error;
use hooq::hooq;
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
let host = std::env::var("APP_HOST")
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 8usize;
let col = 41usize;
let expr = " 8> std::env::var(\"APP_HOST\")?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
let port = std::env::var("APP_PORT")
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 11usize;
let col = 41usize;
let expr = " 11> std::env::var(\"APP_PORT\")?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
let port: u16 = port
.parse()
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 14usize;
let col = 33usize;
let expr = " 14> port.parse()?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
Ok(format!("{0}:{1}", host, port))
}
fn main() -> Result<(), Box<dyn Error>> {
let host_and_port = load_host_and_port()
.inspect_err(|e| {
let path = "mdbook-source-code/index/src/main.rs";
let line = 21usize;
let col = 45usize;
let expr = " 21> load_host_and_port()?\n |";
::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
})?;
{
::std::io::_print(format_args!("Server is running on: {0}\n", host_and_port));
};
Ok(())
}
Warning: line!() macro is discouraged here.
You might think: “Why not just use line!()? Adding a $line meta variable increases cognitive load.”
This is intentional: line!() does not point to the line of the hooked expression; it points to the attribute line (#[hooq] or #[hooq::method(...)]).
We want the exact line of the ? or other target, hence $line exists.
Thanks to this expansion, running the program as shown in Introduction yields output identifying the precise lines where errors occurred:
$ 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
The default method is helpful, but you will likely want to customize it. We cover customization in the next lesson.
Customize Hooks with #[hooq::method(...)]
You can configure which method is hooked.
We will show this using an example program that prints the name field from the project’s Cargo.toml. We add the toml crate first:
cargo add toml
Basic routine to parse a provided path as TOML:
use hooq::hooq;
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let _cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
// snip
Ok(())
}
Instead of applying hooq as‑is, we will customize the hook. We also want success logging via inspect.
Use #[hooq::method(...)] to specify methods to insert. The meta variable .$so_far represents the chain built so far, letting us extend it with additional methods.
use hooq::hooq;
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let _cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
// snip
Ok(())
}
Failure still logs as before:
[mdbook-source-code/tutorial-1/src/main.rs:10:81] Os { code: 2, kind: NotFound, message: "No such file or directory" }
10> std::fs::read_to_string(path)?
|
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
Success now logs too:
Success: `std :: fs :: read_to_string(path) ?` @ Line 10: Col: 81
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 10: Col: 83
Next we extract the package.name field with toml::Value::get. Because it returns an Option, we convert to Result using ok_or_else.
use hooq::hooq;
fn display_name(val: &toml::Value) -> Result<(), String> {
let name = val
.get("package")
.ok_or_else(|| format!("get package [Line: {}]", line!()))?
.get("name")
.ok_or_else(|| format!("get name [Line: {}]", line!()))?
.as_str()
.ok_or_else(|| format!("as_str [Line: {}]", line!()))?;
println!("name: {name}");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name(&cargo_toml)?;
Ok(())
}
As you may guess: .ok_or_else(...) is boilerplate. hooq can reduce it:
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
println!("name: {name}");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name(&cargo_toml)?;
Ok(())
}
We also added $nth (alias of $count) to show which numbered ? caused failure. Running with an error:
Success: `std :: fs :: read_to_string(path) ?` @ Line 26: Col: 80
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 26: Col: 82
[mdbook-source-code/tutorial-3/src/main.rs:28:30] "val.get(\"package\") ? .get(\"name\") ? [Line: 12, 2nd ?]"
28> display_name(&cargo_toml)?
|
Error: "val.get(\"package\") ? .get(\"name\") ? [Line: 12, 2nd ?]"
The TOML had package but not name; the second ? failed as indicated.
Skipping Hooks with #[hooq::skip_all]
Suppose we add a validation: “4 is ominous (like 4041), so reject names containing 4.”
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
hooq also tries to hook return Err(...); but Result lacks ok_or_else, causing a compile error2:
error[E0599]: no method named `ok_or_else` found for enum `Result<T, E>` in the current scope
--> mdbook-source-code/tutorial-4-compile-error/src/main.rs:15:9
|
15 | return Err(format!(
| ^^^^^^
|
help: there is a method `or_else` with a similar name
|
15 - return Err(format!(
15 + or_else Err(format!(
|
For more information about this error, try `rustc --explain E0599`.
error: could not compile `tutorial-4-compile-error` (bin "tutorial-4-compile-error") due to 1 previous error
We do not want a hook here. Add #[hooq::skip_all] to skip inside that scope:
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
#[hooq::skip_all]
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
Complete program
use hooq::hooq;
#[hooq]
#[hooq::method(.ok_or_else(|| {
format!("{} [Line: {}, {}]",
stringify!($source),
$line,
$nth
)
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
let name = val.get("package")?.get("name")?.as_str()?;
#[hooq::skip_all]
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
Compilation succeeds and names containing 4 produce an error:
Success: `std :: fs :: read_to_string(path) ?` @ Line 33: Col: 80
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 33: Col: 82
[mdbook-source-code/tutorial-4/src/main.rs:35:39] "name `tutorial-4` contains '4'. Guido Mista disallow this."
35> display_name_by_mista(&cargo_toml)?
|
Error: "name `tutorial-4` contains '4'. Guido Mista disallow this."
Besides #[hooq::skip_all], multiple attributes exist to adjust behavior mid‑function (including attaching #[hooq::method(...)] to specific expressions). See Attributes.
Custom methods everywhere may get tedious. The next lesson introduces flavor presets.
-
Generally, the number
4is considered an unlucky number, but for server-side engineers,5may be even more unlucky. ↩ -
I apologies for the broken compilation error messages, I’ll fix them someday. ↩
Create / Use Presets via Flavors
At the end of the previous lesson we saw ok_or_else missing on Result when trying to hook uniformly. Can we hook something available on both Result and Option? The Context::with_context method from anyhow fits perfectly: on Option it converts None into an anyhow::Result::Err.
use anyhow::{Context, Result};
use hooq::hooq;
#[hooq]
#[hooq::method(.with_context(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
format!("[{path}:{line}:{col}]\n{expr}")
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<()> {
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(anyhow::anyhow!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq]
#[hooq::method(.with_context(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
format!("[{path}:{line}:{col}]\n{expr}")
}))]
fn main() -> Result<()> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
Run to produce an error: the extra .with_context(...) calls accumulate, giving a trace.
Error: [mdbook-source-code/tutorial-4-with-anyhow/src/main.rs:41:39]
41> display_name_by_mista(&cargo_toml)?
|
Caused by:
0: [mdbook-source-code/tutorial-4-with-anyhow/src/main.rs:17:9]
17> return Err(anyhow::anyhow!(
18| "name `{name}` contains '4'. Guido Mista disallow this."
19| ))
|
1: name `tutorial-4-with-anyhow` contains '4'. Guido Mista disallow this.
Because this is a frequent pattern hooq provides a preset—the anyhow flavor. Presets are called “flavors” in hooq. There are also flavors for log, eyre, and tracing.
Change #[hooq] to #[hooq(anyhow)] to enable the flavor:
use anyhow::Result;
use hooq::hooq;
#[hooq(anyhow)]
fn display_name_by_mista(val: &toml::Value) -> Result<()> {
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(anyhow::anyhow!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq(anyhow)]
fn main() -> Result<()> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
Output matches the manual version.
See Flavors for all built‑in presets.
Define Custom Flavors
You can define flavors in a hooq.toml placed at the crate root (CARGO_MANIFEST_DIR). Example:
[my_flavor]
method = """.inspect_err(|_| {
eprintln!("Error @ Line {}: Col: {}\n{}", $line, $col, ::hooq::summary!($source));
})
.inspect(|_| {
println!("Success @ Line {}: Col: {}\n{}", $line, $col, ::hooq::summary!($source));
})"""
hook_targets = ["?", "return", "tail_expr"]
tail_expr_idents = ["Ok", "Err"]
result_types = ["Result"]
hook_in_macros = true
[my_flavor.ok_or_else]
method = """
.ok_or_else(|| {
format!("[Line: {}, {}]\n{}",
$line,
$nth,
::hooq::summary!($source),
)
})
.$so_far"""
Meaning of the keys:
| Key | Effect |
|---|---|
method | Method to hook |
hook_targets | Which of ?, return, tail_expr to hook |
tail_expr_idents | Idents (e.g. Err) that force hooking on tail / return |
result_types | Return type idents (e.g. Result) whose tail/return values are considered for hooks |
my_flavor.ok_or_else is a sub‑flavor, inheriting settings and overriding a subset.
For all available fields and precedence rules see Attributes and Flavors.
Use the flavor in code:
use hooq::hooq;
#[hooq(my_flavor)]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
// Method can be overridden by the one in flavor!
#[hooq::method = my_flavor::ok_or_else]
let name = val.get("package")?.get("name")?.as_str()?;
if name.contains("4") {
return Err(format!(
"name `{name}` contains '4'. Guido Mista disallow this."
));
}
println!("Mista「name: {name}」");
Ok(())
}
#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());
let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;
display_name_by_mista(&cargo_toml)?;
Ok(())
}
#[hooq::method = flavor_name] performs partial application of a flavor’s settings. Other fields also support this via #[hooq::field = flavor_name]. See Attributes (partial application).
Subtle note: $so_far represents the chain after initial flavor application; you cannot reference $so_far inside the base flavor method itself. Thus #[hooq(my_flavor::ok_or_else)] cannot include .$so_far, while #[hooq::method = my_flavor::ok_or_else] can.
Summary
Across three lessons we introduced core hooq usage.
There are further mechanisms and details not covered in the tutorial; consult the individual reference pages for deeper exploration.
- Reference: Detailed explanations of each feature.
- Recipes & Ideas: Usage examples of built‑in flavors, exploratory scenarios, and hidden tricks.
Comparison With Other Approaches
Applying #[hooq] (or #[hooq(anyhow)]) to every function yields a pseudo stack trace for errors. Here is a comparison with other strategies for obtaining stack‑like traces:
Backtrace | tracing | hooq | |
|---|---|---|---|
| Learning cost / flexibility | ⚠️ | ⚠️ | 🌈 |
| Ease of type definitions | ⚠️ | ✅ | ✅ |
| Macro‑less | 🌈 | ❌ | ❌ |
| Information volume control | ⚠️ | ✅ | 🌈 |
| Platform support | ⚠️ | ✅ | 🌈 |
Legend:
- 🌈: Excellent
- ✅: Good
- ⚠️: So‑so
- ❌: Poor
Explanation:
- Learning cost / flexibility
- ⚠️
Backtracerequires settingRUST_LIB_BACKTRACE=1and relies on OS thread info; extra system knowledge helps. - ⚠️
tracingis powerful but may be overkill if you only want a stack trace analogue. - 🌈
hooqrequires only adding an attribute on functions.
- ⚠️
- Ease of type definitions
- ⚠️ With
thiserror+Backtrace, you must pre‑plan fields; retrofitting is harder as error types multiply. - ✅
tracingimposes no such constraints. - ✅
hooqcooperates with arbitrary error crates.
- ⚠️ With
- Macro‑less
- 🌈
Backtraceneeds no macros. - ❌
tracinggenerally needs#[tracing::instrument]for effortless spans. - ❌
hooqis an attribute macro by design.
- 🌈
- Information volume control
- ⚠️ Raw
Backtraceoutput is often too verbose (and weak for async); pairing withcolor-eyrehelps. - ✅
tracinggives structured spans; precise line of each?still requires manual logging. - 🌈
hooqpinpoints only annotated functions and the exact?/return/ tail expression locations.- Conditional usage via
#[cfg_attr(..., hooq(...))]enables feature/test scoped tracing. - 💡 Combine with
tracingto augment granularity—see tracing flavor.
- Conditional usage via
- ⚠️ Raw
- Platform support
- ⚠️
Backtracehas platform caveats (see official docs ). - ✅ Standard logging with
tracingis widely portable. - 🌈 hooq merely inserts methods; no platform primitives required.
- 💡
#[hooq::method(.unwrap()!)]can alias?to.unwrap()behind a feature.
- 💡
- ⚠️
Reference
This section covers details not addressed in the tutorial.
Contents
| Page | Summary |
|---|---|
| Attributes | Explains #[hooq(...)] and inert attributes like #[hooq::attr(...)]. Mainly controls macro behavior. |
| Method | How hooked methods are specified and inserted/replaced. |
| Meta Variables | Variables available inside hooked methods for logging/debugging. |
| Flavors | Preset configurations that bundle common settings; both built‑in and user‑defined. |
| Features | Cargo features provided by the hooq crate. |
Attributes
The behavior of the hooq macro is controlled by the root meta on #[hooq(...)] and by inert attributes inserted afterwards (#[hooq::attribute(...)]). This page explains configurable items and defaults.
Quick Reference
| Name | Kind | Description |
|---|---|---|
| flavor | root meta | Apply settings from a named flavor. |
| trait_use | root meta | Insert use XXX as _; before the item for the given trait path(s). |
| method | inert | Configure the method to insert/replace. |
| skip_all / skip | inert | Skip hooking the annotated expression; skip_all also skips inside it. |
| hook_targets | inert | Enable hooking on ?, return, and/or tail_expr (default: all). |
| tail_expr_idents | inert | Idents to hook when appearing as tail/return values (default: Err). |
| ignore_tail_expr_idents | inert | Idents to avoid hooking even if otherwise eligible (default: Ok). |
| result_types | inert | Function return type idents considered hook targets for return/tail (default: Result). |
| hook_in_macros | inert | Whether to hook inside macro invocations (default: true). |
| binding | inert | Define custom meta variable bindings (expression or literal). |
- Root meta: attributes written inside
#[hooq(...)]. - Inert attributes:
#[hooq::attribute(...)]placed in various positions after the macro.
Example with all attributes:
use hooq::hooq;
mod sub {
pub trait Trait {}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(flavor = "hook", trait_use(sub::Trait))] // Attribute macro root.
#[hooq::method(.inspect_err(|_| { let _ = "error!"; }))] // All following attributes are inert.
#[hooq::hook_targets("?", "return", "tail_expr")]
#[hooq::tail_expr_idents("Err")]
#[hooq::ignore_tail_expr_idents("Ok")]
#[hooq::result_types("Result")]
#[hooq::hook_in_macros(true)]
#[hooq::binding(xxx = "xxx_value")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::skip_all]
if failable(false)? {
failable(())?;
}
#[hooq::skip]
if failable(false)? {
// Next line is not skipped.
failable(())?;
}
#[hooq::method(.inspect_err(|_| { let _ = $xxx; }))]
failable(())?;
Ok(())
}
Expansion:
use hooq::hooq;
mod sub {
pub trait Trait {}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(unused)]
use sub::Trait as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "error!";
})?;
if failable(false)? {
failable(())?;
}
if failable(false)? {
failable(())
.inspect_err(|_| {
let _ = "error!";
})?;
}
failable(())
.inspect_err(|_| {
let _ = "xxx_value";
})?;
Ok(())
}
Root Meta
Two fields are available inside #[hooq(...)]:
| Name | Syntax |
|---|---|
| flavor | #[hooq(FLAVOR_NAME)] or #[hooq(flavor = "FLAVOR_NAME")] |
| trait_use | #[hooq(trait_use(PATH, ...))] or #[hooq(trait_uses(PATH, ...))] |
flavor
Apply base settings from a flavor:
use hooq::hooq;
#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
func()?;
Ok(())
}
#[hooq(my_flavor::sub_flavor)]
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(flavor = "my_flavor.sub_flavor")]
fn func() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
hooq.toml example:
[my_flavor]
method = """.inspect(|_| {
let _ = $tag;
})"""
bindings = { tag = "\"my_flavor\"" }
[my_flavor.sub_flavor]
bindings = { tag = "\"my_flavor.sub_flavor\"" }
tail_expr_idents = ["Ok"]
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
func()
.inspect(|_| {
let _ = "my_flavor";
})?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
.inspect(|_| {
let _ = "my_flavor.sub_flavor";
})
}
fn func() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect(|_| {
let _ = "my_flavor.sub_flavor";
})?;
Ok(())
.inspect(|_| {
let _ = "my_flavor.sub_flavor";
})
}
Sub‑flavors inherit and override parent flavor settings. Use dotted or path‑like names (e.g. base.sub or base::sub), and they can nest.
Further info, especially built‑ins like anyhow, writing hooq.toml, etc.: see Flavors.
trait_use
Insert #[allow(unused)] use PATH as _; above the item:
use hooq::hooq;
mod sub {
pub trait Inserted {
fn inserted(self) -> Self;
}
impl<T, E> Inserted for Result<T, E> {
fn inserted(self) -> Self {
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(trait_use(sub::Inserted))]
#[hooq::method(.inserted())]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion excerpt:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod sub {
pub trait Inserted {
fn inserted(self) -> Self;
}
impl<T, E> Inserted for Result<T, E> {
fn inserted(self) -> Self {
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(unused)]
use sub::Inserted as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(()).inserted()?;
Ok(())
}
Useful when the hooked method requires a trait (e.g. anyhow::Context for .with_context(...)). You can also configure trait_uses in hooq.toml so a flavor brings the necessary imports.
method
| Name | Syntax |
|---|---|
| method | #[hooq::method(...)] |
Set the method via inert attribute. Works both immediately under #[hooq] and inside the function.
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| { let _ = "specified @ root"; }))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::method(.inspect_err(|_| { let _ = "specified @ inner"; }))]
failable(())?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "specified @ root";
})?;
failable(())
.inspect_err(|_| {
let _ = "specified @ inner";
})?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
Two modes exist:
- Insertion mode: starts with
.; inserts between the expression and?(or at the end forreturn/tail). - Replacement mode: otherwise; replace the target using an expression. Use
$exprto access the original (already recursively hooked) expression.
Meta variables like $line, $source, $fn_name are available. Default flavor inserts:
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = ::hooq::summary!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
See Method and Meta Variables.
Skipping Hooks
| Name | Syntax |
|---|---|
| skip_all | #[hooq::skip_all] |
| skip | #[hooq::skip] |
skip_all
Prevent hooks inside the annotated expression:
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::skip_all]
let f = || -> Option<()> {
optional(())?; // If the hook is applied, an compile error occurs.
Some(())
};
let _ = failable(f())?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn optional<T>(val: T) -> Option<T> {
Some(val)
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(()).inspect_err(|_| {})?;
let f = || -> Option<()> {
optional(())?;
Some(())
};
let _ = failable(f()).inspect_err(|_| {})?;
Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn optional<T>(val: T) -> Option<T> {
Some(val)
}
skip
Skip only within the parent scope (children can still be hooked). Helpful when nested tail expressions would produce noisy logs.
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func1() -> Result<(), String> {
match failable(failable(()))? {
Ok(()) => Ok(()),
Err(s) => Err(s),
}
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func2() -> Result<(), String> {
#[hooq::skip]
match failable(failable(()))? {
Ok(()) => Ok(()),
Err(s) => Err(s),
} // Not hooked here.
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() {
let _ = func1();
let _ = func2();
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn func1() -> Result<(), String> {
match failable(failable(())).inspect_err(|_| {})? {
Ok(()) => Ok(()),
Err(s) => Err(s).inspect_err(|_| {}),
}
.inspect_err(|_| {})
}
fn func2() -> Result<(), String> {
match failable(failable(()))? {
Ok(()) => Ok(()),
Err(s) => Err(s).inspect_err(|_| {}),
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() {
let _ = func1();
let _ = func2();
}
Control Hook Targets
Inert attributes to tweak whether/how hooks apply.
| Name | Syntax | Values |
|---|---|---|
| hook_targets | #[hooq::hook_targets(...)] | Any of "?", "return", "tail_expr". |
| tail_expr_idents | #[hooq::tail_expr_idents(...)] | Idents like Err. |
| ignore_tail_expr_idents | #[hooq::ignore_tail_expr_idents(...)] | Idents like Ok. |
| result_types | #[hooq::result_types(...)] | Return type idents like Result. |
| hook_in_macros | #[hooq::hook_in_macros(...)] | true or false. |
Priority rules:
- If
skip_allis present, do not hook (forskip, skip only in the same scope excluding children). - If target is inside a macro call (like
println!(...)) andhook_in_macrosisfalse, do not hook. - For
?: hook if included inhook_targets. - For
return: hook if included inhook_targets, and either ident is intail_expr_identsor return type ident is inresult_typesand ident is not inignore_tail_expr_idents. - For tail: similar rules as
return.
hook_targets
You can specify whether to hook each of the following three types: the ? operator (Question Operator), return, and tail expressions (tail_expr). By default, all three types are hooked.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("?")]
fn target_question() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? {
Ok(())
} else {
Err("error".into())
}
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("return")]
fn target_return() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? {
Ok(())
} else {
Err("error".into())
}
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("tail_expr")]
fn target_tail_expr() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? {
Ok(())
} else {
Err("error".into())
}
}
fn main() {
let _ = target_question();
let _ = target_return();
let _ = target_tail_expr();
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn target_question() -> Result<(), String> {
failable(()).inspect_err(|_| {})?;
if failable(false).inspect_err(|_| {})? {
return Err("error".into());
}
if failable(true).inspect_err(|_| {})? { Ok(()) } else { Err("error".into()) }
}
fn target_return() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into()).inspect_err(|_| {});
}
if failable(true)? { Ok(()) } else { Err("error".into()) }
}
fn target_tail_expr() -> Result<(), String> {
failable(())?;
if failable(false)? {
return Err("error".into());
}
if failable(true)? { Ok(()) } else { Err("error".into()).inspect_err(|_| {}) }
.inspect_err(|_| {})
}
fn main() {
let _ = target_question();
let _ = target_return();
let _ = target_tail_expr();
}
tail_expr_idents
Idents to hook regardless of result_types. Default: Err.
Identifiers must be single idents (e.g., Zzz), not paths like xxx::yyy::Zzz.
Matching uses the last segment of a path (xxx::yyy::Zzz matches by Zzz).
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), String> {
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res)
};
failable(())
};
#[hooq::tail_expr_idents("Err", "failable")]
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res)
};
failable(()) // This will be hooked because of tail_expr_idents.
};
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), String> {
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res).inspect_err(|_| {})
};
failable(())
};
let _: Result<(), String> = {
let _: Result<(), String> = {
let res = "error".to_string();
Err(res).inspect_err(|_| {})
};
failable(()).inspect_err(|_| {})
};
Ok(())
}
ignore_tail_expr_idents
Avoid hooking these idents even when otherwise eligible. Default: Ok.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() {
let f = || -> Result<(), String> { failable(()) };
#[hooq::ignore_tail_expr_idents("failable")]
let g = || -> Result<(), String> { failable(()) };
#[hooq::tail_expr_idents("!failable")]
let h = || -> Result<(), String> { failable(()) };
f().unwrap();
g().unwrap();
h().unwrap();
}
As shown above, you can also achieve the same behavior without ignore_tail_expr_idents by prefixing an ident with ! in tail_expr_idents (e.g., !Ok).
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() {
let f = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
let g = || -> Result<(), String> { failable(()) };
let h = || -> Result<(), String> { failable(()) };
f().unwrap();
g().unwrap();
h().unwrap();
}
If the same ident appears in both lists, it will be hooked due to simple mechanics; prefer using !Ident via tail_expr_idents instead.
result_types
Function return type idents considered for return/tail hooks. Default: Result.
As with tail_expr_idents, identifiers must be single idents (e.g., Zzz), not full paths like xxx::yyy::Zzz.
Matching uses the final segment of a path (xxx::yyy::Zzz matches by Zzz).
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func1() -> MyResult {
let _ = || -> Result<(), String> { failable(()) };
failable(())
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::result_types("MyResult")]
fn func2() -> MyResult {
// No longer hooked.
let _ = || -> Result<(), String> { failable(()) };
failable(())
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::result_types("Result", "MyResult")]
fn func3() -> MyResult {
let _ = || -> Result<(), String> { failable(()) };
let _ = || {
// Not hooked because return type of the closure is unknown.
failable(())
};
failable(())
}
fn main() {
let _ = func1();
let _ = func2();
let _ = func3();
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
fn func1() -> MyResult {
let _ = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
failable(())
}
fn func2() -> MyResult {
let _ = || -> Result<(), String> { failable(()) };
failable(()).inspect_err(|_| {})
}
fn func3() -> MyResult {
let _ = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
let _ = || { failable(()) };
failable(()).inspect_err(|_| {})
}
fn main() {
let _ = func1();
let _ = func2();
let _ = func3();
}
hook_in_macros
Control hooking inside function‑like macros.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", failable("hello")?);
#[hooq::hook_in_macros(false)]
println!("{}", failable("world")?);
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
{
::std::io::_print(format_args!("{0}\n", failable("hello").inspect_err(|_| {})?));
};
{
::std::io::_print(format_args!("{0}\n", failable("world")?));
};
Ok(())
}
The arguments to function-like macros do not always follow standard Rust syntax, and hooking them incurs a small parsing cost. This inert attribute allows you to disable hooking inside macros. If you don’t need to hook inside macros, setting this to false may slightly reduce compile times.
binding
Define user meta variables (bindings). Several forms:
| Syntax | Note |
|---|---|
#[hooq::binding(xxx = ...)] | |
#[hooq::var(xxx = ...)] | |
#[hooq::xxx = ...] | xxx must not conflict with other inert attribute names. |
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| { let _ = $xxx; }))]
#[hooq::xxx = 10]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::binding(xxx = "in block")]
{
failable(())?;
#[hooq::var(xxx = 42)]
failable(())?;
failable(())?;
}
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = 10;
})?;
{
failable(())
.inspect_err(|_| {
let _ = "in block";
})?;
failable(())
.inspect_err(|_| {
let _ = 42;
})?;
failable(())
.inspect_err(|_| {
let _ = "in block";
})?;
}
failable(())
.inspect_err(|_| {
let _ = 10;
})?;
Ok(())
}
See also Meta Variables — Bindings.
Partial Application via Flavor
You can partially apply flavor settings via inert attributes:
#[hooq::attribute = flavor_name]for a single field.#[hooq::flavor = flavor_name]to override all inert‑attribute fields.#[hooq::bindings = flavor_name]to override existing user bindings.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _x = $xxx;
let _y = $yyy;
}))]
#[hooq::xxx = "from root"]
#[hooq::yyy = "from root"]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
// Not hooked.
let _ = || -> MyResult { failable(()) };
#[hooq::method = "my_flavor"]
// Method will be changed.
failable(())?;
#[hooq::result_types = "my_flavor"]
// Hooked now.
let _ = || -> MyResult { failable(()) };
#[hooq::bindings = "my_flavor"]
// Bindings will be changed.
failable(())?;
#[hooq::flavor = "my_flavor"]
// All will be changed.
failable(())?;
Ok(())
}
hooq.toml:
[my_flavor]
method = """.inspect_err(|_| {
let _ = "from my_flavor";
let _x = $xxx;
let _y = $yyy;
})"""
result_types = ["Result", "MyResult"]
bindings = { xxx = "\"xxx from my_flavor\"", yyy = "\"yyy from my_flavor\"" }
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
type MyResult = Result<(), String>;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _x = "from root";
let _y = "from root";
})?;
let _ = || -> MyResult { failable(()) };
failable(())
.inspect_err(|_| {
let _ = "from my_flavor";
let _x = "from root";
let _y = "from root";
})?;
let _ = || -> MyResult {
failable(())
.inspect_err(|_| {
let _x = "from root";
let _y = "from root";
})
};
failable(())
.inspect_err(|_| {
let _x = "xxx from my_flavor";
let _y = "yyy from my_flavor";
})?;
failable(())
.inspect_err(|_| {
let _ = "from my_flavor";
let _x = "xxx from my_flavor";
let _y = "yyy from my_flavor";
})?;
Ok(())
}
Method
Specify hooked methods via #[hooq::method(...)] or by flavors. The ... has two modes:
- If it starts with
., it is in insertion mode, inserting the method between the expression and?(or at the end forreturn/tail). - Otherwise it is in replacement mode, replacing the expression. Use
$exprto reference the original expression (already recursively hooked where needed).
Meta variables (see Meta Variables) can be used in these expressions.
Insertion Mode
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _ = "inserted mode";
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::method(.inspect_err(|_| {
let _ = "before chainned";
}).$so_far)]
failable(())?;
#[hooq::method(.$so_far.inspect_err(|_| {
let _ = "after chainned";
}))]
failable(())?;
Ok(())
}
You can chain with .$so_far to prepend/append relative to the existing chain.
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "before chainned";
})
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})
.inspect_err(|_| {
let _ = "after chainned";
})?;
Ok(())
}
Replacement Mode
If it does not start with . (dot), it is treated as a replacement function, replacing the original expression with the configured expression. The expression being replaced can be accessed using the $expr meta variable. (While it’s possible to write without using $expr, it would likely not be useful.)
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
println!("Error occurred: {:?}", e);
}
r
}
#[hooq]
#[hooq::method(wrapper($expr))]
fn main() -> Result<(), String> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
{
::std::io::_print(format_args!("Error occurred: {0:?}\n", e));
};
}
r
}
fn main() -> Result<(), String> {
wrapper(failable(()))?;
Ok(())
}
Use $expr (not $source) in replacement mode. $expr holds the expression after inner hooks have been applied, while $source is for logging/display of the original tokens.
Removing ? via Trailing !
When hooking the ? operator, appending ! to the method consumes the trailing ?.
Requires the
consume-questionfeature.
cargo add hooq --features consume-question
Example aliasing ? to .unwrap():
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, &'static str> {
Ok(val)
}
#[hooq]
#[hooq::hook_targets("?")]
#[hooq::method(.unwrap()!)]
fn main() {
failable(())?;
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, &'static str> {
Ok(val)
}
fn main() {
failable(()).unwrap();
}
Meta Variables
Inside #[hooq::method(...)], you can use special meta variables (prefixed with $) that are replaced with values.
Quick Reference
| Name | Literal Kind | Description |
|---|---|---|
$line | usize | Line number of the target. |
$column or $col | usize | Column number of the target. |
$path | string | Relative path to the file of the target. |
$file | string | File name of the target. |
$source | expression | Original tokens of the target (for logging; differs from $expr). |
$count or $nth | string | Display which numbered target within the function. |
$fn_name or $fnname | string | Name of the function containing the target. |
$fn_sig or $fnsig | string | Signature of the function/closure containing the target. |
$xxx | (any) | User‑defined meta variable via inert attribute. |
$bindings or $vars | HashMap | All user bindings. |
$hooq_meta or $hooqmeta | hooq::HooqMeta | Struct bundling key meta information. |
$expr | expression | Original expression for replacement mode (after inner hooks). |
$so_far or $sofar | expression | Current insertion chain, used to chain further. |
Example using most variables:
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::xxx = "user defined binding."]
#[hooq::method(.inspect_err(|_| {
// Fundamental information provided by hooq.
let _line = $line;
let _column = $column;
let _path = $path;
let _file = $file;
let _source = stringify!($source);
let _count = $count;
let _fn_name = $fn_name;
let _fn_sig = $fn_sig;
// Meta vars defined by user.
let _xxx = $xxx;
let _bindings = $bindings;
// All information summarized up to this point.
let _hooq_meta = $hooq_meta;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _line = 28usize;
let _column = 17usize;
let _path = "mdbook-source-code/meta-vars-all/src/main.rs";
let _file = "main.rs";
let _source = "failable(()) ?";
let _count = "1st ?";
let _fn_name = "main";
let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
let _xxx = "user defined binding.";
let _bindings = ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("xxx"),
{
let expr = ::std::string::ToString::to_string(
"\"user defined binding.\"",
);
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"user defined binding.",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]);
let _hooq_meta = ::hooq::HooqMeta {
line: 28usize,
column: 17usize,
path: "mdbook-source-code/meta-vars-all/src/main.rs",
file: "main.rs",
source_str: "failable(()) ?",
count: "1st ?",
bindings: ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("xxx"),
{
let expr = ::std::string::ToString::to_string(
"\"user defined binding.\"",
);
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"user defined binding.",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]),
};
})?;
Ok(())
}
Target Information
line
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _line = $line;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _line = 12usize;
})?;
Ok(())
}
Prefer $line over line!(); the latter points to the attribute location, not the actual target.
column
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _column = $column;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _column = 17usize;
})?;
Ok(())
}
path
Replaced with the relative path from the crate root (CARGO_MANIFEST_DIR) to the file containing the hook target.
However, the relative path’s starting point may not be the crate root in cases such as when using workspaces. Since procedural macros are designed not to obtain accurate absolute file paths, hooq does not provide meta variables for retrieving absolute paths.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _path = $path;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _path = "mdbook-source-code/meta-vars-path/src/main.rs";
})?;
Ok(())
}
file
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _file = $file;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _file = "main.rs";
})?;
Ok(())
}
source
This meta variable provides the token stream (expression) of the hook target before any hooks are applied by the hooq macro. It is intended for debugging purposes.
In contrast, $expr represents the expression after hooks have already been applied internally. $expr is used in replacement mode to determine where to embed the original expression.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _source = stringify!($source);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _source = "failable(()) ?";
})?;
Ok(())
}
Stringification helpers commonly used with $source:
Example with summary!:
use hooq::hooq;
#[hooq]
#[hooq::method(.inspect_err(|_| {
let source = ::hooq::summary!($source);
eprintln!("{source}");
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
Err((
"aaaaaaaaaaaaaaaaaaaa",
"bbbbbbbbbbbbbbbbbbbb",
"cccccccccccccccccccc",
"dddddddddddddddddddd",
"errorerrorerrorerrorerror",
)
.4
.into())
}
Result excerpt:
10> Err((
...
15| "erro..rror",
16| )
17| .4
18| .into())
|
count
Indicates which numbered target this is within the function for each target type (?, return, or tail expression).
This meta variable is a remnant from when $line could only be obtained on nightly. In most cases, $line is more straightforward.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _count = $count;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _count = "1st ?";
})?;
Ok(())
}
fn_name
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _fn_name = $fn_name;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
(|| -> Result<(), String> {
failable(())?;
Ok(())
})()?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _fn_name = "main";
})?;
(|| -> Result<(), String> {
failable(())
.inspect_err(|_| {
let _fn_name = "__closure_in_main__";
})?;
Ok(())
})()
.inspect_err(|_| {
let _fn_name = "main";
})?;
Ok(())
}
fn_sig
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _fn_sig = $fn_sig;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
(|| -> Result<(), String> {
failable(())?;
Ok(())
})()?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
})?;
(|| -> Result<(), String> {
failable(())
.inspect_err(|_| {
let _fn_sig = "| | -> Result < (), String > {}";
})?;
Ok(())
})()
.inspect_err(|_| {
let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
})?;
Ok(())
}
User‑Defined Meta Variables (Bindings)
Define via inert attributes #[hooq::xxx = ...] or in hooq.toml under bindings.
For details on how to define them, see the respective pages:
Example (inert attribute):
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
DataBase,
Server,
}
#[hooq]
// Can be defined in the format #[hooq::xxx = value]
#[hooq::string = "hello!"] // string literal
#[hooq::integer = 10] // integer literal
#[hooq::cause_kind = CauseKind::Server] // some value
#[hooq::method(.inspect_err(|_| {
let _string = $string;
let _integer = $integer;
let _cause_kind = $cause_kind;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
// Overriding meta variables.
#[hooq::cause_kind = CauseKind::DataBase]
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
DataBase,
Server,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _string = "hello!";
let _integer = 10;
let _cause_kind = CauseKind::Server;
})?;
failable(())
.inspect_err(|_| {
let _string = "hello!";
let _integer = 10;
let _cause_kind = CauseKind::DataBase;
})?;
Ok(())
}
bindings
Retrieve all user-defined meta variables (bindings) as a HashMap<String, BindingPayload>.
BindingPayload stores the stringified binding expression and a value using Rc<dyn Any>.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
#[allow(unused)]
DataBase,
Server,
}
#[hooq]
// Can be defined in the format #[hooq::xxx = value]
#[hooq::string = "hello!"] // string literal
#[hooq::integer = 10] // integer literal
#[hooq::cause_kind = CauseKind::Server] // some value
#[hooq::method(.inspect_err(|_| {
let _bindings = $bindings;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
enum CauseKind {
#[allow(unused)]
DataBase,
Server,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _bindings = ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("cause_kind"),
{
let expr = ::std::string::ToString::to_string(
"CauseKind :: Server",
);
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
CauseKind::Server,
);
::hooq::BindingPayload {
expr,
value,
}
},
),
(
::std::string::ToString::to_string("integer"),
{
let expr = ::std::string::ToString::to_string("10");
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
10,
);
::hooq::BindingPayload {
expr,
value,
}
},
),
(
::std::string::ToString::to_string("string"),
{
let expr = ::std::string::ToString::to_string("\"hello!\"");
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"hello!",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]);
})?;
Ok(())
}
hooq_meta
Bundle meta info into hooq::HooqMeta.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::string = "hello!"]
#[hooq::method(.inspect_err(|_| {
let _hooq_meta = $hooq_meta;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _hooq_meta = ::hooq::HooqMeta {
line: 13usize,
column: 17usize,
path: "mdbook-source-code/meta-vars-hooq_meta/src/main.rs",
file: "main.rs",
source_str: "failable(()) ?",
count: "1st ?",
bindings: ::std::collections::HashMap::from([
(
::std::string::ToString::to_string("string"),
{
let expr = ::std::string::ToString::to_string("\"hello!\"");
let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
"hello!",
);
::hooq::BindingPayload {
expr,
value,
}
},
),
]),
};
})?;
Ok(())
}
See also the hook flavor.
Advanced Meta Variables for Hook Construction
The meta variables introduced so far are primarily for obtaining meta information for logging and debugging.
The remaining two, $expr and $so_far, are meta variables that assist in making methods.
expr
Use in replacement mode to access the (already hooked internally) expression.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
println!("Error occurred: {:?}", e);
}
r
}
#[hooq]
#[hooq::method(wrapper($expr))]
fn main() -> Result<(), String> {
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
E: std::fmt::Debug,
{
if let Err(e) = &r {
{
::std::io::_print(format_args!("Error occurred: {0:?}\n", e));
};
}
r
}
fn main() -> Result<(), String> {
wrapper(failable(()))?;
Ok(())
}
so_far
Represents the chain of insertion‑mode hooks so far. Drop the leading dot when stored; write as .$so_far when inserting.
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq]
#[hooq::method(.inspect_err(|_| {
let _ = "inserted mode";
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::method(.inspect_err(|_| {
let _ = "before chainned";
}).$so_far)]
failable(())?;
#[hooq::method(.$so_far.inspect_err(|_| {
let _ = "after chainned";
}))]
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "before chainned";
})
.inspect_err(|_| {
let _ = "inserted mode";
})?;
failable(())
.inspect_err(|_| {
let _ = "inserted mode";
})
.inspect_err(|_| {
let _ = "after chainned";
})?;
Ok(())
}
Flavors
Flavors are presets that bundle hooq settings. Built‑in flavors:
| Name | feature | Contents |
|---|---|---|
| default | - | Default when nothing is specified; overridable via hooq.toml. |
| empty | - | Disables hooking; inert attributes still processed. Not overridable. |
| hook | - | Inserts a hook method taking hooq::HooqMeta; designed for user traits. Overridable. |
| anyhow | anyhow | Inserts .with_context(...). Overridable. |
| eyre | eyre | Inserts .wrap_err_with(...). Overridable. |
| log | log | Inserts inspect_err that calls ::log::error!. Overridable. |
| tracing | tracing | Inserts inspect_err that calls ::tracing::error!. Overridable. |
Flavor features are part of the default feature set, so you usually do not need to enable them explicitly.
Users can define flavors in a hooq.toml at crate root.
User‑Defined Flavors
hooq.toml uses table names as flavor names with fields:
| Field | Type | Description |
|---|---|---|
| trait_uses | array of strings | Trait paths to import. |
| method | string | Method/expression to insert/replace. |
| hook_targets | array of strings | Any of "?", "return", "tail_expr". |
| tail_expr_idents | array of strings | Idents like "Err". |
| ignore_tail_expr_idents | array of strings | Idents like "Ok". |
| result_types | array of strings | Return type idents like "Result". |
| hook_in_macros | bool | true or false. |
| bindings | inline table | Arbitrary bindings; note string literals must be quoted with \". |
All built‑in (except empty) can be overridden by defining the same table name. Sub‑tables other than bindings are sub‑flavors and inherit from their parent.
See Attributes for how to apply flavors.
Example hooq.toml:
[my_flavor]
trait_uses = ["sub::MyTrait"]
method = """.inspect_err(|_| {
let _ = $xxx;
})"""
hook_targets = ["?", "return", "tail_expr"]
tail_expr_idents = ["Err"]
ignore_tail_expr_idents = ["Ok"]
result_types = ["Result"]
hook_in_macros = true
bindings = { xxx = "\"my_flavor\"" }
[my_flavor.sub_flavor]
bindings = { xxx = "\"my_flavor_sub\"" }
Usage:
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
mod sub {
pub trait MyTrait {}
}
#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
#[hooq::flavor = my_flavor::sub_flavor]
failable(())?;
Ok(())
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
mod sub {
pub trait MyTrait {}
}
#[allow(unused)]
use sub::MyTrait as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.inspect_err(|_| {
let _ = "my_flavor";
})?;
failable(())
.inspect_err(|_| {
let _ = "my_flavor_sub";
})?;
Ok(())
}
default
Default configuration when using #[hooq].
It is configured as follows. (To keep the documentation consistent, this is excerpted directly from the source code; the same applies below.)
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};
use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};
pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
mod flavor_path;
mod presets;
mod toml_load;
#[derive(Debug, Clone)]
pub struct Flavor {
pub trait_uses: Vec<Path>,
pub method: Method,
pub hook_targets: HookTargetSwitch,
pub tail_expr_idents: Vec<String>,
pub ignore_tail_expr_idents: Vec<String>,
pub result_types: Vec<String>,
pub hook_in_macros: bool,
pub bindings: HashMap<String, Rc<Expr>>,
pub sub_flavors: HashMap<String, Flavor>,
}
impl Default for Flavor {
fn default() -> Self {
Self {
trait_uses: Vec::new(),
method: default_method(),
hook_targets: HookTargetSwitch {
question: true,
return_: true,
tail_expr: true,
},
tail_expr_idents: vec!["Err".to_string()],
ignore_tail_expr_idents: vec!["Ok".to_string()],
result_types: vec!["Result".to_string()],
hook_in_macros: true,
bindings: HashMap::new(),
sub_flavors: HashMap::new(),
}
}
}
fn default_method() -> Method {
// NOTE:
// $path や $line は eprintln! に直接埋め込みたいところだが、
// CI側のテストの関係でこのようになっている
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
let res: TokenStream = parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
};
Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}
#[derive(Debug)]
pub struct FlavorStore {
flavors: HashMap<String, Flavor>,
}
// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
pub inner: HooqToml,
}
pub struct TomlStore {
inner: Mutex<HashMap<String, CheckedHooqToml>>,
}
pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
inner: Mutex::new(HashMap::new()),
});
impl TomlStore {
fn load() -> Result<Option<CheckedHooqToml>, String> {
if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
return Ok(Some(checked_hooq_toml));
}
let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(dir_path).join("hooq.toml");
if let Ok(false) | Err(_) = path.try_exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;
let hooq_toml: HooqToml = toml::from_str(&content)
.map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;
let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;
LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());
Ok(Some(checked_hooq_toml))
}
// NOTE:
// 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
// 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
//
// 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
// プロジェクトごとに保存領域を分けるようにした
fn set(&self, checked_hooq_toml: CheckedHooqToml) {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().insert(key, checked_hooq_toml);
}
fn get(&self) -> Option<CheckedHooqToml> {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().get(&key).cloned()
}
}
impl FlavorStore {
fn new() -> Self {
let flavors = presets::preset_flavors();
Self { flavors }
}
pub fn with_hooq_toml() -> Result<Self, String> {
let mut flavors = Self::new();
if let Some(hooq_toml) = TomlStore::load()? {
// 変換が成功することはCheckedHooqTomlの生成時に確認済み
toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
}
Ok(flavors)
}
fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
let mut path = path.iter();
let mut current: &Flavor = self.flavors.get(path.next()?)?;
for name in path {
current = current.sub_flavors.get(name)?;
}
Some(current.clone())
}
pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
self.get_flavor_inner(path).ok_or_else(|| {
format!(
"flavor `{}` is not found. available flavors:
{}",
path.join("::"),
self.all_flavor_names()
.into_iter()
.map(|name| format!(" - {name}"))
.collect::<Vec<_>>()
.join("\n")
)
})
}
pub fn all_flavor_names(&self) -> Vec<String> {
fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
let current_name = format!("{prefix}::");
for (sub_name, sub_flavor) in &flavor.sub_flavors {
let full_name = format!("{current_name}{sub_name}");
names.push(full_name.clone());
collect_names(sub_flavor, &full_name, names);
}
}
let mut names = Vec::new();
for (name, flavor) in &self.flavors {
names.push(name.clone());
collect_names(flavor, name, &mut names);
}
names
}
}
impl TryFrom<HooqToml> for FlavorStore {
type Error = String;
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let mut flavors = Self::new();
toml_load::apply::update_flavors(&mut flavors.flavors, value)?;
Ok(flavors)
}
}
impl TryFrom<HooqToml> for CheckedHooqToml {
type Error = String;
// ロード時に変換可能かをあらかじめ確認する
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let _ = FlavorStore::try_from(value.clone())?;
Ok(Self { inner: value })
}
}
Default method (Please ignore Japanese comments):
use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};
use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};
pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
mod flavor_path;
mod presets;
mod toml_load;
#[derive(Debug, Clone)]
pub struct Flavor {
pub trait_uses: Vec<Path>,
pub method: Method,
pub hook_targets: HookTargetSwitch,
pub tail_expr_idents: Vec<String>,
pub ignore_tail_expr_idents: Vec<String>,
pub result_types: Vec<String>,
pub hook_in_macros: bool,
pub bindings: HashMap<String, Rc<Expr>>,
pub sub_flavors: HashMap<String, Flavor>,
}
impl Default for Flavor {
fn default() -> Self {
Self {
trait_uses: Vec::new(),
method: default_method(),
hook_targets: HookTargetSwitch {
question: true,
return_: true,
tail_expr: true,
},
tail_expr_idents: vec!["Err".to_string()],
ignore_tail_expr_idents: vec!["Ok".to_string()],
result_types: vec!["Result".to_string()],
hook_in_macros: true,
bindings: HashMap::new(),
sub_flavors: HashMap::new(),
}
}
}
fn default_method() -> Method {
// NOTE:
// $path や $line は eprintln! に直接埋め込みたいところだが、
// CI側のテストの関係でこのようになっている
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
let res: TokenStream = parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})
};
Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}
#[derive(Debug)]
pub struct FlavorStore {
flavors: HashMap<String, Flavor>,
}
// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
pub inner: HooqToml,
}
pub struct TomlStore {
inner: Mutex<HashMap<String, CheckedHooqToml>>,
}
pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
inner: Mutex::new(HashMap::new()),
});
impl TomlStore {
fn load() -> Result<Option<CheckedHooqToml>, String> {
if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
return Ok(Some(checked_hooq_toml));
}
let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
let path = PathBuf::from(dir_path).join("hooq.toml");
if let Ok(false) | Err(_) = path.try_exists() {
return Ok(None);
}
let content = std::fs::read_to_string(&path)
.map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;
let hooq_toml: HooqToml = toml::from_str(&content)
.map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;
let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;
LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());
Ok(Some(checked_hooq_toml))
}
// NOTE:
// 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
// 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
//
// 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
// プロジェクトごとに保存領域を分けるようにした
fn set(&self, checked_hooq_toml: CheckedHooqToml) {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().insert(key, checked_hooq_toml);
}
fn get(&self) -> Option<CheckedHooqToml> {
let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
self.inner.lock().unwrap().get(&key).cloned()
}
}
impl FlavorStore {
fn new() -> Self {
let flavors = presets::preset_flavors();
Self { flavors }
}
pub fn with_hooq_toml() -> Result<Self, String> {
let mut flavors = Self::new();
if let Some(hooq_toml) = TomlStore::load()? {
// 変換が成功することはCheckedHooqTomlの生成時に確認済み
toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
}
Ok(flavors)
}
fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
let mut path = path.iter();
let mut current: &Flavor = self.flavors.get(path.next()?)?;
for name in path {
current = current.sub_flavors.get(name)?;
}
Some(current.clone())
}
pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
self.get_flavor_inner(path).ok_or_else(|| {
format!(
"flavor `{}` is not found. available flavors:
{}",
path.join("::"),
self.all_flavor_names()
.into_iter()
.map(|name| format!(" - {name}"))
.collect::<Vec<_>>()
.join("\n")
)
})
}
pub fn all_flavor_names(&self) -> Vec<String> {
fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
let current_name = format!("{prefix}::");
for (sub_name, sub_flavor) in &flavor.sub_flavors {
let full_name = format!("{current_name}{sub_name}");
names.push(full_name.clone());
collect_names(sub_flavor, &full_name, names);
}
}
let mut names = Vec::new();
for (name, flavor) in &self.flavors {
names.push(name.clone());
collect_names(flavor, name, &mut names);
}
names
}
}
impl TryFrom<HooqToml> for FlavorStore {
type Error = String;
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let mut flavors = Self::new();
toml_load::apply::update_flavors(&mut flavors.flavors, value)?;
Ok(flavors)
}
}
impl TryFrom<HooqToml> for CheckedHooqToml {
type Error = String;
// ロード時に変換可能かをあらかじめ確認する
fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
let _ = FlavorStore::try_from(value.clone())?;
Ok(Self { inner: value })
}
}
Usage:
use hooq::hooq;
#[hooq]
fn main() -> Result<(), Box<dyn std::error::Error>> {
Err("Hello, world!".into())
}
Result:
[mdbook-source-code/flavor-default/src/main.rs:5:5] "Hello, world!"
5> Err("Hell..rld!".into())
|
Error: "Hello, world!"
You can override via hooq.toml.
empty
Disables hooking; intended for conditional builds like #[cfg_attr(feature = "...", hooq(empty))].
use std::collections::HashMap;
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn empty_flavor() -> Flavor {
Flavor {
trait_uses: Vec::new(),
method: empty_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
hook_targets: HookTargetSwitch {
question: false,
return_: false,
tail_expr: false,
},
tail_expr_idents: Vec::new(),
ignore_tail_expr_idents: Vec::new(),
result_types: Vec::new(),
hook_in_macros: false,
bindings: HashMap::new(),
sub_flavors: HashMap::new(),
}
}
fn empty_method() -> TokenStream {
parse_quote! {
$expr
}
}
Not overridable.
hook
(Please ignore Japanese comments)
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn hook_flavor() -> Flavor {
Flavor {
// NOTE: Traitの存在を前提とするflavorだがユーザーが決定する必要あり
// trait_uses: Vec::new(), // Default と同じ
method: hook_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn hook_method() -> TokenStream {
parse_quote! {
.hook(|| {
$hooq_meta
})
}
}
Designed for user traits to implement a hook method. It is useful when you do not want to use hooq.toml.
Usage:
use hooq::hooq;
mod my_error {
pub trait MyHook {
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
}
impl<T, E> MyHook for Result<T, E>
where
E: std::fmt::Debug,
{
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
if let Err(e) = &self {
let meta = meta_fn();
eprintln!(
"[{}:{}:{}] error occurred: {:?}",
meta.file, meta.line, meta.column, e
);
}
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(hook, trait_uses(my_error::MyHook))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())?;
Ok(())
}
The second argument is a closure (meta_fn) for lazy evaluation to avoid constructing HooqMeta everywhere.
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod my_error {
pub trait MyHook {
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
}
impl<T, E> MyHook for Result<T, E>
where
E: std::fmt::Debug,
{
fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
if let Err(e) = &self {
let meta = meta_fn();
{
::std::io::_eprint(
format_args!(
"[{0}:{1}:{2}] error occurred: {3:?}\n",
meta.file,
meta.line,
meta.column,
e,
),
);
};
}
self
}
}
}
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(unused)]
use my_error::MyHook as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
failable(())
.hook(|| {
::hooq::HooqMeta {
line: 33usize,
column: 17usize,
path: "mdbook-source-code/flavor-hook/src/main.rs",
file: "main.rs",
source_str: "failable(()) ?",
count: "1st ?",
bindings: ::std::collections::HashMap::from([]),
}
})?;
Ok(())
}
anyhow
Requires
anyhowfeature (included in default).
This flavor is intended to be used with the anyhow crate.
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn anyhow_flavor() -> Flavor {
Flavor {
trait_uses: vec![parse_quote! { ::anyhow::Context }],
method: anyhow_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn anyhow_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.with_context(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
format!("[{path}:{line}:{col}]\n{expr}")
})
}
}
Imports anyhow::Context for .with_context(...).
Usage:
use hooq::hooq;
#[hooq(anyhow)]
fn func1() -> anyhow::Result<i32> {
Err(anyhow::anyhow!("Error in func1"))
}
#[hooq(anyhow)]
fn func2() -> anyhow::Result<i32> {
let res = func1()?;
println!("{res}");
Ok(res)
}
#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
func2()?;
Ok(())
}
Result:
Error: [mdbook-source-code/flavor-anyhow/src/main.rs:19:12]
19> func2()?
|
Caused by:
0: [mdbook-source-code/flavor-anyhow/src/main.rs:10:22]
10> func1()?
|
1: [mdbook-source-code/flavor-anyhow/src/main.rs:5:5]
5> Err(anyhow::anyhow!("Error in func1"))
|
2: Error in func1
eyre
Requires
eyrefeature (included in default).
This flavor is intended to be used with the eyre crate.
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn eyre_flavor() -> Flavor {
Flavor {
trait_uses: vec![parse_quote! { ::eyre::WrapErr }],
method: eyre_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn eyre_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.wrap_err_with(|| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
format!("[{path}:{line}:{col}]\n{expr}")
})
}
}
Imports eyre::WrapErr for .wrap_err_with(...).
Usage:
use hooq::hooq;
#[hooq(eyre)]
fn func1() -> eyre::Result<i32> {
Err(eyre::eyre!("Error in func1"))
}
#[hooq(eyre)]
fn func2() -> eyre::Result<i32> {
let res = func1()?;
println!("{res}");
Ok(res)
}
#[hooq(eyre)]
fn main() -> eyre::Result<()> {
func2()?;
Ok(())
}
Result:
Error: [mdbook-source-code/flavor-eyre/src/main.rs:19:12]
19> func2()?
|
Caused by:
0: [mdbook-source-code/flavor-eyre/src/main.rs:10:22]
10> func1()?
|
1: [mdbook-source-code/flavor-eyre/src/main.rs:5:5]
5> Err(eyre::eyre!("Error in func1"))
|
2: Error in func1
Location:
mdbook-source-code/flavor-eyre/src/main.rs:5:9
log
Requires
logfeature (included in default).
This flavor is intended to be used with the log crate.
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn log_flavor() -> Flavor {
Flavor {
method: log_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn log_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);
::log::error!("({path}:{line}:{col}) {e}\n{expr}");
})
}
}
Usage:
use hooq::hooq;
#[hooq(log)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::new()
.format_timestamp(None)
.filter_level(log::LevelFilter::Error)
.init();
Err("Hello, world!".into())
}
Result:
[ERROR flavor_log] (mdbook-source-code/flavor-log/src/main.rs:10:5) Hello, world!
10> Err("Hell..rld!".into())
|
Error: "Hello, world!"
tracing
Requires
tracingfeature (included in default).
This flavor is intended to be used with the tracing crate.
use proc_macro2::TokenStream;
use syn::parse_quote;
use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;
pub fn tracing_flavor() -> Flavor {
Flavor {
method: tracing_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
..Default::default()
}
}
fn tracing_method() -> TokenStream {
let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();
parse_quote! {
.inspect_err(|e| {
let path = $path;
let line = $line;
let col = $col;
let expr = #excerpted_helpers_path ::one_line_stringify!($source);
::tracing::error!(
path,
line,
col,
error = %e,
expr,
);
})
}
}
Place #[hooq(tracing)] above #[tracing::instrument] to ensure order.
Usage:
use hooq::hooq;
use tracing::instrument;
#[hooq(tracing)]
#[instrument]
fn func1() -> Result<i32, String> {
Err("Error in func1".into())
}
#[hooq(tracing)]
#[instrument]
fn func2() -> Result<i32, String> {
println!("func2 start");
let res = func1()?;
println!("func2 end: {res}");
Ok(res)
}
#[hooq(tracing)]
#[instrument]
fn func3() -> Result<i32, String> {
println!("func3 start");
let res = func2()?;
println!("func3 end: {res}");
Ok(res)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let format = tracing_subscriber::fmt::format()
.with_ansi(false)
.without_time();
tracing_subscriber::fmt().event_format(format).init();
func3()?;
Ok(())
}
Result:
func3 start
func2 start
ERROR func3:func2:func1: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=7 col=5 error=Error in func1 expr="Err(\"Error in func1\".into())"
ERROR func3:func2: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=15 col=22 error=Error in func1 expr="func1()?"
ERROR func3: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=27 col=22 error=Error in func1 expr="func2()?"
Error: "Error in func1"
Features
The hooq crate provides several Cargo features:
| Feature | default | Description |
|---|---|---|
| default | ✓ | Default set; includes all except consume-question. |
| full | ✗ | Includes all features. |
| anyhow | ✓ | Provides the anyhow flavor. |
| eyre | ✓ | Provides the eyre flavor. |
| log | ✓ | Provides the log flavor. |
| tracing | ✓ | Provides the tracing flavor. |
| consume-question | ✗ | Allows removing the ? operator via a trailing ! on hooked methods. |
Only consume-question is excluded from default. Flavor‑related features are included by default.
Recipes & Ideas
This chapter collects smaller ideas enabled by hooq.
Practical usage has already been shown in the Tutorial. These entries may be less practical but explore possibilities and might inspire new use cases.
Contents
| Page | Summary |
|---|---|
| Perfect color-eyre | Use color-eyre for BACKTRACE/SPANTRACE, while also getting hooq’s pseudo trace for maximum insight. |
| Batch apply under a module | hooq can be attached to items like mod to hook inner functions recursively. |
| Using in functions returning Option | Adjust tail_expr_idents and result_types to target Option returns. |
Turn ? into .unwrap() behind a feature | Use #[cfg_attr(...)] to change behavior conditionally. |
Reinvent match desugaring | Replace ? with match using consume-question + custom flavor. |
Perfect color-eyre
color-eyre provides advanced ways to obtain BACKTRACE and SPANTRACE and is hard to ignore.
Let’s extend the official example by adding hooq’s pseudo trace.
Create a new project and set the following in Cargo.toml (replace hooq with the latest version):
[features]
default = ["capture-spantrace"]
capture-spantrace = []
[dependencies]
color-eyre = "0.6.5"
hooq = { path = "../../hooq" } # Please rewrite `hooq = "*"`
tracing = "0.1.43"
tracing-error = "0.2.1"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }
We want color_eyre::eyre::WrapErr instead of ::eyre::WrapErr, so place hooq.toml next to Cargo.toml to override the import. Other settings inherit from the built‑in eyre flavor.
[eyre]
trait_uses = ["color_eyre::eyre::WrapErr"]
Write main.rs. use hooq::hooq; and annotate #[hooq(eyre)] above #[instrument] (we also add .without_time() to stabilize snapshots).
use color_eyre::Section;
use color_eyre::eyre::{Report, WrapErr};
use hooq::hooq;
use tracing::{info, instrument};
#[hooq(eyre)]
#[instrument]
fn main() -> Result<(), Report> {
#[cfg(feature = "capture-spantrace")]
install_tracing();
color_eyre::install()?;
read_config()
}
#[cfg(feature = "capture-spantrace")]
fn install_tracing() {
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{EnvFilter, fmt};
let fmt_layer = fmt::layer().with_target(false).without_time();
let filter_layer = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info"))
.unwrap();
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::default())
.init();
}
#[hooq(eyre)]
#[instrument]
fn read_file(path: &str) -> Result<(), Report> {
info!("Reading file");
std::fs::read_to_string(path).map(drop)
}
#[hooq(eyre)]
#[instrument]
fn read_config() -> Result<(), Report> {
read_file("fake_file")
.wrap_err("Unable to read config")
.suggestion("try using a file that exists next time")
}
Run with RUST_LIB_BACKTRACE=1 to also see the BACKTRACE:
Error:
0: [mdbook-source-code/recipe-color-eyre/src/main.rs:14:5]
14> read_config()
|
1: [mdbook-source-code/recipe-color-eyre/src/main.rs:45:5]
45> read_file("fake_file")
46| .wrap_err("Unab..nfig")
47| .suggestion("try ..time")
|
2: Unable to read config
3: [mdbook-source-code/recipe-color-eyre/src/main.rs:39:5]
39> std::fs::read_to_string(path).map(drop)
|
4: No such file or directory (os error 2)
Location:
mdbook-source-code/recipe-color-eyre/src/main.rs:39
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0: recipe_color_eyre::read_file with path="fake_file"
at mdbook-source-code/recipe-color-eyre/src/main.rs:36
1: recipe_color_eyre::read_config
at mdbook-source-code/recipe-color-eyre/src/main.rs:43
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⋮ 4 frames hidden ⋮
5: <E as eyre::context::ext::StdError>::ext_report::h0e9fcf40b567f05e
at /home/USER/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/eyre-0.6.12/src/context.rs:26
⋮ 1 frame hidden ⋮
7: recipe_color_eyre::read_file::h74782b4bb409e5a9
at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:39
8: recipe_color_eyre::read_config::h8139da57eb93337e
at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:45
9: recipe_color_eyre::main::h9c27f45de169a3be
at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:14
10: core::ops::function::FnOnce::call_once::h724113526cc98cab
at /home/USER/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250
11: std::sys::backtrace::__rust_begin_short_backtrace::h9e20a34286867e4d
at /home/USER/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:158
⋮ 14 frames hidden ⋮
Suggestion: try using a file that exists next time
Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering.
Run with RUST_BACKTRACE=full to include source snippets.
This is likely the most detailed output.
Batch Apply Under a Module
hooq can be attached not only to functions but to items containing executable syntax.
Here, an “item” refers to Rust syntax elements that can appear directly inside a module/scope: functions, mod, impl blocks, trait blocks, etc. (See syn::Item.) Items without executable syntax are irrelevant.
When a mod { ... } (brace form, not mod xxx;) is annotated with #[hooq], inner functions can be hooked without annotating each function.
This recursive behavior is not limited to mod. If functions/closures nest, hooq will greedily hook according to settings for inner functions/closures as well. Use #[hooq::skip_all] if compilation fails or you want to disable in a region.
Puzzle example below illustrating which inner values get hooked:
use hooq::hooq;
#[hooq] // attribute macro root exist only here
#[hooq::method(.inspect_err(|_| {}))]
mod hoge {
pub fn bar() -> i32 {
// NOT hook target because this function does not return Result
42
}
pub fn failable<T>(val: T) -> Result<T, ()> {
// The return type of this function is Result
// but "Ok" is specified as ignore_tail_expr_idents by default
// so NOT hook target
Ok(val)
}
#[hooq::tail_expr_idents("Ok", "Err")]
pub fn _failable_2<T>(val: T) -> Result<T, ()> {
if failable(false).unwrap() {
// ↑ above is NOT hook target
// ↓ below: hook target because the return type of this function is Result
return failable(val);
}
// hook target because "Ok" is now specified as tail_expr_idents
Ok(val)
}
pub fn fuga() -> Result<(), ()> {
// hook target because of `?`
failable(())?;
let _ = || {
// hook target because of `?`
failable(())?;
// hook target because of `?`
if failable(false)? {
// hook target because "Err" is specified as tail_expr_idents by default
return Err(());
}
// NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
Ok(())
};
let _ = || {
// NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
Result::<(), ()>::Ok(())
};
let _ = {
let _ = bar();
// hook target because "Err" is specified as tail_expr_idents by default
// even if this is the tail expression of the block not a closure
Result::<(), ()>::Err(())
};
let _ = || -> Result<(), ()> {
// hook target because hooq can know this closure returns Result
failable(())
};
let _ = || {
// NOT hook target because hooq cannot know this closure returns Result
failable(())
};
// NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
Ok(())
}
}
fn main() {
let _ = hoge::bar();
let _ = hoge::failable(123);
let _ = hoge::fuga();
}
Expansion shows aggressive yet fairly natural hooking for a macro1:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod hoge {
pub fn bar() -> i32 {
42
}
pub fn failable<T>(val: T) -> Result<T, ()> {
Ok(val)
}
pub fn _failable_2<T>(val: T) -> Result<T, ()> {
if failable(false).unwrap() {
return failable(val).inspect_err(|_| {});
}
Ok(val).inspect_err(|_| {})
}
pub fn fuga() -> Result<(), ()> {
failable(()).inspect_err(|_| {})?;
let _ = || {
failable(()).inspect_err(|_| {})?;
if failable(false).inspect_err(|_| {})? {
return Err(()).inspect_err(|_| {});
}
Ok(())
};
let _ = || { Result::<(), ()>::Ok(()) };
let _ = {
let _ = bar();
Result::<(), ()>::Err(()).inspect_err(|_| {})
};
let _ = || -> Result<(), ()> { failable(()).inspect_err(|_| {}) };
let _ = || { failable(()) };
Ok(())
}
}
fn main() {
let _ = hoge::bar();
let _ = hoge::failable(123);
let _ = hoge::fuga();
}
-
Procedural macros lack rich reflection; we cannot type‑infer
Result. ↩
Using in Functions Returning Option
We previously introduced controlling hook target decisions via attributes, which may feel abstract. Here is a more concrete scenario: target only Option values.
In the example below, we hook functions defined inside main (nesting is supported; see Batch apply). We want to inspect when a value is Some.
tail_expr_idents: set toSometo hook onlySome.ignore_tail_expr_idents: set toNoneto avoid wasteful hooks.result_types: set toOptionso tail/return values are considered.
use std::fmt::Debug;
use hooq::hooq;
#[hooq]
#[hooq::method(.my_inspect())]
#[hooq::tail_expr_idents("Some")]
#[hooq::ignore_tail_expr_idents("None")]
#[hooq::result_types("Option")]
fn main() {
fn option_fn_1() -> Option<i32> {
// hook target
Some(42)
}
fn option_fn_2<T: Debug>(flag: bool, val: T) -> Option<T> {
// hook target
let _ = option_fn_1()?;
// hook target because the return type of the function is Option
if flag {
// hook target
Some(val)
} else {
// NOT hook target
None
}
}
fn result_fn_1() -> Result<i32, ()> {
// NOT hook target because the return type of the function is Result not Option
Ok(42)
}
fn result_fn_2() -> Result<i32, ()> {
// HOOK TARGET because of `?`
// so, #[hooq::skip_all] is needed
#[hooq::skip_all]
let _ = result_fn_1()?;
// NOT hook target because the return type of the function is Result not Option
Ok(42)
}
let _ = option_fn_1();
let _ = option_fn_2(true, 123);
let _ = result_fn_1();
let _ = result_fn_2();
}
trait MyInspect {
fn my_inspect(self) -> Self;
}
impl<T> MyInspect for Option<T>
where
T: Debug,
{
fn my_inspect(self) -> Self {
match self {
Some(val) => {
println!("Inspecting value: {:?}", val);
Some(val)
}
None => None,
}
}
}
Expansion:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use std::fmt::Debug;
use hooq::hooq;
fn main() {
fn option_fn_1() -> Option<i32> {
Some(42).my_inspect()
}
fn option_fn_2<T: Debug>(flag: bool, val: T) -> Option<T> {
let _ = option_fn_1().my_inspect()?;
if flag { Some(val).my_inspect() } else { None }.my_inspect()
}
fn result_fn_1() -> Result<i32, ()> {
Ok(42)
}
fn result_fn_2() -> Result<i32, ()> {
let _ = result_fn_1()?;
Ok(42)
}
let _ = option_fn_1();
let _ = option_fn_2(true, 123);
let _ = result_fn_1();
let _ = result_fn_2();
}
trait MyInspect {
fn my_inspect(self) -> Self;
}
impl<T> MyInspect for Option<T>
where
T: Debug,
{
fn my_inspect(self) -> Self {
match self {
Some(val) => {
{
::std::io::_print(format_args!("Inspecting value: {0:?}\n", val));
};
Some(val)
}
None => None,
}
}
}
The point: while Result/Ok/Err are common defaults, hooq does not hard‑code these; you can target other types/idents.
Potentially useful once Try trait stabilizes.
Turn ? into .unwrap() Behind a Feature
As explained in Method, with the consume-question feature enabled, appending ! to the method allows consuming ?.
We can use this to treat ? as an alias for .unwrap().
Example: when an unwrap feature is enabled, replace ? with .unwrap(). Cargo.toml features:
[dependencies]
hooq = { version = "*", features = ["consume-question"] }
# ..
[features]
unwrap = []
Prepare a flavor in hooq.toml. We restrict hook_targets to only ? so tails/returns are unaffected.
[unwrap]
method = """.unwrap()!"""
hook_targets = ["?"]
main.rs:
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[cfg_attr(not(feature = "unwrap"), hooq(empty))]
#[cfg_attr(feature = "unwrap", hooq(unwrap))]
fn process(flag: bool) -> Result<(), String> {
if flag {
return Err("An error occurred".into());
}
let _ = failable(42)?;
Ok(())
}
#[cfg_attr(not(feature = "unwrap"), hooq(empty))]
#[cfg_attr(feature = "unwrap", hooq(unwrap))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
process(false)?;
Ok(())
}
Without the unwrap feature, expansion uses the empty flavor and nothing changes:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn process(flag: bool) -> Result<(), String> {
if flag {
return Err("An error occurred".into());
}
let _ = failable(42)?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
process(false)?;
Ok(())
}
With the unwrap feature, expansion replaces with .unwrap():
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
fn process(flag: bool) -> Result<(), String> {
if flag {
return Err("An error occurred".into());
}
let _ = failable(42).unwrap();
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
process(false).unwrap();
Ok(())
}
Highlights:
- You can replace
?with special behaviors like.unwrap(). - Feature‑gated
#[cfg_attr(..., hooq(...))]lets you vary hooks by build configuration.
Reinvent match Desugaring
In Turn ? into .unwrap() behind a feature we showed that with the consume-question feature enabled, adding ! at the end of the method allows replacing ?.
We can therefore replace ? with a match. A fun example worth documenting.
First, enable consume-question:
cargo add hooq --features consume-question
Define the match expression in hooq.toml (the original is $expr). The flavor is named my_match since match is reserved:
[my_match]
method = """
match $expr {
Ok(val) => val,
Err(err) => return Err(From::from(err)),
}!
"""
Write main.rs as usual (add #[allow(clippy::question_mark)] to avoid lints):
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[hooq(my_match)]
#[allow(clippy::question_mark)]
fn main() -> Result<(), String> {
let _ = failable(42)?;
Ok(())
}
Expansion shows a match as intended:
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
Ok(val)
}
#[allow(clippy::question_mark)]
fn main() -> Result<(), String> {
let _ = match failable(42) {
Ok(val) => val,
Err(err) => return Err(From::from(err)),
};
Ok(())
}