Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.


  1. Generally, the number 4 is considered an unlucky number, but for server-side engineers, 5 may be even more unlucky.

  2. I apologies for the broken compilation error messages, I’ll fix them someday.