Closed32

Rust勉強にTauriやsqlxやSvelteも盛ってみる。

ピン留めされたアイテム
えっつえっつ

作業記録・備忘録なのでちゃんとした情報求めていたら他を探した方が有意義と思う。

えっつえっつ

データ移行だるいから助けてほしいので支援ツールを作る。

  1. データ構造を2つ以上登録
  2. 移行プロジェクトを作成
  • 移行元データ構造と移行先データ構造を指定
  • 移行元から移行先へ列の関係性を指定
  1. 移行プロジェクトからデータ出力
  • SQL
  • CSV
えっつえっつ

そんなに知識ないけど Rust/Tauri/Sqlx/Svelte/TypeScript と欲張っておく。

えっつえっつ

入れておくもの

# cargo tauri init 等 carg tauri * なコマンド
cargo install tauri-cli
えっつえっつ

入れておくと良さげなもの。

# cargo パッケージ更新が楽になる cargo upgrade が使えたりする
cargo install cargo-edit
えっつえっつ

Tauri プロジェクトをサクッと作るならこれ。
Svelte テンプレートだと Rust ソース用に src-tauri ができるのでほとんど手を加えなくて良さそう。

npm create tauri-app
えっつえっつ

rust-version = "1.57" だった。rust-version = "1.62" に書き換えて良いものかな。
アプリケーションで使う Rust バージョンだから多分良いと思うけど。

えっつえっつ

設計資料も vivliostyle をあえて使っていくぞ。
業務にも使いたいので設計資料が別出しできると良い(Office/PDF な会社なので)。

えっつえっつ
# ビルド
cargo tauri build
# 開発実行
cargo tauri dev

tauri.conf.jsonbeforeBuildCommand の値が npm run build となっているか、 beforeDevCommand の値が npm run dev となっているか確認。

えっつえっつ

Installation 見ながら、 Cargo.toml に案内通り書いてみると tag 見つからんとかでエラー。
リポジトリ見ると「確かにー」なんだけど。下記にして cargo update はうまくいったわよ

[dependencies.tauri-plugin-sql]
git = "https://github.com/tauri-apps/tauri-plugin-sql"
features = ["sqlite"]
branch = "release"
えっつえっつ

一旦 tauri-plugin-sql は外して様子見、Rust から使うのに必要なかったりして。。。

えっつえっつ

っぱ、Rust 側に DB は制御してほしいので、sqlx-cli 使って DB 構成する方向で。
下記でスキーマ定義のマイグレーション作っておく。

sqlx migrate add schema

下記で sqlite データベースを作成して、マイグレーション走らせる。

sqlx db create --database-url "sqlite:database.db"
sqlx migrate run --database-url "sqlite:database.db"
えっつえっつ

create_db なるものが下記の Issue に有った。

https://github.com/launchbadge/sqlx/issues/1114

これに似た感じで実装したところ、下記を自動化できた。

sqlx db create --database-url "sqlite:database.db"
sqlx migrate run --database-url "sqlite:database.db"

で、下記が実装。わからないことだらけなので #[async_std::main] のことは分かっちゃいない。一応、async_stdfeatures にある attributes に入ってることは分かった。Rust の非同期 Crate (?)多くない?

#[async_std::main]
async fn main() -> Result<(), sqlx::Error> {
    create_db().await?;

    tauri::Builder::default()
        .run(tauri::generate_context!())
        .expect("error while running tauri application");

    Ok(())
}

// DB 作成
use sqlx::{migrate::MigrateDatabase, sqlite::SqlitePool};

async fn create_db() -> Result<SqlitePool, sqlx::Error> {
    use std::path::Path;

    let db_url: &str = "sqlite:database.db";

    if !sqlx::Sqlite::database_exists(&db_url).await? {
        sqlx::Sqlite::create_database(&db_url).await?;
    }

    let db: SqlitePool = SqlitePool::connect(&db_url).await?;

    sqlx::migrate::Migrator::new(Path::new("./migrations"))
        .await?
        .run(&db)
        .await?;

    Ok(db)
}
えっつえっつ

