Rustで構築する最終プロジェクト:学んだ知識をまとめよう

これまでにRustの基本文法から高度なトピックまで学び、様々なプログラムを作成するスキルを身に付けてきました。

最終回では、学んだ知識を活かして実際にRustを使ったプロジェクトを構築してみましょう。

このプロジェクトでは、基本的なRustの文法、モジュール、マクロ、そして外部ライブラリの活用など、Rustの主要な機能を総合的に活用します。

今回は、シンプルなタスク管理アプリをRustで構築します。

このアプリでは、タスクの追加、削除、リスト表示といった操作をコマンドラインから実行できるようにします。


プロジェクト概要

タスク管理アプリの機能

  • タスクの追加
  • タスクの一覧表示
  • タスクの削除
  • タスクの完了ステータスの更新

使用するRustの要素

  • 構造体(Struct): タスクデータを保持するために使用。
  • 列挙型(Enum): タスクのステータス(完了/未完了)を表現。
  • モジュールシステム: コードを整理して管理。
  • 外部ライブラリ: serde クレートを使ってタスクデータをJSON形式で保存・読み込み。
  • CLI引数処理: コマンドラインからタスクの操作を行うために std::env を使用。

プロジェクトの準備

まず、新しいRustプロジェクトを作成します。

$ cargo new task_manager
$ cd task_manager

次に、Cargo.toml に以下の依存関係を追加します。

serdeserde_json は、タスクデータをJSON形式で保存・読み込むために使用します。

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

セクション2: 構造体と基本機能の実装

次に、タスクを表現する構造体と、そのステータスを表す列挙型を定義します。

また、タスクのリストを管理するための機能を実装します。

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Task {
    pub id: u32,
    pub description: String,
    pub status: TaskStatus,
}

#[derive(Serialize, Deserialize, Debug)]
pub enum TaskStatus {
    Pending,
    Completed,
}

impl Task {
    pub fn new(id: u32, description: String) -> Self {
        Self {
            id,
            description,
            status: TaskStatus::Pending,
        }
    }

    pub fn complete(&mut self) {
        self.status = TaskStatus::Completed;
    }
}

タスクの新規作成

新しいタスクを作成する関数 new を実装しました。TaskStatusPending (未完了)と Completed(完了)という2つのステータスを持ちます。


コマンドライン引数の処理

次に、コマンドライン引数を処理し、タスクを追加、表示、削除できるようにします。

std::env::args を使って、コマンドライン引数を取得します。

mod task;

use task::{Task, TaskStatus};
use std::env;
use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use serde_json::Result;

const FILE_PATH: &str = "tasks.json";

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage: task_manager <add|list|complete|delete>");
        return;
    }

    let command = &args[1];
    match command.as_str() {
        "add" => {
            if args.len() < 3 {
                eprintln!("Usage: task_manager add <description>");
                return;
            }
            let description = &args[2];
            add_task(description.to_string());
        }
        "list" => list_tasks(),
        "complete" => {
            if args.len() < 3 {
                eprintln!("Usage: task_manager complete <task_id>");
                return;
            }
            let task_id: u32 = args[2].parse().expect("Task ID should be a number");
            complete_task(task_id);
        }
        "delete" => {
            if args.len() < 3 {
                eprintln!("Usage: task_manager delete <task_id>");
                return;
            }
            let task_id: u32 = args[2].parse().expect("Task ID should be a number");
            delete_task(task_id);
        }
        _ => eprintln!("Unknown command: {}", command),
    }
}

タスクの保存と読み込み

タスクのデータをJSON形式で保存・読み込みするために、serdeserde_json を活用します。

タスクのデータは tasks.json ファイルに保存されます。

タスクをJSONファイルに保存する関数

fn save_tasks(tasks: &[Task]) {
    let serialized = serde_json::to_string(tasks).expect("Failed to serialize tasks");
    let mut file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(FILE_PATH)
        .expect("Failed to open tasks file");
    file.write_all(serialized.as_bytes()).expect("Failed to write tasks to file");
}

