Yomiage
メニュー

Rust実践講座 18:総合演習・保存とCLIを完成させる

更新 2026/9/27 6:22:11 公開

投稿者: しゅりりん

第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回の目次 | 前の回 | 次の回

このブラウザでは Web Speech API が利用できません。

1 / 13 · 停止中