2024年8月27日火曜日

Tauri 2.0 のウィンドウにメニューを追加する

やること

Tauri 2.0 での Menu の追加方法がわかったのでやってみる。

前提

Windows 上の WSL2 上の Ubuntu 上のコンテナで実行。

プロジェクトのひな形を作成

$ npm create tauri-app@latest -- --rc
Need to install the following packages:
create-tauri-app@4.3.0
Ok to proceed? (y) y


> npx
> create-tauri-app --rc

 Project name · menu-firststep
 Identifier · com.menu-firststep.app
 Choose which language to use for your frontend · TypeScript / JavaScript - (pnpm, yarn, npm, bun)
 Choose your package manager · npm
 Choose your UI template · React - (https://react.dev/)
 Choose your UI flavor · TypeScript

Template created! To get started run:
  cd menu-firststep
  npm install
  npm run tauri android init

For Desktop development, run:
  npm run tauri dev

For Android development, run:
  npm run tauri android dev

ひな形の動作確認

cd menu-firststep
npm i
npm run tauri dev

実装

書きたいことは全部コメントに書いたので、コードを貼るだけ。

use tauri::menu::{MenuBuilder, MenuId, MenuItemKind};
use tauri_plugin_shell::ShellExt;

// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            // メニュー組み立て開始

            // ビルダーを作って
            let menu = MenuBuilder::new(app)
                // テキストメニューアイテムを追加
                // 第一引数は ID, 第二引数がラベル
                .text("open-url", "Open URL")
                // チェックボックスメニューアイテムを追加
                // 第一引数は ID, 第二引数がラベル
                .check("toggle", "Toggle")
                // アイコンメニューアイテム追加
                // 第一引数は ID, 第二引数がラベル, 第三引数がアイコン
                .icon(
                    "show-app",
                    "Show App",
                    app.default_window_icon().cloned().unwrap(),
                )
                // ビルドしてメニュー完成
                .build()?;

            // app にメニューをセット
            app.set_menu(menu.clone())?;

            // 各メニューがクリックされたとき、
            // on_menu_event で定義した処理が走る。
            app.on_menu_event(move |app, event| {
                // イベントからメニューアイテム ID を取得して
                let MenuId(menu_id) = event.id();

                // メニューアイテム ID に応じた処理を実行する
                if menu_id == "open-url" {
                    println!("Received open-url event.");

                    // デフォルトブラウザで URL を開く
                    app.shell()
                        .open("https://github.com/mikoto2000/TIL", None)
                        .unwrap();
                } else if menu_id == "toggle" {
                    println!("Received toggle event.");

                    // トグルはこんな感じで CheckMenuItem が取得できるので、
                    // それを使って ON/OFF を判別できる
                    let option_menu_item_kind = menu.get("toggle");
                    if let Some(MenuItemKind::Check(check)) = option_menu_item_kind {
                        println!("Check is {:?}.", check.is_checked());
                    } else {
                        panic!("???");
                    }
                } else if menu_id == "show-app" {
                    println!("Received show-app event.");
                } else {
                    println!("Unknown event.");
                }
                println!("{:?}", event);
            });

            Ok(())
        })
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

動作確認

npm run tauri dev
  • 各メニューを押下すると標準出力にどれが押されたかが表示される
  • Open URL を押下すると、デフォルトブラウザで mikoto2000/TIL のページが表示される(コンテナ上だと失敗するので、これだけ Windows で試した)
  • Toggle を押下すると、切り替え後の ON/OFF が true/false で表示される

OK, よさそう。

以上。

参考資料

2024年8月13日火曜日

Rust の sqlx クレートで select クエリを発行する

やること

Rust の sqlx クレートの使い方を学んでいくよ。

前提

プロジェクト作成

cargo init
cargo add tokio --features full
cargo add sqlx --features postgres,runtime-tokio

DB 作成

sudo apt install postgresql-client
postgres://postgres:postgres@localhost/postgres
create table account (
  id serial primary key not null,
  name varchar,
  age integer
);

insert into account
  (
    name, age
  )
values
  (
    'mikoto2000', 2000
  ),
  (
    'mikoto2048', 2048
  ),
  (
    's2000', 25
  );

