ぺんぎんらぼ

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です。たまに絵を描きます。

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です

RustでToDoアプリ②~jsonファイルから読み取ったデータをDtoにセット~

この記事はRustでCLIベースのToDoアプリを作っていくシリーズの第二回です。 第一回はこちら

今回はToDo用のデータを入れておくためのDtoを定義し、jsonファイルから読み取ったデータをそのDtoに設定する処理を書いていきましょう。

コード(main.rs)

use std::io;
use std::fs::File; // 追加
use std::io::BufReader; // 追加
use serde::*;// 追加

fn main() {

    println!("■■■■■ToDoリスト■■■■■");// 追加
    let todo_list = read_data("C:\\Users\\matsuki\\todo-app\\todo-data\\data.json"); // 追加
    println!("{:?}", todo_list); // 追加

    println!("▶ 実行したい内容を選択してください");
    println!("▶ 登録:0,編集:1,削除:2");
    let mut input_data = String::new();
    io::stdin()
        .read_line(&mut input_data)
        .expect("標準入力の読み込みに失敗しました");
    let trimmed_input = input_data.trim();
    println!("input_data is {}", trimmed_input);
    
}

/**
 * 追加
 */
#[derive(Debug, Deserialize)]//  (7)
struct ToDo {// (1)
    id: i32,
    name: String,
    deadline: String,
}

/**
 * 追加
 */
fn read_data(file_path: &str) -> Vec<ToDo> { //(2)
    let file = File::open(file_path); // (3)
    match file {
        Ok(f) => {
            let buf_reader = BufReader::new(f); // (4)
            serde_json::from_reader(buf_reader).expect("デシリアライズに失敗しました")//(5)
        }
        Err(_) => {//(6)
            panic!("ファイルが存在しませんでした。")
        }
    }
}

data.jsonの中身

[
  {
    "id": 1,
    "name": "牛乳を買う",
    "deadline": "2023-10-01"
  },
  {
    "id": 2,
    "name": "銀行に行く",
    "deadline": "2023-10-02"
  },
  {
    "id": 3,
    "name": "月見バーガーを食べる",
    "deadline": "2023-10-05"
  }
]

出力結果

■■■■■ToDoリスト■■■■■
[ToDo { id: 1, name: "牛乳を買う", deadline: "2023-10-01" }, ToDo { id: 2, name: "銀行に行く", deadline: "2023-10-02" }, ToDo { id: 3, name: "月見バーガーを食べる", deadline: "2023-10-05" }]
▶ 実行したい内容を選択してください
▶ 登録:0,編集:1,削除:2
0 <-- 値を入力
input_data is 0

解説(1)Rustの「構造体」について

struct ToDo {
    id: i32,
    name: String,
    deadline: String,
}
  • JavaでいうDtoやVOといった構造体データをRustで使用するにはstructキーワードを使用します。structはJavaのClassにあたるものです。Rustは色んな言語の特徴を併せ持っていますが、ここら辺はオブジェクト指向と同じです。
  • このToDo構造体は3つの「フィールド」(id、name、deadline)を持っており、フィールド名の後ろにコロンをつけて、i32(整数型)やString(文字列型)など型を書きます。
  • なお、Rustで「構造体」というと、狭義の意味と広義の二つの意味があります。狭義では今述べたDtoなどのユーザー定義オブジェクトを指します。一方で広義では、VecやHashMapなどのコレクション型も構造体と呼ばれます。
  • Rustの場合、メソッドはstructの中に定義せず別途implの中に書きます
  • 今回のコードでは割愛しましたが、以下のようにコンストラクタ的役割のメソッドを定義してインスタンスを生成する書き方もよく使われます。
//ToDo構造体
struct ToDo {
    id: i32,
    name: String,
    deadline: String,
}

impl ToDo {
    // コンストラクタ的役割のメソッド
    pub fn new(id: i32, name: String, deadline: String) -> Self {
        ToDo {
            id,
            name,
            deadline,
        }
    }
}

fn main() {
    // ToDo構造体のインスタンスを生成
    let todo = ToDo::new(1, String::from("Task 1"), String::from("2023-08-31"));

    // 生成したインスタンスを使用
    println!("ID: {}", todo.id);
    println!("Name: {}", todo.name);
    println!("Deadline: {}", todo.deadline);
}

解説(2)Rustのコレクション型について

fn read_data(file_path: &str) -> Vec<ToDo> {
  // 割愛
}
  • このread_data関数の戻り値型「Vec < ToDo >」はJavaでいう「ArrayList < ToDo > 」です。
  • JavaでHashMapやSet、LinkedListなど様々なコレクションAPIがあるようにRustにも様々なAPIがあります。詳細は公式ドキュメントを参照ください。

解説(3)Fileオブジェクトからの読み取り処理

    let file = File::open(file_path); // (3)
    match file {
        Ok(f) => {
            let buf_reader = BufReader::new(f);
            serde_json::from_reader(buf_reader).expect("デシリアライズに失敗しました")
        }
        Err(_) => {
            panic!("ファイルが存在しませんでした。")
        }
    }
  • std::fs::Fileはファイルに対して読み書き機能を提供するオブジェクトです(公式ドキュメント)。
  • ここでは、読み取り対象のファイルパスをString型で渡しFileオブジェクトを生成し、パスの指すファイルが存在しない場合はpanicを起こす(処理を中断する)ようにしています。
  • 上記のコードはmatch式とBufReaderが入っていて長いコードになっていますが、ファイル読み取り処理は最小3行で書くことができます。以下は公式ページのサンプルコードです。
    let mut file = File::open("foo.txt")?; // 【補足】
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
  • 【補足】 上記サンプルコードではエラーハンドリングにmatch式ではなく "?" 演算子が使われています。"?"演算子を使うとエラーは処理されず、そのまま呼び出し元にエラーが伝播します。なので呼び出し元の方でエラーを処理してあげる必要があります。成功時は単純にResult のOkバリアントから取得された値(つまりFileオブジェクト)が返されます。

