Tauri+SycamoreでYAMLの読み込みと書き出しをする

前回は、Tauri+SycamoreでHelloWorld(UIもほんの少しだけ変更)しました。 次は、ファイルの読み書きを行い、データを保存してみます。

前回はこちら a3colorr.hatenablog.com

環境

やること

次のユースケースの通りです。

  1. テキストフィールドに単語などを入力する
  2. Saveボタンを押す
  3. HOMEディレクトリにあるword_data.yamlに入力した単語などを保存する

今回はYAML形式でテキストファイルにデータを保存します。
YAML形式でファイルに書き込むときは、以下のようにリストで書き込むことにします。

- what
- where
- who

完成イメージはこんな感じ

yaml-save-example
入力した単語をYAMLに保存する

ファイルの読み書き処理を書くfiles.rsを作る

main.rsと同じディレクトリにファイルを作っても良いのですが、せっかくなので、src-tauri/srcディレクトyaml_ioを作り、その中にfiles.rsを作ります。

$ cd src-tauri/src
$ mkdir yaml_io
$ touch yaml_io/mod.rs
$ touch yaml_io/files.rs

// mod.rsに次の1行を追加
pub mod files

これ以降は、files.rsに処理を書いていきます。

YAML形式を扱えるようにする

YAMLファイル自体はテキストファイルなので、ファイルの読み書きさえできれば後は自力で、Rustで扱える形(HashMapなど)にパースしたり、ファイルに書き込める形式に加工したりしても良いのですが、既存の優秀なクレートを使った方が良いことづくめです。
そこでYAMLを扱うクレートを探したところ、serde_yamlが良さそうでしたので、今回はこれをありがたく使わせて頂きました。 github.com

serde_yamlGithubリポジトリにあるREADMEに書いてある通り、serdeserde_yamlの依存関係をsrc-tauri/Cargo.tomlに追加します。Cargo.tmolには既にserdeの依存関係があったので、serde_yamlだけ追加しました。

