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

フレーバー

フレーバー はhooqマクロのフックするメソッドや挙動等をまとめたプリセットのことを指します。hooqではあらかじめ以下のフレーバーを用意しています。

組み込みフレーバーのクイックリファレンス

フレーバー名feature内容
default-何も指定しない場合に設定されるフレーバー。hooq.tomlで上書き可
empty-全く何もフックしない場合に用いるフレーバー。上書きは不可
hook-hooq::HooqMeta を引数に取る hook メソッドを挿入するフレーバー。ユーザー定義のトレイト経由での利用を想定。上書き可
anyhowanyhowwith_context メソッドを挿入するフレーバー。上書き可
eyreeyrewrap_err_with メソッドを挿入するフレーバー。上書き可
loglog::log::error! を呼び出す inspect_err メソッドを挿入するフレーバー。上書き可
tracingtracing::tracing::error! を呼び出す inspect_err メソッドを挿入するフレーバー。上書き可

一応feature名を記載しましたが、フレーバーに関係するfeatureはdefault featureに含まれているので明示的にCargo.tomlの features に含める必要はありません。

フレーバーはユーザーが自分で定義することも可能です。クレートのルート( CARGO_MANIFEST_DIR が指し示すディレクトリ) に置いた hooq.toml という名前のtomlファイルにて定義します。詳細は次節にて解説します。

ユーザー定義のフレーバー

hooq.tomlファイルはテーブル名をフレーバー名とし、テーブルに次のフィールドを記述することで設定できます。

フィールド名取りうる値の説明
trait_uses文字列配列トレイトパス
method文字列フックするメソッド
hook_targets文字列配列"?" or "return" or "tail_expr"
tail_expr_idents文字列配列"Err" などフックしたい識別子
ignore_tail_expr_idents文字列配列Ok などフックしたくない識別子
result_types文字列配列"Result" などの返り値型
hook_in_macros真偽値true or false
bindingsインラインテーブル任意のバインディング。文字列リテラル指定時は \" で囲む必要がある点に注意

empty を除いたhooqで用意している組み込みフレーバーをテーブル名にすることも可能であり、その場合設定値の上書きとなります。

フレーバーテーブルが持つ bindings 以外のテーブルは サブフレーバー となります。サブフレーバーは親フレーバーの設定値を引き継ぎ、部分的に設定を変更したフレーバーとなります。

設定項目の説明やフレーバーの適用方法等は属性を参照してください。

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\"" }

使用例:

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

展開結果:

#![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

何も指定しなかった場合( #[hooq] として付与した場合)の設定となるフレーバーです。

次のような設定になっています。(ドキュメントの整合性を保つためソースコードから直接抜粋しています。以降同様)

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() で設定しているメソッドは次の通りです。(コメントは気にしないでください。)

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

使用例:

use hooq::hooq;

#[hooq]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    Err("Hello, world!".into())
}

実行結果:

[mdbook-source-code/flavor-default/src/main.rs:5:5] "Hello, world!"
   5>    Err("Hell..rld!".into())
    |
Error: "Hello, world!"

この設定はhooq.tomlで上書きが可能です。その場合、 #[hooq] とした際の設定値が上書きした設定値になります。

empty

全くフックを行わないフレーバーです。 #[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
    }
}

特殊なフレーバーであるため 唯一上書き不可 としています。

hook

hookフレーバーの設定は次の通りです。(コメント部分は気にしないでください。)

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

ユーザー側でトレイトを定義し、そこでロギングをする場合に便利なフレーバーです。hooq.tomlを使いたくない際に便利です。

使用例:

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

第2引数部分を meta_fn というクロージャにしているのは、遅延評価のためです。ここをクロージャにしない場合すべての場所で HooqMeta が生成されてしまうため処理が重くなってしまいます。

展開結果:

#![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

anyhow feature が必要ですが、defaultに含まれています。

anyhowクレート と共に使うことを想定したフレーバーです。

次の設定になっています。

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

.with_context(...) メソッドを利用するために、 anyhow::Context トレイトをuseしています。

使用例:

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

実行結果:

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

eyre feature が必要ですが、defaultに含まれています。

eyreクレート と共に使うことを想定したフレーバーです。anyhowとはuseしているトレイト、呼び出しているメソッドが異なるだけでほぼ同じです。

次の設定になっています。

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

.wrap_err_with(...) メソッドを利用するために、 eyre::WrapErr トレイトをuseしています。

使用例:

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

実行結果:

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

log feature が必要ですが、defaultに含まれています。

logクレート と共に使うことを想定したフレーバーです。

次の設定になっています。

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

使用例:

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

実行結果:

[ERROR flavor_log] (mdbook-source-code/flavor-log/src/main.rs:10:5) Hello, world!
      10>    Err("Hell..rld!".into())
        |
Error: "Hello, world!"

tracing

tracing feature が必要ですが、defaultに含まれています。

tracingクレート と共に使うことを想定したフレーバーです。

次の設定になっています。

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

#[tracing::instrument] と併用する場合、 #[hooq(tracing)] が先に適用される必要があるため、 #[tracing::instrument] より上に #[hooq(tracing)] を書くことを推奨します。

使用例:

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

実行結果:

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"