タスクをファイルから読み込む関数

fn load_tasks() -> Vec<Task> {
    if let Ok(mut file) = OpenOptions::new().read(true).open(FILE_PATH) {
        let mut content = String::new();
        file.read_to_string(&mut content).expect("Failed to read tasks file");
        if let Ok(tasks) = serde_json::from_str(&content) {
            return tasks;
        }
    }
    Vec::new()
}

タスク操作の実装

最後に、タスクの追加、一覧表示、削除、完了を処理する関数を実装します。

タスクの追加

fn add_task(description: String) {
    let mut tasks = load_tasks();
    let new_id = tasks.len() as u32 + 1;
    let new_task = Task::new(new_id, description);
    tasks.push(new_task);
    save_tasks(&tasks);
    println!("タスクを追加しました!");
}

タスクの一覧表示

fn list_tasks() {
    let tasks = load_tasks();
    for task in tasks {
        println!("ID: {}, 内容: {}, ステータス: {:?}", task.id, task.description, task.status);
    }
}

タスクの完了

fn complete_task(task_id: u32) {
    let mut tasks = load_tasks();
    if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
        task.complete();
        save_tasks(&tasks);
        println!("タスク {} を完了しました!", task_id);
    } else {
        println!("タスクが見つかりませんでした。");
    }
}

タスクの削除

fn delete_task(task_id: u32) {
    let mut tasks = load_tasks();
    tasks.retain(|task| task.id != task_id);
    save_tasks(&tasks);
    println!("タスク {} を削除しました!", task_id);
}

まとめ

今回の最終回では、Rustを使ったシンプルなタスク管理アプリの開発を通じて、これまで学んだ知識を総合的に活用しました。

Rustの構造体、列挙型、モジュールシステム、外部ライブラリの使い方を理解し、実際にコマンドラインツールを構築することで、Rustの実践力がさらに高まったかと思います。


これまで学んだことを活かして、今後のRustプログラミングをさらに楽しんでください!

最終コード

Cargo.toml

[package]
name = "task_manager"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

src/main.rs

mod task;

use task::{Task, TaskStatus};
use std::env;
use std::fs::{OpenOptions};
use std::io::{Read, Write};

const FILE_PATH: &str = "tasks.json";

fn main() {
    let args: Vec<String> = env::args().collect();

    if args.len() < 2 {
        eprintln!("Usage: task_manager <add|list|complete|delete>");
        return;
    }

    let command = &args[1];
    match command.as_str() {
        "add" => {
            if args.len() < 3 {
                eprintln!("Usage: task_manager add <description>");
                return;
            }
            let description = &args[2];
            add_task(description.to_string());
        }
        "list" => list_tasks(),
        "complete" => {
            if args.len() < 3 {
                eprintln!("Usage: task_manager complete <task_id>");
                return;
            }
            let task_id: u32 = args[2].parse().expect("Task ID should be a number");
            complete_task(task_id);
        }
        "delete" => {
            if args.len() < 3 {
                eprintln!("Usage: task_manager delete <task_id>");
                return;
            }
            let task_id: u32 = args[2].parse().expect("Task ID should be a number");
            delete_task(task_id);
        }
        _ => eprintln!("Unknown command: {}", command),
    }
}

fn save_tasks(tasks: &[Task]) {
    let serialized = serde_json::to_string(tasks).expect("Failed to serialize tasks");
    let mut file = OpenOptions::new()
        .write(true)
        .create(true)
        .truncate(true)
        .open(FILE_PATH)
        .expect("Failed to open tasks file");
    file.write_all(serialized.as_bytes()).expect("Failed to write tasks to file");
}

fn load_tasks() -> Vec<Task> {
    if let Ok(mut file) = OpenOptions::new().read(true).open(FILE_PATH) {
        let mut content = String::new();
        file.read_to_string(&mut content).expect("Failed to read tasks file");
        if let Ok(tasks) = serde_json::from_str(&content) {
            return tasks;
        }
    }
    Vec::new()
}