実装

今回やりたいことをちょこちょこ試していたらよくわからない firststep になってしまった…。

まぁ、最低限 select 文を投げるところは分かるでしょう。

src/main.rs:

use sqlx::postgres::PgPoolOptions;
use sqlx::Column;
use sqlx::Row;
use sqlx::TypeInfo;

#[tokio::main]
async fn main() -> Result<(), sqlx::Error> {
    let pool = PgPoolOptions::new()
        .max_connections(5)
        .connect("postgres://postgres:postgres@localhost/postgres")
        .await
        .unwrap();

    // 静的なタプルにマッピング
    let row: Vec<(i32, String, i32)> = sqlx::query_as("SELECT id, name, age from account")
        .fetch_all(&pool)
        .await
        .unwrap();

    println!("{:?}", row);


    // 動的に各  row, column を確認していく
    let age: i32 = 2000;
    let result = sqlx::query("SELECT id, name, age from account WHERE age >= $1")
        .bind(age)
        .fetch_all(&pool)
        .await
        .unwrap();

    for row in result {
        for column in row.columns() {
            let type_info = column.type_info();
            let type_name = type_info.name();
            match type_name {
                "INT4" => {
                    let value: i32 = row.try_get(column.ordinal()).unwrap();
                    print!("{}, ", value);
                }
                "VARCHAR" => {
                    let value: String = row.try_get(column.ordinal()).unwrap();
                    print!("{}, ", value);
                }
                _ => {
                    print!("unknown type {}", type_name);
                }
            }
        }
        println!()
    }

    Ok(())
}

動作確認

