Crate hooq_helpers

Crate hooq_helpers 

Source
Expand description

This is sub-crate for hooq crate. Please use hooq crate instead of using this crate directly.

hooq

A simple macro that inserts (hooks) a method before question operator (`?`).

crate docs Rust

?🪝 The name hooq comes from combining ‘HOOk’ and the ‘Question mark operator ( ? )’. 🪝?

Enhance your questions by hooq!?

Keywords: Result, Option, hook, Result hook, Option hook, ? hook, question hook, error, logging

Documentations:

[!NOTE] 日本語版ドキュメントはこちら: docs/ja/README.md


Insert methods specified with #[hooq::method(...)] between expressions and the ? operator (Question Operator)!

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)
}

fn main() {
    assert_eq!(double("21").unwrap(), double_expanded("21").unwrap());
}

You don’t have to explicitly specify #[hooq::method(...)]—there’s also a mechanism called “flavors” for easily applying pre-configured settings!

§Why hooq?

Being able to insert a method before the ? operator lets you reduce boilerplate for debugging and logging. As a result, you can increase logging information without sacrificing readability.

The Context::with_context method from the anyhow crate is a prime example. The hooq crate provides a flavor (anyhow) to make inserting this method easy.

use hooq::hooq;

#[hooq(anyhow)]
fn func1() -> anyhow::Result<i32> {
    Err(anyhow::anyhow!("Error in func1"))
}

#[hooq(anyhow)]
fn func2() -> anyhow::Result<i32> {
    func1()
}

#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
    func2()?;

    Ok(())
}

.with_context(|| {...}) gets hooked, and the output looks like this:

Error: [mdbook-source-code/flavor-anyhow/src/main.rs:15:12]
  15>    func2()?
    |

Caused by:
    0: [mdbook-source-code/flavor-anyhow/src/main.rs:10:5]
         10>    func1()
           |
    1: [mdbook-source-code/flavor-anyhow/src/main.rs:5:5]
          5>    Err(anyhow::anyhow!("Error in func1"))
           |
    2: Error in func1

Other concrete examples would make the README too long, so they have been compiled in the mdBook: Why use hooq?

Applying #[hooq] (or #[hooq(anyhow)]) to all functions yields information close to an error stack trace.

Below is a comparison table with other ways to obtain stack-trace-like information.

Backtracetracinghooq
Learning cost & flexibility⚠️⚠️🌈
Ease of type definitions⚠️
Macro-less🌈
Information control⚠️🌈
Platform support⚠️🌈

Legend:

  • 🌈: Excellent
  • ✅: Good
  • ⚠️: Not so good
  • ❌: Poor

Explanations:

  • Learning cost & flexibility
    • ⚠️ Backtrace requires defining the environment variable RUST_LIB_BACKTRACE=1, and because it depends on OS threads you need knowledge outside pure Rust control flow.
    • ⚠️ tracing can be overkill if your only goal is a stack-trace-like overview. If you are already comfortable with it, it is a reasonable option.
    • 🌈 hooq only needs attaching an attribute macro to function heads.
  • Ease of type definitions
    • ⚠️ When combining Backtrace with crates like thiserror, you must include the backtrace field up front. The more granular your error types, the harder retrospective addition becomes, or the less simple the error representation stays.
    • tracing imposes no particular constraints here.
    • hooq works smoothly with any error handling crate.
  • Macro-less
    • 🌈 Backtrace does not rely on macros, which feels lightweight.
    • ❌ To conveniently obtain stack-trace-equivalent info with tracing, using #[tracing::instrument(err)] (or similar) is almost mandatory.
    • hooq is an attribute macro crate; if you prefer to avoid macros entirely, it is not suitable.
  • Information control
    • ⚠️ The raw output of Backtrace is often too verbose 😵. In async contexts raw frames can be nearly useless and frequently excessive.
      • Crates like color-eyre can improve formatting and make it more practical.
    • tracing can follow functions even across async boundaries; however, to know details such as “which line’s ? operator?” you must add manual logging.
    • 🌈 hooq (similar to #[tracing::instrument]) traces only functions annotated with #[hooq], giving you precise, opt‑in coverage. It can capture exact positions of ?, return, and tail expressions for finer granularity.
      • Being an attribute macro you can conditionally attach it only in tests or behind features (#[cfg_attr(..., hooq(...))]).
      • 💡 Can be combined with tracing to increase information granularity. See the flavor docs for tracing in the mdBook.
  • Platform support
    • ⚠️ Backtrace may be unavailable or partial on some platforms (see official docs: https://doc.rust-lang.org/std/backtrace/index.html#platform-support).
    • ✅ Ordinary tracing logging use cases typically face few platform restrictions.
    • 🌈 hooq merely inserts a method before ? (and optionally return / tail expressions), so it does not depend on platform-specific features. Creative usage per platform is possible.
      • 💡 For example #[hooq::method(.unwrap()!)] can make ? behave akin to a forced unwrap alias.

§Documentation

For detailed usage instructions, please refer to the following! (Also included at the beginning, but repeated here)

[!NOTE] 日本語版ドキュメントはこちら: docs/ja/README.md

§Install

Add it with cargo add as shown below,

cargo add hooq

or add it to your Cargo.toml.

[dependencies]
hooq = "0.3.0"

[!NOTE] MSRV is 1.88 due to $line meta variable line retrieval via proc_macro::Span::line.

§Method inserted by default

If you don’t specify anything for #[hooq], the following method is inserted by default:

.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}");
})