#src-tauri/Cargo.toml
[dependencies]
tauri = { version = "1.2", features = ["shell-open"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9" <-これ

Cargo.tomlを保存したら、すぐにcargo tauri devで依存関係をインストールしても良いですし、最後の最後に行っても良いです。

YAMLの扱いですが、

  • YAMLファイルから読み込むとき
    • 文字列で読み込み、その文字列をリストっぽいものに変換する
  • YAMLファイルに書き込むとき
    • リストっぽいものを文字列に変換し、その文字列を書き込む

Rustでリストっぽいものというと、配列([])かベクタ(Vec)あたりになるかと思いますが、serde_yamlではVec<Value>エイリアスとしてSequenceが定義されているので、今回はこれを利用します。Vec<Value>Valueもまた、serde_yamlで定義されているYAMLで有効な値となります。中身はBoolStringなどが列挙型で定義されています。
serde_yamlのリファレンスを参考に、以下のような文字列->YAML変換yaml_fromYAML->文字列変換yaml_string_toの2つの関数を作りました。

/* files.rs */
use serde_yaml;
use serde_yaml::Sequence;

// file_dataをYAMLに変換する
fn yaml_from(file_data: &str) -> Result<Sequence, serde_yaml::Error> {
    let yaml: Sequence = serde_yaml::from_str(file_data)?;
    return Ok(yaml);
}

// Sequence型のdataをString型に変換する
fn yaml_string_to(data: Sequence) -> Result<String, serde_yaml::Error> {
    let string_data = serde_yaml::to_string(&data)?;
    return Ok(string_data);
}

ファイルを読み書きできるようにする

ファイルを読み込むread_wordとファイルに書き込むwrite_wordを追加します。
ファイルパスや変換するデータは引数で受け取るようにしました。
これで4つの関数ができましたが、個別に呼ぶとソースコードがにぎやかになるので、読み込みと書き込みで'reading'とwritingを1つ呼べば良いようにします。外から呼ぶのはこの2つだけにしたいので、この2つだけをpubとしました。

/* files.rs */
use serde_yaml;
use serde_yaml::Sequence;

use std::fs;
use std::io;

// file_dataをYAMLに変換する
fn yaml_from(file_data: &str) -> Result<Sequence, serde_yaml::Error> {
    let yaml: Sequence = serde_yaml::from_str(file_data)?;
    return Ok(yaml);
}

// Sequence型のdataをString型に変換する
fn yaml_string_to(data: Sequence) -> Result<String, serde_yaml::Error> {
    let string_data = serde_yaml::to_string(&data)?;
    return Ok(string_data);
}

// file_pathを読み込んで、Result<String, serde_yaml::Error>を返す
fn read_word(file_path: &str) -> Result<String, io::Error> {
    match fs::read_to_string(file_path) {
        Ok(file) => {
            return Ok(file);
        },
        Err(e) => {
            println!("Error: {}", e);
            return Err(e);
        }
    };
}

// file_pathを開いて、yaml_stringを書き込む
fn write_word(file_path: &str, yaml_string: &str) -> Result<(), io::Error> {
    match fs::write(file_path, yaml_string) {
        Ok(_) => {
            return Ok(());
        },
        Err(e) => {
            println!("Error: {}", e);
            return Err(e);
        }
    };
}

// data_pathを読み込んで、Sequence型の値を返す
// ただし、data_pathが存在しない場合とYAMLのシリアライズに失敗した場合は、
// 空のValueを返す
pub fn reading(data_path: &str) -> Sequence {
    let data = match read_word(data_path) {
        Ok(data) => {
            data
        },
        Err(e) => {
            println!("Error: {}", e);
            return Sequence::new();
        }
    };

    match yaml_from(&data) {
        Ok(yaml) => {
            return yaml;
        },
        Err(e) => {
            println!("Error: {}", e);
            return Sequence::new();
        }
    };
}

// Sequence型のdataをString型のyaml_stringに変換して、file_pathに書き込む
pub fn writing(data: Sequence, file_path: &str) -> bool {
    let yaml_string: String = match yaml_string_to(data) {
        Ok(yaml_string) => {
            yaml_string
        },
        Err(e) => {
            println!("Error: {}", e);
            return false;
        }
    };

    match write_word(file_path, &yaml_string) {
        Ok(_) => {
            return true;
        },
        Err(e) => {
            println!("Error: {}", e);
            return false;
        }
    };
}

tauri-src/src/main.rsからYAMLの読み書き関数を呼ぶ

main.rsreading関数とwriting関数を定義して、各々の中で先ほどのreturn yaml_io::files::readingreturn yaml_io::files::writingを呼びます。
また、データを保存するYAMLword_data.yamlはホームディレクトリに保存することにします。

// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use serde_yaml::{Value, Sequence};
use std::env;

#[tauri::command]
fn inputed(word: &str) -> String {
    // reading()でSequence型の値を取得してdataに代入する
    // その後、nameをdataに追加する
    let mut data = reading();
    data.push(Value::String(word.to_string()));

    // dataを書き込む
    writing(data);

    // UIに表示する文字列を返す
    return format!("I saved the {} in YAML!", word);
}

mod yaml_io;

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![inputed])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

// word_data.yamlを読み込む
fn reading() -> Sequence {
    return yaml_io::files::reading(&file_create_path());
}

// word_data.yamlに書き込む
fn writing(data: Sequence) {
    yaml_io::files::writing(data, &file_create_path());
}

// word_data.yamlのパスを作成する
// ここでは、ホームディレクトリにword_data.yamlを作成する
fn file_create_path() -> String {
    let home_path = env::var("HOME").unwrap();
    return format!("{}/word_data.yaml", home_path);
}

UIを作る

最後に、UIを作ります。
単語等を入力できれば十分なので、最初に作られたテンプレートUIから余分なものを削除して、変数名を変更しました。

/* src/app.rsを一部抜粋 */

#[derive(Serialize, Deserialize)]
struct InputedArgs<'a> {
    word: &'a str,
}

#[component]
pub fn App<G: Html>(cx: Scope) -> View<G> {
    let word = create_signal(cx, String::new());
    let inputed_word = create_signal(cx, String::new());

    let inputed = move |e: Event| {
        e.prevent_default();
        spawn_local_scoped(cx, async move {
            let new_msg =
                invoke("inputed", to_value(&InputedArgs { word: &word.get() }).unwrap()).await;

            log(&new_msg.as_string().unwrap());

            inputed_word.set(new_msg.as_string().unwrap());
        })
    };

    view! { cx,
        main(class="container") {
            form(class="row",on:submit=inputed) {
                input(id="word-input", bind:value=word, placeholder="Enter a word...")
                button(type="submit") {
                    "Save"
                }
            }
            p {
                b {
                    (inputed_word.get())
                }
            }
        }
    }
}

あとは、cargo tauri devでアプリを起動して動作を確認します。