$ cargo run
   Compiling firststep v0.1.0 (/workspaces/firststep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/firststep`
[(1, "mikoto2000", 2000), (2, "mikoto2048", 2048), (3, "s2000", 25)]
1, mikoto2000, 2000, 
2, mikoto2048, 2048, 

OK.

以上。

参考資料

2024年8月10日土曜日

Tauri 2.0 の Android プラグインの Example を helloworld に変更するまで

やること

Android と正しくやり取りするために変更が必要な個所がたくさんあって苦労したので、備忘のために記録しておく。

前提

  • OS: Windows 11 Pro 23H2 ビルド 22631.3880
  • Docker Desktop: Version 4.32.0 (157355)
  • tauri2 と Android ビルドの環境が構築済みであること。
  • Windows 上に Android Studio がインストール済みであり、 adb コマンドが使えること
  • firststep でやったことも含めて全部まとめる

まずは Example が正しく動くことを確認する

プラグインのひな形プロジェクト(Example) を作成

$ npx @tauri-apps/cli@next plugin new --android helloworld
✔ What should be the Android Package ID for your plugin? · com.plugin.helloworld

api プロジェクトのビルド

これで、フロントエンドから import { ping } from "tauri-plugin-example"; して ping(...) できるようになる。

cd tauri-plugin-helloworld/
npm i
npm run build

Example アプリのビルド

「プラグインプロジェクト単体でビルド」という概念は無いので、 Example プロジェクトのビルドを行う。

cd examples/tauri-app
yarn

そして Android プロジェクトのビルド。

identifiercom.tauri.dev, version0.0.0 のままだとビルドができないのでこれを書き換えてビルド。

sed -i -e 's/com\.tauri\.dev/dev.mikoto2000.study.android.plugin.helloworld/' ./src-tauri/tauri.conf.json
sed -i -e 's/0\.0\.0/0.0.1/' ./src-tauri/tauri.conf.json
npm run tauri android init
npm run tauri android build -- --target aarch64 -d

これで、 src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk に apk ファイルができるので、 Android 端末かエミュレーターか WSA にインストールする。

動作確認

自分の環境では物理端末が一番手っ取り早かったのでそれにインストール。

adb.exe install \\wsl.localhost\Ubuntu-24.04\home\mikoto\project\TIL\tauri\2.0.0-beta\plugin\android\helloworld\tauri-plugin-helloworld\examples\tauri-app\src-tauri\gen\android\app\build\outputs\apk\universal\debug\app-universal-debug.apk
ひな形の動作確認結果

Ping したら Pong が返ってくる。OK.

自分のプロジェクトに組み込みたい場合には、この Example プロジェクトの真似をして package.jsonCargo.toml にプラグインプロジェクトを登録すれば OK.

Hello World プラグインへ変更する

ゼロ引数で呼び出したら、 Hello, World! 文字列を返却する hello コマンドを定義する。

ひな形で出力された Java のクラス名を変更

ExamplePlugin という名前で出力されているので、 HelloWorldPlugin という名前に変更する。

呼び出し側も同様。

名前の変更

mv android/src/main/java/ExamplePlugin.kt android/src/main/java/HelloWorldPlugin.kt

次にクラス名の変更。

android/src/main/java/ExamplePlugin.kt:

--- ExamplePlugin.kt    2024-08-10 07:45:56.445244184 +0000
+++ HelloWorldPlugin.kt 2024-08-10 07:47:59.953060605 +0000
@@ -14,7 +14,7 @@
 }
 
 @TauriPlugin
-class ExamplePlugin(private val activity: Activity): Plugin(activity) {
+class HelloWorldPlugin(private val activity: Activity): Plugin(activity) {
     private val implementation = Example()
 
     @Command

そして、このプラグインを呼び出している箇所も変更する。

src/mobile.rs:

--- src/mobile_old.rs   2024-08-10 07:49:34.954659013 +0000
+++ src/mobile.rs       2024-08-10 07:49:57.512796998 +0000
@@ -18,7 +18,7 @@
   api: PluginApi<R, C>,
 ) -> crate::Result<Helloworld<R>> {
   #[cfg(target_os = "android")]
-  let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "ExamplePlugin")?;
+  let handle = api.register_android_plugin(PLUGIN_IDENTIFIER, "HelloWorldPlugin")?;
   #[cfg(target_os = "ios")]
   let handle = api.register_ios_plugin(init_plugin_helloworld)?;
   Ok(Helloworld(handle))

動作確認

どこでつまづくかわからんので、何か修正するたびに動作確認するのを推奨。

cd {PLUGIN_ROOT}
cd examples/tauri-app
npm run tauri android build -- --target aarch64 -d
adb.exe install \\wsl.localhost\Ubuntu-24.04\home\mikoto\project\TIL\tauri\2.0.0-beta\plugin\android\helloworld\tauri-plugin-helloworld\examples\tauri-app\src-tauri\gen\android\app\build\outputs\apk\universal\debug\app-universal-debug.apk

プラグインの実装を変更

Hello, World! を返却するように実装を修正する。

android/src/main/java/HelloWorldPlugin.kt:

--- android/src/main/java/HelloWorldPlugin_old.kt       2024-08-10 07:56:36.663657163 +0000
+++ android/src/main/java/HelloWorldPlugin.kt   2024-08-10 07:59:07.338791931 +0000
@@ -15,14 +15,11 @@
 
 @TauriPlugin
 class HelloWorldPlugin(private val activity: Activity): Plugin(activity) {
-    private val implementation = Example()
 
     @Command
     fun ping(invoke: Invoke) {
-        val args = invoke.parseArgs(PingArgs::class.java)
-
         val ret = JSObject()
-        ret.put("value", implementation.pong(args.value ?: "default value :("))
+        ret.put("message", "Hello, World!")
         invoke.resolve(ret)
     }
 }

ret は、単純な JSON オブジェクトなので、要件に合わせてキーもバリューも変更可能。 今回は valuemessage に変更した。

呼び出されなくなった Example.kt を削除

rm android/src/main/java/Example.kt

valuemessage に変えたので、呼び出し元を修正

src/desktop.rs:

--- src/desktop_old.rs  2024-08-10 08:08:19.963657124 +0000
+++ src/desktop.rs      2024-08-10 08:06:01.223738063 +0000
@@ -16,7 +16,7 @@
 impl<R: Runtime> Helloworld<R> {
   pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
     Ok(PingResponse {
-      message: "Hello, World!",
+      value: payload.value,
     })
   }
 }

デスクトップ版のコードが Hello, World! を返却するようになっていなかったのでついでに直した。

src/models.rs:

--- src/models_old.rs   2024-08-10 08:07:30.517003748 +0000
+++ src/models.rs       2024-08-10 08:07:35.770305605 +0000
@@ -9,5 +9,5 @@
 #[derive(Debug, Clone, Default, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct PingResponse {
-  pub value: Option<String>,
+  pub message: Option<String>,
 }

返却する JSON のキーバリューに合わせて構造体のキーバリューも変更。

guest-js/index.ts:

ここでプラグインからの戻り値を解析しているので、 valuemessage に変更。

--- guest-js/index_old.ts       2024-08-10 08:31:43.500586948 +0000
+++ guest-js/index.ts   2024-08-10 08:31:46.271840251 +0000
@@ -1,9 +1,9 @@
 import { invoke } from '@tauri-apps/api/core'
 
 export async function ping(value: string): Promise<string | null> {
-  return await invoke<{value?: string}>('plugin:helloworld|ping', {
+  return await invoke<{message?: string}>('plugin:helloworld|ping', {
     payload: {
       value,
     },
-  }).then((r) => (r.value ? r.value : null));
+  }).then((r) => (r.message ? r.message : null));
 }

動作確認

どこでつまづくかわからんので、何か修正するたびに動作確認するのを推奨。

cd {PLUGIN_ROOT}
npm run build
cd examples/tauri-app
npm run tauri android build -- --target aarch64 -d
adb.exe install \\wsl.localhost\Ubuntu-24.04\home\mikoto\project\TIL\tauri\2.0.0-beta\plugin\android\helloworld\tauri-plugin-helloworld\examples\tauri-app\src-tauri\gen\android\app\build\outputs\apk\universal\debug\app-universal-debug.apk
挙動変更後の動作確認結果

コマンド名の変更

ひな形では ping になっているので hello に直していく。

数が多いので無言で変更点だけ列挙。

バックエンド

android/src/main/java/HelloWorldPlugin.kt:

--- HelloWorldPlugin_old.kt     2024-08-10 08:43:37.958074978 +0000
+++ HelloWorldPlugin.kt 2024-08-10 08:43:54.033949948 +0000
@@ -17,7 +17,7 @@
 class HelloWorldPlugin(private val activity: Activity): Plugin(activity) {
 
     @Command
-    fun ping(invoke: Invoke) {
+    fun hello(invoke: Invoke) {
         val ret = JSObject()
         ret.put("message", "Hello, World!")
         invoke.resolve(ret)

build.rs:

--- build_old.rs        2024-08-10 08:45:29.460349190 +0000
+++ build.rs    2024-08-10 08:45:36.816924888 +0000
@@ -1,4 +1,4 @@
-const COMMANDS: &[&str] = &["ping"];
+const COMMANDS: &[&str] = &["hello"];
 
 fn main() {
   tauri_plugin::Builder::new(COMMANDS)

src/mobile.rs:

--- mobile_old.rs       2024-08-10 08:42:34.527832052 +0000
+++ mobile.rs   2024-08-10 08:47:32.356542347 +0000
@@ -28,10 +28,10 @@
 pub struct Helloworld<R: Runtime>(PluginHandle<R>);
 
 impl<R: Runtime> Helloworld<R> {
-  pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
+  pub fn hello(&self, payload: PingRequest) -> crate::Result<PingResponse> {
     self
       .0
-      .run_mobile_plugin("ping", payload)
+      .run_mobile_plugin("hello", payload)
       .map_err(Into::into)
   }
 }

src/desktop.rs:

--- desktop_old.rs      2024-08-10 08:42:25.142614925 +0000
+++ desktop.rs  2024-08-10 08:48:37.711540195 +0000
@@ -14,7 +14,7 @@
 pub struct Helloworld<R: Runtime>(AppHandle<R>);
 
 impl<R: Runtime> Helloworld<R> {
-  pub fn ping(&self, payload: PingRequest) -> crate::Result<PingResponse> {
+  pub fn hello(&self, payload: PingRequest) -> crate::Result<PingResponse> {
     Ok(PingResponse {
       value: payload.value,
     })

src/command.rs:

--- commands_old.rs     2024-08-10 08:42:16.608263014 +0000
+++ commands.rs 2024-08-10 08:50:13.923587186 +0000
@@ -5,9 +5,9 @@
 use crate::HelloworldExt;
 
 #[command]
-pub(crate) async fn ping<R: Runtime>(
+pub(crate) async fn hello<R: Runtime>(
     app: AppHandle<R>,
     payload: PingRequest,
 ) -> Result<PingResponse> {
-    app.helloworld().ping(payload)
+    app.helloworld().hello(payload)
 }

app.helloworld().hello() は、 build.rsconst COMMANDS: &[&str] = &["hello"]; が効いているみたい。

src/lib.rs:

--- lib_old.rs  2024-08-10 08:42:43.143107028 +0000
+++ lib.rs      2024-08-10 08:49:37.915377589 +0000
@@ -35,7 +35,7 @@
 /// Initializes the plugin.
 pub fn init<R: Runtime>() -> TauriPlugin<R> {
   Builder::new("helloworld")
-    .invoke_handler(tauri::generate_handler![commands::ping])
+    .invoke_handler(tauri::generate_handler![commands::hello])
     .setup(|app, api| {
       #[cfg(mobile)]
       let helloworld = mobile::init(app, api)?;

permissions/default.toml:

--- default_old.toml    2024-08-10 08:59:59.670456017 +0000
+++ default.toml        2024-08-10 09:00:05.236614391 +0000
@@ -1,3 +1,3 @@
 [default]
 description = "Default permissions for the plugin"
-permissions = ["allow-ping"]
+permissions = ["allow-hello"]

これで、 capabilityhelloworld:default と書くだけで hello が許可される。

フロントエンド

guest-js/index.ts:

--- index_old.ts        2024-08-10 08:54:09.632238557 +0000
+++ index.ts    2024-08-10 08:54:18.780645318 +0000
@@ -1,7 +1,7 @@
 import { invoke } from '@tauri-apps/api/core'
 
-export async function ping(value: string): Promise<string | null> {
-  return await invoke<{message?: string}>('plugin:helloworld|ping', {
+export async function hello(value: string): Promise<string | null> {
+  return await invoke<{message?: string}>('plugin:helloworld|hello', {
     payload: {
       value,
     },

examples/tauri-app/src/App.svelte:

--- App_old.svelte      2024-08-10 08:56:23.722477132 +0000
+++ App.svelte  2024-08-10 08:56:42.557878920 +0000
@@ -1,6 +1,6 @@
 <script>
   import Greet from './lib/Greet.svelte'
-  import { ping } from 'tauri-plugin-helloworld-api'
+  import { hello } from 'tauri-plugin-helloworld-api'
 
        let response = ''
 
@@ -8,8 +8,8 @@
                response += `[${new Date().toLocaleTimeString()}] ` + (typeof returnValue === 'strin
g' ? returnValue : JSON.stringify(returnValue)) + '<br>'
        }
 
-       function _ping() {
-               ping("Pong!").then(updateResponse).catch(updateResponse)
+       function _hello() {
+               hello("Pong!").then(updateResponse).catch(updateResponse)
        }
 </script>
 
@@ -37,7 +37,7 @@
   </div>
 
   <div>
-    <button on:click="{_ping}">Ping</button>
+    <button on:click="{_hello}">Hello</button>
     <div>{@html response}</div>
   </div>
 

動作確認

どこでつまづくかわからんので、何か修正するたびに動作確認するのを推奨。

cd {PLUGIN_ROOT}
npm run build
cd examples/tauri-app
npm run tauri android build -- --target aarch64 -d
adb.exe install \\wsl.localhost\Ubuntu-24.04\home\mikoto\project\TIL\tauri\2.0.0-beta\plugin\android\helloworld\tauri-plugin-helloworld\examples\tauri-app\src-tauri\gen\android\app\build\outputs\apk\universal\debug\app-universal-debug.apk
コマンド名変更後の動作確認結果

入出力の型名の変更

PingRequest, PingResponse となっているのでそれを修正する。

src/command.rs:

--- commands_old.rs     2024-08-10 09:08:01.554610301 +0000
+++ commands.rs 2024-08-10 09:07:51.677197634 +0000
@@ -7,7 +7,7 @@
 #[command]
 pub(crate) async fn hello<R: Runtime>(
     app: AppHandle<R>,
-    payload: PingRequest,
-) -> Result<PingResponse> {
+    payload: HelloRequest,
+) -> Result<HelloResponse> {
     app.helloworld().hello(payload)
 }

src/desktop.rs:

--- desktop_old.rs      2024-08-10 09:08:25.065568537 +0000
+++ desktop.rs  2024-08-10 09:08:37.947672343 +0000
@@ -14,8 +14,8 @@
 pub struct Helloworld<R: Runtime>(AppHandle<R>);
 
 impl<R: Runtime> Helloworld<R> {
-  pub fn hello(&self, payload: PingRequest) -> crate::Result<PingResponse> {
-    Ok(PingResponse {
+  pub fn hello(&self, payload: HelloRequest) -> crate::Result<HelloResponse> {
+    Ok(HelloResponse {
       value: payload.value,
     })
   }

src/mobile.rs:

--- mobile_old.rs       2024-08-10 09:09:26.290321301 +0000
+++ mobile.rs   2024-08-10 09:09:51.865233902 +0000
@@ -28,7 +28,7 @@
 pub struct Helloworld<R: Runtime>(PluginHandle<R>);
 
 impl<R: Runtime> Helloworld<R> {
-  pub fn hello(&self, payload: PingRequest) -> crate::Result<PingResponse> {
+  pub fn hello(&self, payload: HelloRequest) -> crate::Result<HelloResponse> {
     self
       .0
       .run_mobile_plugin("hello", payload)

src/models.rs:

--- models_old.rs       2024-08-10 09:10:21.880734202 +0000
+++ models.rs   2024-08-10 09:10:15.473778847 +0000
@@ -2,12 +2,12 @@
 
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct PingRequest {
+pub struct HelloRequest {
   pub value: Option<String>,
 }
 
 #[derive(Debug, Clone, Default, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct PingResponse {
+pub struct HelloResponse {
   pub message: Option<String>,
 }

※ フロントエンドは {message?: string} という感じで型に名前を付けてないので変更なし。

動作確認

どこでつまづくかわからんので、何か修正するたびに動作確認するのを推奨。

cd {PLUGIN_ROOT}
npm run build
cd examples/tauri-app
npm run tauri android build -- --target aarch64 -d
adb.exe install \\wsl.localhost\Ubuntu-24.04\home\mikoto\project\TIL\tauri\2.0.0-beta\plugin\android\helloworld\tauri-plugin-helloworld\examples\tauri-app\src-tauri\gen\android\app\build\outputs\apk\universal\debug\app-universal-debug.apk

入出力変数のフィールド変更

入力に値はいらないので削除、出力は今のままでOK.

バックエンド

src/desktop.rs:

--- desktop_old.rs      2024-08-10 09:27:30.922875662 +0000
+++ desktop.rs  2024-08-10 09:28:06.748192666 +0000
@@ -14,9 +14,9 @@
 pub struct Helloworld<R: Runtime>(AppHandle<R>);
 
 impl<R: Runtime> Helloworld<R> {
-  pub fn hello(&self, payload: HelloRequest) -> crate::Result<HelloResponse> {
+  pub fn hello(&self) -> crate::Result<HelloResponse> {
     Ok(HelloResponse {
       value: "Hello, World!",
     })

src/mobile.rs:

--- mobile_old.rs       2024-08-10 09:29:03.239267742 +0000
+++ mobile.rs   2024-08-10 09:29:13.963827352 +0000
@@ -28,10 +28,10 @@
 pub struct Helloworld<R: Runtime>(PluginHandle<R>);
 
 impl<R: Runtime> Helloworld<R> {
-  pub fn hello(&self, payload: HelloRequest) -> crate::Result<HelloResponse> {
+  pub fn hello(&self) -> crate::Result<HelloResponse> {
     self
       .0
-      .run_mobile_plugin("hello", payload)
+      .run_mobile_plugin("hello", {})
       .map_err(Into::into)
   }
 }

src/commands.rs:

--- commands_old.rs     2024-08-10 11:36:36.714869837 +0000
+++ commands.rs 2024-08-10 11:36:39.536555095 +0000
@@ -7,7 +7,6 @@
 #[command]
 pub(crate) async fn hello<R: Runtime>(
     app: AppHandle<R>,
-    payload: HelloRequest,
 ) -> Result<HelloResponse> {
-    app.helloworld().hello(payload)
+    app.helloworld().hello()
 }

src/models.rs:

--- models_old.rs       2024-08-10 09:15:16.789108196 +0000
+++ models.rs   2024-08-10 09:16:05.562759826 +0000
@@ -2,9 +2,7 @@
 
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
-pub struct HelloRequest {
-  pub value: Option<String>,
-}
+pub struct HelloRequest { }
 
 #[derive(Debug, Clone, Default, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]

フロントエンド

guest-js/index.ts:

--- index_old.ts        2024-08-10 09:18:45.723552366 +0000
+++ index.ts    2024-08-10 09:18:49.203552425 +0000
@@ -1,9 +1,6 @@
 import { invoke } from '@tauri-apps/api/core'
 
 export async function hello(value: string): Promise<string | null> {
-  return await invoke<{message?: string}>('plugin:helloworld|hello', {
-    payload: {
-      value,
-    },
-  }).then((r) => (r.message ? r.message : null));
+  return await invoke<{message?: string}>('plugin:helloworld|hello', {})
+    .then((r) => (r.message ? r.message : null));
 }

examples/tauri-app/src/App.svelte:

--- App_old.svelte      2024-08-10 09:20:24.190648621 +0000
+++ App.svelte  2024-08-10 09:20:27.197116066 +0000
@@ -9,7 +9,7 @@
        }
 
        function _hello() {
-               hello("Pong!").then(updateResponse).catch(updateResponse)
+               hello().then(updateResponse).catch(updateResponse)
        }
 </script>

動作確認

ここまでで一通り hello 向けに修正したので、最後の動作確認。

cd {PLUGIN_ROOT}
npm run build
cd examples/tauri-app
npm run tauri android build -- --target aarch64 -d
adb.exe install \\wsl.localhost\Ubuntu-24.04\home\mikoto\project\TIL\tauri\2.0.0-beta\plugin\android\helloworld\tauri-plugin-helloworld\examples\tauri-app\src-tauri\gen\android\app\build\outputs\apk\universal\debug\app-universal-debug.apk

参考資料

2024年8月4日日曜日

Rust で DB を使う(diesel + SQLite3)

やること

Tauri で diesel + SQLite3 の知見がちょっとたまったので、記録しておく。 まずは単純な CRUD ができるところまで。

前提

プロジェクト作成

プロジェクト初期化

cargo init

必要なパッケージの追加

今回は SQLite3 を使うので、 featuressqlite を指定。

ORM に必要なので serde, serde_json も入れる。

また、実効のたびに DATABASE_URL を指定するのが面倒なので、 dotenv パッケージも追加する。

cargo add diesel --features sqlite
cargo add serde --features derive
cargo add serde_json
cargo add dotenv

diesel の操作をするためのツールのインストール

diesel_cli に sqlite をバンドルして使うため、 featuressqlite-bundled を指定

cargo install diesel_cli --no-default-features --features sqlite-bundled

DB の準備

DB の場所設定

env ファイルを作成し、 DATABASE_URL に DB の配置場所を指定する。

env:

DATABASE_URL = ./sqlite.db

テーブル定義

diesel の初期化

diesel setup --database-url=./sqlite.db

diesel.tomlmigrations ディレクトリが作成される

  • diesel.toml: diesel 設定ファイル。さわったことない…
  • migrations: テーブル生成・破棄・バージョンアップ操作等を定義していくディレクトリ

マイグレーションファイル作成

diesel migration generate v1

migrations/<作成時刻>_v1/up.sqlmigrations/<作成時刻>_v1/down.sql が作成される。

ここに、テーブルを生成・破棄するための SQL を書いていく。

up.sql の作成

今回は、単純な CRUD を試すのが目的なので、 id, name を持つ user テーブルを作る。

初版なので、単純な CREATE TABLE だけ。

migrations/<作成時刻>_v1/up.sql:

-- Your SQL goes here
create table user (
  id integer primary key autoincrement not null,
  name text not null
);

down.sql の作成

こちらも初版なので、単純な DROP TABLE だけ。

migrations/<作成時刻>_v1/down.sql:

-- This file should undo anything in `up.sql`
drop table user;

マイグレーション機能を使ったテーブル生成

diesel migration run コマンドで先ほど作ったマイグレーションファイルを実行し、テーブルを生成する。

$ diesel migration run --database-url=./sqlite.db
Running migration <作成時刻>_v1

出力を見ると、先ほど作ったマイグレーションファイルが実行されていることがわかる。

実装

概要

diesel を使って DB 操作をする手順は、大まかにいうと以下の感じになる。

  1. 事前準備
    • モデルを作る
  2. DB 接続
    • コネクションを張る
  3. DB 操作
    • diesel の DSL を使って操作

モデルを作る

DB から取得した値をマッピングするための構造体を作成する。

今回は、 task を select した際にマッピングするものと、 Insert 時に指定する値を入れておくものを作る。

src/models.rs:

use diesel::{deserialize::Queryable, prelude::Insertable};
use serde::{Deserialize, Serialize};

use crate::schema::user;

#[derive(Queryable, Deserialize, Serialize, Clone, Debug)]
pub struct User {
    pub id: i32,
    pub name: String,
}

#[derive(Insertable, Queryable, Deserialize, Serialize, Clone, Debug)]
#[diesel(table_name = user)]
pub struct CreateUserParam {
    pub name: String,
}

コネクションを作って DB 操作

ここからは main.rs に処理を書いていく工程なのでまとめてコメントで説明していく。

use dotenv::dotenv;
use models::{CreateUserParam, User};

use std::env;

use crate::schema::user::dsl;

use diesel::{Connection, ExpressionMethods, QueryDsl, RunQueryDsl, SqliteConnection};

// さっき作ったモデルを使えるようにする
mod models;
mod schema;

fn main() {
    // env ファイル内の定義読み込み
    dotenv().ok();

    // コネクションの定義
    let database_url = env::var("DATABASE_URL").unwrap_or("./sqlite.db".to_string());
    let mut conn = SqliteConnection::establish(&database_url)
        .unwrap_or_else(|_| panic!("Error connecting to {}", database_url));


    // CRUD 操作開始

    // Read
    let users = dsl::user.load::<User>(&mut conn).unwrap();
    println!("{:?}", users); // []

    // Create
    let new_user = CreateUserParam {
        name: "mikoto2000".to_string(),
    };
    diesel::insert_into(dsl::user)
        .values(&new_user)
        .execute(&mut conn)
        .expect("Error saving new task");

    // Read
    let users = dsl::user.load::<User>(&mut conn).unwrap();
    println!("{:?}", users); // [User { id: x, name: "mikoto2000" }]

    // Update
    diesel::update(dsl::user.filter(dsl::name.eq("mikoto2000".to_string())))
        .set((dsl::name.eq("makoto2000".to_string()),))
        .execute(&mut conn)
        .expect("Error update task");

    // Read
    let users = dsl::user.load::<User>(&mut conn).unwrap();
    println!("{:?}", users); // [User { id: x, name: "makoto2000" }]

    // Delete
    diesel::delete(dsl::user.filter(dsl::name.eq("makoto2000".to_string())))
        .execute(&mut conn)
        .expect("Error saving new task");

    // Read
    let users = dsl::user.load::<User>(&mut conn).unwrap();
    println!("{:?}", users); // []
}

動作確認

$ cargo run
   Compiling firststep v0.1.0 (/workspaces/TIL/rust/sql/diesel/sqlite3/firststep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/firststep`
[]
[User { id: 6, name: "mikoto2000" }]
[User { id: 6, name: "makoto2000" }]
[]

CRUD, ちゃんとできていそう、 OK.

以上。

参考資料