はじめに
Day 15では、Pythonプロトタイプの完成とRust移行を決断した経緯について書きました。
今回はRust版timer機能の実装について書きます。Pythonプロトタイプとの設計上の違いと、Rust移行に伴って変わった点を記録します。
フロントとの接続方法の変更
Pythonプロトタイプではフロントとの連携にElectron + FastAPIによるHTTP通信を使用していました。Rust版ではフレームワークをTauriに変更し、IPC通信でバックエンドと連携する方式を採用しています。
Tauriを選んだ理由は2点です。配布時のバイナリサイズをさらに削減できることと、Three.jsを用いた3D描画にも対応できることです。
IPCコマンドはwrapper.rsに集約し、ロジックはtimer.rsに分離する構成にしています。
#[tauri::command]
pub fn start_timer_cmd(file: String, timer_start: State<'_,Mutex<Timer>>) -> Result<(), String>{
let mut timer_start_cmd = timer_start.lock().map_err(|e|e.to_string())?;
file_operations::create_file(file)?;
timer::start_timer(&mut timer_start_cmd)?;
Ok(())
}
IPCコマンドの受け口とロジックを分離することで、将来的にコマンドが増えた際も各ファイルの責務が明確に保てます。
Timer構造体の設計
RustにはPythonのようなクラス構文がありません。同等の処理を実現するには構造体の定義が必要です。
pub struct Timer{
pub flag: bool,
pub start_time: Option<DateTime<Local>>,
pub end_time: Option<DateTime<Local>>,
pub total: Option<TimeDelta>
}
flagを除く各フィールドはOption<>型で定義しています。アプリ起動直後など値が格納されていない状態をNoneで表現するためです。Pythonプロトタイプではself.flag = Falseの初期化のみで対応していましたが、Rustでは型レベルで「値がない状態」を表現できます。
状態の永続化はTauriのState管理で実現しています。lib.rsでTimerインスタンスをMutexで包んでStateに登録することで、リクエストをまたいで同一インスタンスを保持できます。
.manage(Mutex::new(Timer{
flag: false,
start_time: None,
end_time: None,
total: None
}))
Pythonプロトタイプではtimer = TimeCheker()をモジュールレベルで永続インスタンス化していましたが、Rust版ではMutex<Timer>をTauri Stateに登録する方式で同等の設計を実現しています。
パス取得方法の変更
Pythonプロトタイプではplatform.system()による自前OS判定でパスを取得していました。Rust版ではdirsクレートを使用する方式に変更しています。
pub fn create_file(paths: String) -> Result<PathBuf, String>{
let data_dir = dirs::data_dir().map(|f| f.join("milestone_manager").join(paths));
match data_dir {
Some(f) => if f.exists(){
return Ok(f);
}
else {
if let Some(parent) = f.parent(){
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
File::create(&f).map_err(|e|e.to_string())?;
Ok(f)
},
None => Err("No such dir".to_string())
}
}
自前実装からクレートに切り替えた理由は、OS判定の網羅性をクレートに任せることでより安全な実装になると判断したためです。dirs::data_dir()は各OSの標準的なユーザーデータ領域を返すため、Pythonプロトタイプと同じ保存先を追加設定なしで実現できます。
CSV書き込みの設計
CSV書き込みはPythonプロトタイプのロジックをそのまま移植しています。csv::Readerでヘッダーの有無を確認し、なければヘッダーを書き込んでから追記する方式です。
let mut header_data = Reader::from_path(&file).map_err(|e|e.to_string())?;
let _header = header_data.headers().map_err(|e|e.to_string())?;
if _header.is_empty(){
let header = ["start-date", "start-time", "end-date", "end-time", "total"];
let mut csv_file = Writer::from_path(file).map_err(|e|e.to_string())?;
csv_file.write_record(header).map_err(|e|e.to_string())?;
}
let csv_file = File::options().append(true).open(file).map_err(|e|e.to_string())?;
let mut csv_writer = csv::Writer::from_writer(csv_file);
ファイルの有無はバイト数ではなくヘッダーの確認で判断しています。今回のツールでは書き込み前にファイル生成を行う設計のため、書き込み時点では必ずファイルが存在します。このためバイト数確認を省略し、ヘッダーの有無のみで判定できる設計になっています。
現在の状態と今後
現時点では基本的な書き込み動作の確認が完了しています。ただしバリデーションチェック機構が不足していることは認識しており、テストを進めながら追加実装を行っています。バリデーション実装が完了した段階で次回記事にまとめます。
おわりに
今日はRust版timer機能の実装について書きました。
PythonからRustへの移植は単純な書き直しではなく、Rustの型システムや所有権の仕組みに合わせた設計の見直しが必要でした。特にOption型による状態表現とMutexによる状態管理は、Pythonとは異なるアプローチです。
次の記事はバリデーション実装が完了した段階で公開します。
この記事は連載「クラウドに依存しないマイルストーン管理ツール開発記」のDay 16です。