タスク モジュールを作成する
tasks
モジュールは、タスクと、それを保存してアクセスする方法を表します。
src ディレクトリに tasks.rs
という名前の新しいファイルを作成します。 そのファイルの中に、まず、プログラムで to-do 項目がどのように表示されるかを表す簡単な構造体を定義します。
use chrono::{DateTime, Utc};
#[derive(Debug)]
pub struct Task {
pub text: String,
pub created_at: DateTime<Utc>,
}
この構造体には、次の 2 つのフィールドがあります。
text
には、"pay the bills"
のようにタスクの説明が格納されます。created_at
には、タスクの作成のタイムスタンプが格納されます。
to-do リストをタスクのベクトル (Vec<Task>
) として表すため、status
または is_complete
フィールドは追加しません。 そのため、タスクが完了したら、ベクトルから削除することができます。
サードパーティー製クレート chrono
を使用していることにお気づきでしょうか。 DateTime
構造体に Utc
パラメーターを指定しました。 chrono
は、Rust で日付と時刻のデータを処理する必要がある場合に使用するのに適したクレートです。 これには、ある時点を表す簡単な API が用意されています。
これを使用しているので、Cargo.toml
ファイルで宣言する必要があります。
[dependencies]
chrono = "0.4"
structopt = "0.3"
次の手順は、新しいタスクをインスタンス化するためのメソッドを実装することです。 タスクには、常に現在の日付と時刻のタイムスタンプが付けられます。 次のコードを Task
構造体の後に追加します。
impl Task {
pub fn new(text: String) -> Task {
let created_at: DateTime<Utc> = Utc::now();
Task { text, created_at }
}
}
このコードには Task::new
関数が定義されています。 この関数に必要なものは、タスクの説明のみです。 Utc::now()
メソッドを使用して、現在のタイムスタンプをキャプチャします。
これでタスクの構造体は完成したようです。 さあ、このモジュールの次の項目 "永続化" に取り組みましょう。
to-do リストをタスクのベクトルとして表すため、JSON ファイルを使用してデータを簡単に永続化することができます。 これを実現するには、Rust エコシステムの別の優れたクレート serde_json
を使用することをお勧めします。
serde_json
を使用したタスクのシリアル化と逆シリアル化
次に進む前に、Rust でのエンコードとデコードに関して推奨される方法を説明しておきます。
構造体と列挙型インスタンスを永続化する必要がある場合は、"シリアル化" について考える必要があります。 そのデータをプログラムに戻す必要があるときは、"逆シリアル化" について話します。
シリアル化と逆シリアル化は、データをバイト ストリームに格納し、情報を失うことなく、後で使用できるようにデータを取得するプロセスです。 これにより、接続を介してこのようなバイトを送信し、ストレージ デバイスのファイルに保存できるようになります。 シリアル化と逆シリアル化の詳細については、この OWASP チートシートを参照してください。
Rust コミュニティでは、Rust データ構造のほとんどのシリアル化と逆シリアル化を効率的かつ汎用的に処理するために serde
クレートを推奨しています。 この既存のクレートを使用することで、生産性と慣用性をさらに高めることができます。
Task
型のシリアル化を始めるには、次の 2 つのクレートが必要です。
serde
。 型からSerialize
とDeserialize
の特性を派生できるようにするベース クレート。serde_json
。 これらの特性を、選択したファイル仕様形式である JSON に実装するクレート。
いつものように、最初の手順は、Cargo.toml
ファイルの [dependencies]
セクションに serde_json
と serde
を含めることです。 今回は、いくつかの serde
機能を条件付きでコンパイルする必要があるため、別の表記法を使用して指定します。 ファイルは次のようになります。
[dependencies]
serde_json = "1.0" # Add serde_json.
structopt = "0.3"
[dependencies.chrono]
features = ["serde"] # We're also going to need the serde feature for the chrono crate, so we can serialize the DateTime field.
version = "0.4"
[dependencies.serde] # Add serde in its own section.
features = ["derive"] # We'll need the derive feature.
version = "1.0"
これで、Task
構造体を調整して serde
の新しい機能を使用できるようになりました。 tasks.rs
ファイルを開き、構造体を次のように変更します。
use chrono::{serde::ts_seconds, DateTime, Local, Utc};
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Deserialize, Serialize)]
pub struct Task {
pub text: String,
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
}
違いに注意してください。
- 実装する特性のリストに
Deserialize
とSerialize
を追加しました。 ts_seconds
をchrono
からserde(with = ...)
属性に渡してcreated_at
フィールドに注釈を付けました。これで、chrono
からserde
に対して、Datetime
型によって 2 つの新しい特性がどのように実装されるかを通知できるようになりました。
Task
型を使用してシリアル化と逆シリアル化の両方を実行できるようになったので、次に進み、ファイル処理関数を実装することができます。
ファイル システムと対話する
プログラムから実行する必要のある 3 つのアクションを確認しましょう。
- to-do リストに新しいタスクを追加する。
- そのリストから完了したタスクを削除する。
- リスト内の現在のすべてのタスクを出力する。
モジュール インターフェイスは、そのリストと同じくらい単純であるべきです。そのため、アクションごとに 1 つずつ、3 つの関数を使用します。
use std::io::Result;
use std::path::PathBuf;
pub fn add_task(journal_path: PathBuf, task: Task) -> Result<()> { ... }
pub fn complete_task(journal_path: PathBuf, task_position: usize) -> Result<()> { ... }
pub fn list_tasks(journal_path: PathBuf) -> Result<()> { ... }
まず、各関数のシグネチャを見てみましょう。 これらのいずれも journal_path: PathBuf
引数が必須であることに注目してください。 これは、いずれも作業を完了するにはファイルのパス (タスクが格納されるファイルのパス) が必要なためです。
add_task
は、Task
引数も必須です。 この引数に、リストに追加されるタスクを指定します。complete_task
は、削除するTask
を示すtask_position
引数が必須です。 タスクが削除されると、それが完了したことを意味します。list_tasks
に追加の情報は必要ありません。 ジャーナル ファイルに現在格納されているすべてのタスクが、整形された形式でユーザーに表示されるだけです。
いずれの関数も、戻り値の型は同じ std::io::Result<()>
です。 この形式は、戻り値の型が I/O の結果であることを示します。 この戻り値の型は、物理的なワード内のデータを処理するときに発生する可能性のある、さまざまな望ましくない結果が予想されることを示しています。 Ok
バリアントは空のタプル ()
です。通常、これはデータがまったく関連付けられていない型です。 その唯一の目的は、関数から Ok
が返され、エラーが発生しなかったことを通知することです。
次の 3 つのユニットでは、各関数の内容の記述について詳しく説明します。