第17回のプロジェクトのCargo.tomlを次にします。tempfileは一時ファイルを安全に作り、保存先へ置き換えるために使います。
[package]
name = "study-tasks-rs"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "=1.0.228", features = ["derive"] }
serde_json = "=1.0.145"
tempfile = "=3.23.0"
src/main.rsを次にします。src/task.rsは前回のものを使います。
mod task;
use task::Task;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::error::Error;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct Store { version: u32, tasks: Vec<Task> }
fn load(path: &Path) -> Result<Vec<Task>, Box<dyn Error>> {
let file = match File::open(path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(error.into()),
};
let mut text = String::new();
file.take(1_000_001).read_to_string(&mut text)?;
if text.len() > 1_000_000 { return Err("保存ファイルが大きすぎます".into()); }
let store: Store = serde_json::from_str(&text)?;
if store.version != 1 { return Err("未対応の保存形式です".into()); }
let mut ids = HashSet::new();
for task in &store.tasks {
task.validate()?;
if !ids.insert(task.id) { return Err("IDが重複しています".into()); }
}
Ok(store.tasks)
}
fn save(path: &Path, tasks: Vec<Task>) -> Result<(), Box<dyn Error>> {
let bytes = serde_json::to_vec_pretty(&Store { version: 1, tasks })?;
if bytes.len() > 1_000_000 { return Err("保存ファイルが大きすぎます".into()); }
let parent = path.parent().filter(|p| !p.as_os_str().is_empty()).unwrap_or(Path::new("."));
let mut temp = tempfile::NamedTempFile::new_in(parent)?;
temp.write_all(&bytes)?;
temp.as_file().sync_all()?;
temp.persist(path)?;
Ok(())
}
fn run(args: &[String]) -> Result<(), Box<dyn Error>> {
if args.len() < 2 { return Err("使い方: 保存先 add|list|done [値]".into()); }
let path = Path::new(&args[0]);
let mut tasks = load(path)?;
match args[1].as_str() {
"list" if args.len() == 2 => {
for task in &tasks {
let mark = if task.done { "x" } else { " " };
println!("{} [{mark}] {}", task.id, task.title);
}
}
"add" if args.len() == 3 => {
let id = tasks.iter().map(|t| t.id).max().unwrap_or(0)
.checked_add(1).ok_or("IDの上限です")?;
tasks.push(Task::new(id, &args[2])?);
save(path, tasks)?;
println!("追加: {id}");
}
"done" if args.len() == 3 => {
let id: u64 = args[2].parse()?;
let task = tasks.iter_mut().find(|t| t.id == id).ok_or("IDがありません")?;
task.complete();
save(path, tasks)?;
println!("完了: {id}");
}
_ => return Err("コマンドまたは引数が不正です".into()),
}
Ok(())
}
fn main() -> std::process::ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(()) => std::process::ExitCode::SUCCESS,
Err(error) => {
eprintln!("エラー: {error}");
std::process::ExitCode::FAILURE
}
}
}
cargo run -- tasks.json add "Rustを復習"、cargo run -- tasks.json done 1、cargo run -- tasks.json list で完了状態を確認します。二つのハイフン以降がアプリの引数です。
保存関数はVecの所有権を受け取り、呼び出し元が保存後に使わない設計です。所有権を渡す必要がない設計なら、借用する保存用の型も検討できます。一時ファイルの置き換えは同時更新の競合を解決せず、親ディレクトリの同期まで行っていないため、完全な停電耐性の例ではありません。
練習と解答
練習:存在しないIDと不正なJSONで、終了結果を確かめます。
解答:どちらもエラー表示と非0の終了コードになり、保存へ進みません。失敗を成功表示へ変えず、元のデータを維持する契約です。
公式資料
NamedTempFileとExitCodeが参照先です。
Rust全20回の目次 | 前の回 | 次の回