解説(4)BufReaderを使おう

    let file = File::open(file_path);
    match file {
        Ok(f) => {
            let buf_reader = BufReader::new(f); // (4)
                 serde_json::from_reader(buf_reader).expect("デシリアライズに失敗しました")
        }
        Err(_) => {
            panic!("ファイルが存在しませんでした。")
        }
    }
  • ここでは、取得したFileオブジェクトから更にBufReaderオブジェクトを生成しています。RustのBufReaderはJavaのBufferedReaderと同じで、データをバッファに溜めつつ読み込むことで、ファイル読み取り操作のオーバーヘッドにかかるコストを低減します。
  • RustのBufReaderを使うことでファイルの内容を1文字ずつではなくデフォルトで8Kバイトずつ読み込みます。

解説(5)serde_json外部ライブラリクレート

let buf_reader = BufReader::new(f);
serde_json::from_reader(buf_reader).expect("デシリアライズに失敗しました")//(5)
  • serde_jsonクレートは、JSONデータのシリアライズ(データをJSON形式に変換すること)およびデシリアライズJSONデータをRustのデータ型に変換すること)をするライブラリグレーです。
  • 前回、ファイルの読み書き機能を提供する「std::ioクレート」について紹介しました。std::ioクレートはRustに標準で組み込まれているクレートです。その一方でserde_jsonは「外部」ライブラリクレートで、外部ライブラリクレートはRustコミュニティの開発者達が「便利なライブラリ作ったから使って」という体で提供しているサードパーティ製のライブラリになります。これらの外部ライブラリクレートは、コード中で使用するためにはcargo.tomlに依存関係を追記しておく必要があります。
# Cargo.toml
[dependencies]
serde_json = "1.0.105"
  • Rustの外部ライブラリクレートの公式サイトを紹介します。Rustコミュニティが作成したさまざまなクレートがここに登録されており、例えば "json" と検索し、直近でUpdateされた順にソートすると以下のような感じで数時間おきにアップされておりコミュニティの活発さを伺い知ることができます。

  • コードの話に戻ります。Rustの型推論は非常に強力で、serde_json::from_readerメソッドに戻り値型の情報(Vec < ToDo >)について何も渡していないにも関わらず、serde_jsonJSONファイルの内容を解析してToDo構造体に変換します。JSONファイルの内容とToDo構造体(structで定義したオブジェクトのフィールドメンバー)の内容が一致していない時はデシリアライズに失敗します。

解説(6)expectマクロ

    match file {
        Ok(f) => {
            let buf_reader = BufReader::new(f);
            serde_json::from_reader(buf_reader).expect("デシリアライズに失敗しました")
        }
        Err(_) => {
            panic!("ファイルが存在しませんでした。")
        }
    }
  • ここで、Fileのオープン処理では今回はエラーハンドリングにmatch式ではなくexpectを使っています。第一回の記事でも少し触れました。
  • expectは、エラーが発生した場合に開発者が指定したエラーメッセージを表示してプログラムをパニック状態に移行させます。この通り、expectはエラーが発生した際にプログラムを停止させてしまうため、エラーハンドリング方法は用途に応じて使い分ける必要があります。ここでは処理の見通しをよくするためにexpectを使っています。
  • 参考までに今回のexpectを使っているコードをmatch式に置き換えると以下のようになります。
fn read_data(file_path: &str) -> Vec<ToDo> {
    let file = File::open(file_path);

    match file {
        Ok(f) => {
            let buf_reader = BufReader::new(f);
            match serde_json::from_reader(buf_reader) {
                Ok(data) => data,
                Err(_) => Vec::new(),
            }
        }
        Err(_) => {
            panic!("ファイルが存在しませんでした。");
        }
    }
}

解説(7)derive属性

#[derive(Deserialize,Debug)]//  (7)
struct ToDo {
  • Rustの「derive属性」はJavaアノテーションにあたるものです。ここでは構造体をデシリアライズできるようにDesirialize値を、構造体をデバッグ出力できるようにDebug値を設定しています。
  • Debugは、main.rsの10行目で「println!("{:?}", todo_list); 」としてToDo構造体のリストの内容を出力するために必要です。
  • JavalombokやJacksonなどでアノテーションをクラス宣言に付与することによって、コンパイル時や実行時の振る舞いをカスタマイズできるのと同じです。Javaで@SerializableとDtoに付与することでオブジェクトのシリアライズ(直列化)とデシリアライズが可能になるように、ここではserdeライブラリと組み合わせて、struct構造体にDeserialize属性を付与することで、開発者が頑張ってデシリアライズのコードを書かずとも、デシリアライズができるようにしてくれています。
  • Javaアノテーションと同じようにRustではマクロを定義してderive属性の値に設定することで、開発者が振る舞いをカスタマイズすることが可能です。公式ドキュメント

おわり

第二回の今回は、ToDo用のstruct構造体や、ファイルからのデータの読み取り方を解説しました。次回はコレクションの操作やファイル書き込み処理のコードを書いていきまましょう。