async_std は標準 API の非同期バージョンっていう Crate。

https://async.rs/

tokio はネットワークアプリケーションの実装に必要な API を非同期に処理しようっていう Crate。

https://tokio.rs/

tokio で十分な気もするけど async_std だともっと細かい所も非同期処理に……ってできるのかな?

えっつえっつ

Svelte で SPA な時にハッシュベースルーティングを担ってくれる svelte-spa-router を導入。SPA も初めてなので、ちょっとでも「いつもの感」を維持する為ルーティングしたかった。

https://github.com/ItalyPaleAle/svelte-spa-router

ナビゲーションのクラス制御でちょっとてこずったのでメモ。

js 標準の location.href 取得して「今どこの配下?」を視覚的に表現しようとして、リアクティブステートメントで確認させて~ってやってみたけど上手くいかなかった。

何となく、SPA でルーティングしてるからかな~、なんて思って svelte-spa-routerlocation でイメージ通りのことが出来そうだったので試したところ上手くいった。

<script lang="ts">
  import { location } from "svelte-spa-router";
  let nowGroup: string = "top";

  function getNowGroup(location: string) {
    const reDataStructure: RegExp = new RegExp("data-structure");
    const reMigrateProject: RegExp = new RegExp("migrate-project");

    if (reDataStructure.test(location)) {
      nowGroup = "data-structure";
    } else if (reMigrateProject.test(location)) {
      nowGroup = "migrate-project";
    } else {
      nowGroup = "top";
    }
  }
  $: getNowGroup($location);
</script>

で、nowGroup によって class を変化させるってわけ。

えっつえっつ

共通で参照できるクラスを定義したものの JS に変換するとダメなの(プロパティの型指定を指される)、って言われちゃったので調べてみたところ、rollup.config.js で typescript のところに rootDir を指定しないといけないっぽい。

事前に js が生成されて解釈できるようになるっぽい。

えっつえっつ

他の環境で動かなかったとき確認すること

  • rustup update
  • npm install
  • C++ ビルドツールの有無★
    • Visual Studio Installer から C++ デスクトップ開発を追加するなど
えっつえっつ

open-color を npm でインストールした後でバンドルするのに手間取ったのでメモ

main.tsimport "open-color/open-color.css"; を追加したらうまくいった。

えっつえっつ

フォーム変更の確認は SPA だとどうやってるんだろうか。

えっつえっつ

やっとこさバックエンドを進めてく。

str, Stringって何の差があるの?」「Vecってなに?」と思ったので公式リファレンス読んでまとめ。

https://doc.rust-jp.rs/book-ja/ch03-02-data-types.html

データ型

そもそもデータ型にはスカラ型、複合型がある。
スカラ型はプリミティブ型に当たる。

type kind type name type string description
スカラ型 整数型 i8 8bit 符号有り整数
スカラ型 整数型 i16 16bit 符号有り整数
スカラ型 整数型 i32 32bit 符号有り整数, 標準且つ最速
スカラ型 整数型 i64 64bit 符号有り整数
スカラ型 整数型 u8 8bit 符号無し整数
スカラ型 整数型 u16 16bit 符号無し整数
スカラ型 整数型 u32 32bit 符号無し整数
スカラ型 整数型 u64 64bit 符号無し整数
スカラ型 浮動小数点数型 f32 32bit 浮動小数点数
スカラ型 浮動小数点数型 f64 64bit 浮動小数点数, 標準
スカラ型 論理値型 bool 論理値
スカラ型 文字型 char 16bit 符号無し整数
複合型 タプル型 (type, type, ..., type) 異なる型の固定長のデータの集まり
複合型 配列型 [type; size] 同じ型の固定長のデータの集まり

整数型は接頭辞 0x, 0o, 0b を付けられ、16 進数、8 進数、2 進数を見たまま書ける(整数リテラル)。
タプル型は let (x, y, z) = <tuple> とすると分配される(<tuple> はタプル型のデータ)。
配列型は let a = [3; 5] と書くことで同値の入った配列を作れる。