fn add_task(description: String) {
    let mut tasks = load_tasks();
    let new_id = tasks.len() as u32 + 1;
    let new_task = Task::new(new_id, description);
    tasks.push(new_task);
    save_tasks(&tasks);
    println!("タスクを追加しました!");
}

fn list_tasks() {
    let tasks = load_tasks();
    for task in tasks {
        println!("ID: {}, 内容: {}, ステータス: {:?}", task.id, task.description, task.status);
    }
}

fn complete_task(task_id: u32) {
    let mut tasks = load_tasks();
    if let Some(task) = tasks.iter_mut().find(|t| t.id == task_id) {
        task.complete();
        save_tasks(&tasks);
        println!("タスク {} を完了しました!", task_id);
    } else {
        println!("タスクが見つかりませんでした。");
    }
}

fn delete_task(task_id: u32) {
    let mut tasks = load_tasks();
    tasks.retain(|task| task.id != task_id);
    save_tasks(&tasks);
    println!("タスク {} を削除しました!", task_id);
}

Rustプロジェクトで使用するライブラリが、Cargo.toml ファイルに適切に記述されているかを確認する。

Rustプロジェクトでは、外部のライブラリやクレート(Rustのパッケージ)を使用する際に、依存関係を**Cargo.toml** という設定ファイルに記述します。

このファイルに依存関係を追加することで、cargo コマンドを使って外部ライブラリを自動でダウンロードし、プロジェクトで使えるようになります。

依存関係とは?

依存関係とは、プロジェクト内で使いたい外部のライブラリやツールのことです。例えば、今回のタスク管理アプリでは、タスクのデータをJSON形式で保存・読み込むために、以下の2つのクレートを使います。

  1. serde: データをシリアライズ(データ構造を保存可能な形式に変換)するためのライブラリ。
  2. serde_json: JSONフォーマットのシリアライズとデシリアライズ(データ構造に戻す)を行うためのライブラリ。

Cargo.toml の設定例

以下は、依存関係として serdeserde_json を追加した Cargo.toml ファイルの例です。プロジェクトでこれらのライブラリを使用するために、このように記述します。

[package]
name = "task_manager"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

意味:

  • serde = { version = "1.0", features = ["derive"] }: serde バージョン1.0を使用し、derive 機能を有効にしています。この機能により、Rustの構造体や列挙型に対して、自動的にシリアライズやデシリアライズを行うコードを生成できます。
  • serde_json = "1.0": serde_json バージョン1.0を使用し、データをJSON形式で操作できるようにしています。

確認方法:

プロジェクトの Cargo.toml ファイルに、上記のような依存関係が記述されていれば、serdeserde_json を正しく使用するための準備ができています。その後、以下のコマンドで依存関係をインストールします。

$ cargo build

これで、serdeserde_json がインストールされ、プロジェクトで使用できるようになります。

task_manager/
├── Cargo.toml
├── src/
│   ├── main.rs
│   └── task.rs
└── tasks.json  (このファイルはタスクが保存されると自動生成されます)

src/task.rs

  • タスクの構造体と、その操作に関するロジックが定義されています。
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct Task {
    pub id: u32,
    pub description: String,
    pub status: TaskStatus,
}

#[derive(Serialize, Deserialize, Debug)]
pub enum TaskStatus {
    Pending,
    Completed,
}

impl Task {
    pub fn new(id: u32, description: String) -> Self {
        Self {
            id,
            description,
            status: TaskStatus::Pending,
        }
    }

    pub fn complete(&mut self) {
        self.status = TaskStatus::Completed;
    }
}

tasks.json

  • タスクデータを保存するファイルです。このファイルは、タスクを追加すると自動的に生成されます。手動で作成する必要はありませんが、初めてタスクを保存するときに作成されます。