rust知らない人のためのtauri

この記事は、[Tauriアドベントカレンダー 2024](https://qiita.com/advent-calendar/2024/tauri) 15日目の記事です。

## はじめに

Rustの学習としてTauriを使ってみたのですが、やはりRust、難しかったので、詰まった部分や最初から知っておきたかったライブラリなどを紹介していきます。

## 詰まった部分やTips

### デバッグ

Debugトレイトがついた型なら以下の形でデバッグできます。

```rust
println!("{:#?}", hoge);
/* 
=> Hoge {
    foo: 1,
    bar: false,
}
*/
```

## Commandのテスト

私が使っているtauri: 2.0.6では `tauri::test::mock_builder`  の実行でエラーが出て `tauri::State` や `tauri::AppHandle` が作成できなかったので以下のようにエラーが出ないよう回避しました。

**tauri::Stateを使ったCommandのテスト**

stateの中身だけを受ける関数を作ってそのテストをしました。

中身だけを受ける関数を作成することでテストでtauri::Stateを作成する必要がなくなります。

```rust
async fn get_game_state_exec(my_state: &MyState) -> Result<(),String> {
    my_state.call_function()
}

#[tauri::command]
#[specta::specta]
pub async fn get_game_state(state: tauri::State<'_, MyState>) -> Result<(), String> {
    get_game_state_exec(state.inner()).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::dto::GameState;
    // 非同期処理があるなら#[tokio:test]が必要
    #[tokio::test]
**    async fn should_return_game_state() {
        assert_eq!(get_game_state_exec(&container).await, Ok(()));
    }
}

```

**tauri::AppHandleを使ったCommandのテスト**

こちらはtauri::Apphandleをラップすることでtauri::AppHandleの作成を回避しています。

ラップしてあげることでテストでtauri::AppHandleを作成する必要がなくなります。

mockallを使ってますが、これについては後述します。

```rust
#[mockall::automock]
pub trait CommandAppHandle {
    fn version(&self) -> Result<String, String>;
}

pub struct CommandAppHandleImpl {
    app: tauri::AppHandle,
}

impl CommandAppHandleImpl {
    pub fn new(app: tauri::AppHandle) -> Self {
        Self { app }
    }
}
impl CommandAppHandle for CommandAppHandleImpl {
    fn version(&self) -> Result<String, String> {
        self.app
            .config()
            .version
            .clone()
            .ok_or("No version found".to_string())
    }
}

```

```rust
async fn hello_exec(state: impl CommandAppHandle) -> Result<(), String> {
    println!("result: {:?}", state.version());
    Ok(())
}

#[tauri::command]
#[specta::specta]
pub async fn hello(state: tauri::AppHandle) -> Result<(), String> {
    let command_handle = CommandAppHandleImpl::new(state);
    hello_exec(command_handle).await
}

#[cfg(test)]
mod tests {
    use crate::commands::app_handle::MockCommandAppHandle;

    use super::*;
    #[tokio::test]
    async fn should_return_ok() {
        let mut app_handle = MockCommandAppHandle::new();
        app_handle
            .expect_version()
            .returning(|| Ok("test_version".to_string()));
        assert_eq!(hello_exec(app_handle).await, Ok(()));
    }
}

```

## 便利なライブラリ

### [mockall](https://docs.rs/mockall/latest/mockall/)

簡単にモックが作れるライブラリです。

テストに便利

```rust
use mockall::automock;

#[automock]
trait MyTrait {
    fn foo(&self, x: u32) -> u32;
}

fn call_with_four(x: &dyn MyTrait) -> u32 {
    x.foo(4)
}

#[cfg(test)]
mod tests {
    use mockall::predicate;
    #[test]
    fn should_return_5() {
        let mut mock = super::MockMyTrait::new();
        mock.expect_foo()
            .with(predicate::eq(4))
            .times(1)
            .returning(|x| x + 1);
        assert_eq!(5, super::call_with_four(&mock));
    }
}

```

### [dotenvy](https://docs.rs/dotenvy/latest/dotenvy/)と[envy](https://docs.rs/envy/latest/envy/)

dotenvyは .envファイルを環境変数に読み込むクレート

envyは環境変数を構造体で取得するクレート

です。

```rust
use serde::Deserialize;

#[derive(Deserialize, Debug)]
pub struct Config {
    pub foo: u16,
    pub bar: bool,
    pub baz: String,
    pub boom: Option<u64>,
}

fn main(){
    dotenvy::dotenv().expect(".env file not found");
    let config = envy::from_env::<env::Config>().expect("environment variables not found");
    println!("{:#?}", config);
    /*
	    => Config {
			    foo: 1,
			    bar: false,
			    baz: "",
			    boom: None,
			}
    */
}
```

### [shaku](https://docs.rs/shaku/latest/shaku/)

DIコンテナ

mockallと組み合わせてテストが書きやすくなる

いろいろ調べましたが、shakuを使ったDIが一番シンプルで手っ取り早いです。

```rust
pub struct Player {
    pub is_main_player: bool,
}

#[mockall::automock]
pub trait PlayerRepository: shaku::Interface {
    fn get_party_of_main_player(&self) -> Result<Vec<Player>, DomainError>;
    fn get_party(&self, party_id: i32) -> Result<Vec<Player>, DomainError>;
}

#[derive(shaku::Component)]
#[shaku(interface = PlayerRepository)]
pub struct PlayerRepositoryImpl;

impl player::PlayerRepository for PlayerRepositoryImpl {
    fn get_party_of_main_player(
        &self,
    ) -> Result<Vec<player::Player>, crate::domain::error::DomainError> {
        todo!()
    }

    fn get_party(
        &self,
        party_id: i32,
    ) -> Result<Vec<player::Player>, crate::domain::error::DomainError> {
        todo!()
    }
}

```

```rust
shaku::module! {
    pub AppModule {
        components = [crate::domain::player::PlayerRepositoryImpl,],
        providers = []
    }
}

#[cfg(test)]
pub mod test {
    use shaku::ModuleBuilder;

    use super::*;
    pub fn create_mock_module_builder() -> ModuleBuilder<AppModule> {
		    // テスト時にいちいち置き換えるコードを書かなくていいように、ここでモックを入れちゃうといいです。
        AppModule::builder()
            .with_component_override::<dyn crate::domain::player::PlayerRepository>(Box::new(
                crate::domain::player::MockPlayerRepository::new(),
            ))
    }
}
```

### [async_trait](https://docs.rs/async-trait/latest/async_trait/)

非同期関数を持つtraitに対してshakuを使うとobject safe系のエラーが出るので対処する必要がるのですが、async_traitを使うとそういったRust特有のよくわかんないエラーを無視することができます。

次のコードはエラーが出ます。

```rust

#[mockall::automock]
pub trait GameStateRepository: shaku::Interface {
    async fn get_game_state(&self) -> GameState;
}

// ↓で「for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>」が出る
#[derive(shaku::Component)]
#[shaku(interface = game_state::GameStateRepository)]
pub struct GameStateRepositoryImpl {
    #[shaku(inject)]
    valorant_api: Arc<dyn crate::infra::valorant::ValorantAPI>,
}

impl game_state::GameStateRepository for GameStateRepositoryImpl {
    async fn get_game_state(&self) -> game_state::GameState {
        match self.valorant_api.get_game_state().await {
            Ok(_) => game_state::GameState::Login,
            Err(_) => game_state::GameState::None,
        }
    }
}

```

以下の修正をするだけでエラーが消えます。

```diff
+ #[async_trait::async_trait]
#[mockall::automock]
pub trait GameStateRepository: shaku::Interface {
    async fn get_game_state(&self) -> GameState;
}

#[derive(shaku::Component)]
#[shaku(interface = game_state::GameStateRepository)]
pub struct GameStateRepositoryImpl {
    #[shaku(inject)]
    valorant_api: Arc<dyn crate::infra::valorant::ValorantAPI>,
}

+ #[async_trait::async_trait]
impl game_state::GameStateRepository for GameStateRepositoryImpl {
    async fn get_game_state(&self) -> game_state::GameState {
        match self.valorant_api.get_game_state().await {
            Ok(_) => game_state::GameState::Login,
            Err(_) => game_state::GameState::None,
        }
    }
}

```

### [anyhow](https://docs.rs/anyhow/latest/anyhow/)と[thiserror](https://docs.rs/thiserror/latest/thiserror/)

ただでさえシンプルなrustのエラーハンドリングがさらに楽になります。

thiserrorで [`std::error::Error`](https://doc.rust-lang.org/nightly/core/error/trait.Error.html) トレートを実装したエラーが簡単に作成でき、anyhowでそれやio:Errorなどをまとめて処理することができます。

```rust
// ↓便利
#[derive(Debug, thiserror::Error)]
enum MyError {
    #[error("this is less than {0}")]
    Small(i32),
    #[error("this is more than {0}")]
    Big(i32),
}

// 本来なら `pub fn sample(i: i32) -> Result<i32, io::Error | MyError> `みたいなことを書く必要があるが、anyhowならまとめて処理できる
pub fn sample2(i: i32) -> anyhow::Result<i32> {
    if i == 0 {
        Err(io::Error::from(io::ErrorKind::Other))?
    } else if i < 5 {
        Err(MyError::Small(i))?
    } else if i > 10 {
        Err(MyError::Big(i))?
    } else {
        Ok(i)
    }
}

```