Vec, str, String とか出てこなかったな。

ベクタ型

https://doc.rust-jp.rs/book-ja/ch08-01-vectors.html

Vec<T> で表現する特定型のコレクション型。まあリスト。

文字列型

https://doc.rust-jp.rs/book-ja/ch08-02-strings.html

文字列スライス型: str
文字列型: String

スライス型は所有権が無くコレクションの一連の要素を参照。
& は C 言語さんと同じ参照を示す。Rust では所有権をもらわずに値参照できるよ。

とりあえず違いは分かったのでここまで。

えっつえっつ

~.await?? はパニック発生時にエラーを挙げてくれる。raise とか throw とかのような。
エラー発生の場合は即時エラー値を返す、エラー発生しなかったら呼び出した関数の返り値を返す。

えっつえっつ

Result<T, E>Option<T> から T のデータを取り出してくれる unwrap()
T のデータを取り出せなかったときに panic! 起こしてエラー発生。

えっつえっつ

sqlx によるクエリ作成するときのメソッドメモ。メソッドでなくて関数の方が良いかしら?

  • sqlx::query<DB>(): 単純にクエリ作成
  • sqlx::query_as<DB, O>(): Osqlx_core::from_row::FromRow(データベースから返された行から作成できるレコード)を派生した(derive)構造体に基づいてデータベースからの返り値をマッピングする前提でクエリ作成。

多分よくあるメソッドの使い分けで良いと思う。

どっち 実行
SELECT sqlx::query_as<DB, O>() fetch(executer), fetch_all(executer), fetch_one(executer),fetch_optional(executer)
INSERT, UPDATE, DELETE sqlx::query<DB>() execute(executer)

後者は PostgreSQL で RETURNING せずに INSERT 文使ったりすると INSERT 0 1 が返ってくるだろうからエラーになると思う。SELECT 文でないときは使わなくていいと思う。

他言語の同様なモジュール・ライブラリにもそんな使い分けあったよなぁと思いつつ。

えっつえっつ

sqlx 特有かわからないけど INSERT 文を実行したら、結果として Ok(SqliteQueryResult { changes: 1, last_insert_rowid: 2 }) が返ってくる。追加したデータの PK が返ってくるようになってるっぽい?

えっつえっつ

Rust 側とフロントエンド側のやり取りにおける引数渡しについて、わかりづらかったのでメモ。

[tauri::command] を指定した Rust 側の関数を tauri::app::Builder.invoke_handler() でコマンドとして登録できるが、この引数渡し。

下記のように定義した Rust コマンドはその下の TypeScript/JavaScript で呼び出せるよ。Rust 側はスネークケース、TypeScript/JavaScript ではキャメルケースに。

#[tauri::command]
pub fn command(arg_any: i64) -> bool { ... }

fn main() -> Result<(), sqlx::Error> {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            command,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from "@tauri-apps/api/tauri";
function anyFunction() {
  invoke("command", { argAny: 1 });
}

引数に構造体を使うこともできる。構造体にシリアライズ・デシリアライズをしてくれるフレームワーク serdeserde::Serialize, serde::Deserialize から派生させておく。

構造体の中身のパラメータ名はスネークケースのままで良かった。

#[serde::Serialize, serde::Deserialize]
struct command_data {
  param_one: String,
  param_two: i64
}

#[tauri::command]
pub fn command(arg_any: command_data) -> bool { ... }

fn main() -> Result<(), sqlx::Error> {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            command,
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from "@tauri-apps/api/tauri";
function anyFunction() {
  let struct = {
      param_one: "xxx",
      param_two: 1
  }
  invoke("command", { 
    argAny: struct
  });
}
えっつえっつ

Rust 側からの返り値が構造体の場合も中身にアクセスするのは Rust 側通り(スネークケース)

このスクラップは2022/10/12にクローズされました