開発

[binllion] データのエクスポート機能を追加する

インポートとくればエクスポートです。

インポートと違い、エクスポートはファイルの書き込み失敗のおそれがあるのでエラー処理が必要です。

Rustでファイルに書き出す処理はオプションがいくつかあるようで、それについて調べるために下記を実験してみました。

// write_file_create

use std::io::Write;

// NOTE: 事前に下記コマンドでファイルを用意しておく
// echo "01234567890123456789" > ./test.data

// main 関数
#[rustfmt::skip]
fn main() -> Result<(), std::io::Error> {
    let path = "./test.data";
    let mut file = std::fs::File::create(path)?; // 上書き+余分なデータ削除
    // let mut file = std::fs::OpenOptions::new().write(true).open(path)?;  // 上書き+余分なデータは残る
    // let mut file = std::fs::OpenOptions::new().write(true).create(true).open(path)?;  // 上書き+余分なデータは残る
    // let mut file = std::fs::OpenOptions::new().write(true).truncate(true).create(true).open(path)?;  // 上書き+余分なデータ削除
    // let mut file = std::fs::OpenOptions::new().write(true).create(true).truncate(true).open(path)?;  // 上書き+余分なデータ削除
    let _ = file.write_all(b"test write create")?;

    Ok(())
}
Code language: PHP (php)

std::fs::OpenOptionsを使う場合、ファイルに対する処理の仕方が異なってきます。

まず、前提として

echo "01234567890123456789" > ./test.dataCode language: PHP (php)

std::fs::OpenOptions::new().write(true).open(path)?;の場合は、ファイルが存在しないとエラーとなります。

また、ファイルが存在する場合は書き込みますが、

test write create789

となり、書き込みデータの分だけ上書きされてファイル全体での処理とはなりません。

ファイルが存在しない場合に書き込みできるようにするには、create(true)を追加すると、新規で作成するようになります。

ファイル全体で上書きしたい、余計なデータが残らないようにするには、truncate(true)を追加することでエクスポートした内容がそのままファイルの内容となります。

std::fs::File::create(path)を使うとcreate(true)truncate(true)も指定された状態になります。

ということで、ファイルのエクスポート処理は最終的に下記のようにしました。

    pub(crate) fn export_to(&self, path: &String) -> Result<(), std::io::Error> {
        let mut file = std::fs::File::create(path)?;
        let _ = file.write_all(self.buf())?;

        Ok(())
    }
Code language: PHP (php)

イベントの追加

ファイルのエクスポートを実行するために入力キーの処理を追加します。

起動時に引数でファイル指定されている場合は内容を上書き、指定がない場合はエラーメッセージを出すようにしました。

ファイルのpathを指定するような機能を追加しても良かったのですが、次のバージョンの機能としたいと思います。

            // ファイルへ保存
            KeyCode::Char('s') | KeyCode::Char('S') => {
                if let Some(path) = message.current_file().path() {
                    if let Err(e) = message.bin_data().export_to(path) {
                        message.notice_mut().add(e.to_string());
                    } else {
                        let success_msg = String::from("Saved!");
                        message.notice_mut().add(success_msg);
                    }
                } else {
                    let err_msg = String::from("Not specified file path");
                    message.notice_mut().add(err_msg);
                }
            }
Code language: JavaScript (javascript)

エクスポート成否のステータス表示

エクスポートの成否がわかるように画面にメッセージを出すようにしています。

一瞬だけ表示するとわからないのでしばらく表示するような仕掛けをいれるため新たにNotice構造体を追加しました。

// ステータス伝達
pub(crate) struct Notice {
    count: Cell<u8>,
    notice: RefCell<VecDeque<String>>,
    cache: RefCell<String>,
}

impl Notice {
    pub(crate) fn new() -> Self {
        Self {
            count: Cell::new(0),
            notice: RefCell::new(VecDeque::new()),
            cache: RefCell::new(String::new()),
        }
    }

    pub(crate) fn add(&mut self, state: String) {
        self.notice.borrow_mut().push_back(state);
    }

    pub(crate) fn pop_front(&self) -> String {
        const LIMIT: u8 = 2;
        match self.count.get() {
            0 => {
                if let Some(state) = self.notice.borrow_mut().pop_front() {
                    *self.cache.borrow_mut() = format!(" {state} ");
                    self.count.set(1);
                } else {
                    self.cache.borrow_mut().clear();
                }
            }
            x if x > LIMIT => {
                self.count.set(0);
            }
            x => {
                self.count.set(x + 1);
            }
        }

        self.cache.borrow().clone()
    }
}
Code language: PHP (php)
    // メッセージ取り出し
    let notice = message.notice().pop_front();

    let status_bar_mid = Line::from(notice).centered();
Code language: JavaScript (javascript)

この処理で苦労したことがpop_front()の処理です。

pop_front()を呼ぶ場面ではmessageは可変参照を使わないことを想定していたのです。

ですが、pop_front()はmessage構造体の中の変数の内容に変更が生じます。

これはコードのデザインが悪い可能性が高いのですが、ここからデザインに変更を加えるのは大変なので別の方法を検討しました。

Rustの標準ライブラリにはCellやRefCellのような内部可変性を扱う機能が提供されています。今回はこちらを使用しました。

unsafe寄りな機能なのでスレッドで使えない等制約がありますがいまのところ、そういった機能は使わないので一旦これで良しとします。

GitHubにコードをアップロードしています。0.1.0バージョン

コードのコメントに書かれているfirst_stepなどをcargoコマンドに渡すと実行できます。

# Example
$ cargo run --example first_stepCode language: Bash (bash)
管理人

Recent Posts

情報セキュリティマネジメント試験取得への道

スキルアップを図るべく情報セキ…

2か月 ago

ファイナンシャルプランナー3級試験取得への道

スキルアップを図るべくファイナ…

2か月 ago

[rust] New Type Patternを使ってみる

DDDの考えを取り入れることで…

5か月 ago

RustでDDDの要素を取り入れてみる

前回SOLID原則というものを…

5か月 ago

期間限定!書籍無料キャンペーン2025

「mdBookではじめるKin…

5か月 ago