You can switch the method to hook with the inert attribute #[hooq::method(...)]. Also, when you specify a flavor at the macro call site such as #[hooq(log)] or #[hooq(anyhow)], the inserted method will change according to that flavor!

§Attributes Quick Reference

The hooq macro can modify its behavior using inert attributes such as #[hooq::method(...)].

See mdbook’s Attributes for more details!

NameTypeDescription
flavorMacro root metaApply settings for the specified flavor
trait_useMacro root metaInsert use XXX as _; before the item for the specified path (XXX)
methodInert attributeSet the method to insert (or expression to replace in replace mode)
skip_all / skipInert attributeDisable hooking for expressions with this attribute
hook_targetsInert attributeToggle hooking for ?, return, and tail expressions (default: all three)
tail_expr_identsInert attributeSpecify idents to hook when they appear in tail position (default: Err)
ignore_tail_expr_identsInert attributeSpecify idents to not hook even when they would be hooked (default: Ok)
result_typesInert attributeSpecify return types for functions to hook return and tail expressions (default: Result)
hook_in_macrosInert attributeSpecify whether to hook inside macros (default: true)
bindingInert attributeCreate meta variables that are replaced with specified literals/expressions

Usage example:

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(())
}

§Meta Variables Quick Reference

With method settings for insertion such as #[hooq::method(...)], you can use information convenient for debugging and logging through meta variables such as $line.

See mdbook’s Meta Variables for more details!

NameLiteral TypeDescription
$lineusize integerLine number where the hook target is located
$column or $colusize integerColumn number where the hook target is located
$pathstringRelative path to the file containing the hook target
$filestringName of the file containing the hook target
$sourceexpressionExpression for insertion/replacement target used for debugging/logging (note the difference from $expr)
$count or $nthstringIndicates which replacement target this is
$fn_name or $fnnamestringName of the function containing the hook target
$fn_sig or $fnsigstringSignature of the function containing the hook target
$xxx (example)(arbitrary)User-defined meta variable via inert attribute #[hooq::xxx = ...]
$bindings or $varsHashMapAll meta variable bindings
$hooq_meta or $hooqmetahooq::HooqMetaStruct combining $line, $col, $path, $file, $source, $count, and $bindings
$exprexpressionExpression for replacement target used for replacement (note the difference from $source)
$so_far or $sofarexpressionHook set so far, mainly used for insertion

Usage example:

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(())
}

§Built-in Flavors Quick Reference

Configuring settings with attributes every time is tedious. hooq has a feature called “flavors” that allows you to pre-configure settings.

Flavors can be user-defined, and hooq also provides several built-in ones!

See mdbook’s Flavors for more details! Below are the pre-configured flavors (built-in flavors).

Flavor NameFeatureDescription
default-The flavor used when nothing is specified. Can be overridden with hooq.toml
empty-A flavor for when you don’t want to hook anything. Cannot be overridden
hook-Inserts a hook method taking hooq::HooqMeta as an argument. Intended for use via user-defined traits. Can be overridden
anyhowanyhowInserts the with_context method. Can be overridden
eyreeyreInserts the wrap_err_with method. Can be overridden
loglogInserts an inspect_err method calling ::log::error!. Can be overridden
tracingtracingInserts an inspect_err method calling ::tracing::error!. Can be overridden

Usage example:

use hooq::hooq;

#[hooq(anyhow)]
fn func1() -> anyhow::Result<i32> {
    Err(anyhow::anyhow!("Error in func1"))
}

#[hooq(anyhow)]
fn func2() -> anyhow::Result<i32> {
    func1()
}

#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
    func2()?;

    Ok(())
}

Output example:

Error: [mdbook-source-code/flavor-anyhow/src/main.rs:15:12]
  15>    func2()?
    |

Caused by:
    0: [mdbook-source-code/flavor-anyhow/src/main.rs:10:5]
         10>    func1()
           |
    1: [mdbook-source-code/flavor-anyhow/src/main.rs:5:5]
          5>    Err(anyhow::anyhow!("Error in func1"))
           |
    2: Error in func1

§License

This project is dual-licensed:

§Contributing

Issues and PRs are welcome!

Contribution guidelines are summarized on a separate page with the following topics:

  • Snapshot testing
  • CI
  • sync.rs command
  • Languages (English/Japanese)
  • Stance on using generative AI

CONTRIBUTING

Structs§

BindingPayload
Binding payload that contains the expression string and its value.
HooqMeta
Metadata about the invocation of the hooq macro.