お題
以下の記事の改善編。
「簡単なツール作成を通してRustを学ぶ」
コンソールアプリという形で簡易的なキーバリューストアを作ったのだけど、標準入力からコマンドを受け付けるたびにJSONファイルに結果を読み書きしている。
所詮コンソールアプリだし、そんなに巨大なJSONファイルになる想定もないけど、まあ、JSONファイルの読み書きはアプリ起動時と終了時だけでよいよねということで、改修。
アプリ起動時にJSONファイル(なければ作成)から前回アプリ終了時のキーバリュー情報をオンメモリに読み込み。
アプリ起動中は標準入力から受け付けたコマンド群の実行結果をオンメモリのキーバリューストアに読み書き。
アプリ終了時にJSONファイルにオンメモリの情報を書き込み。
当然ながら、副作用は「予期せぬアプリ終了時にオンメモリのキーバリュー読み書き情報が消える」こと。
ちゃんとしたアプリならそのへんもケア(例えば、一定周期、ないし、OSシグナル受信時にオンメモリの情報をJSONファイルに強制書き込みする処理を入れるとか)すると思う。
が、今回はちょっとしたツール作成というレベルなので、そこまではやらない。
前提
- Rustについてはビギナーレベル(※)の人間が書いています。
※ 以下のチュートリアルを第18章まで写経してみたレベル
https://doc.rust-jp.rs/book/second-edition/
- Rustで書いてみたという記事であり、Rustについていろいろ説明したものではないです。(そもそもまだ説明なんてできるレベルじゃない。)
- 別の言語と比較して、書きっぷりにちょっとした気付きくらいはあるかもしれません。
- 前回の記事と比較すると、ちょっとした気付きくらいはあるかもしれません。
試行Index
- 第1回:簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ
- 第2回:【改善編】簡単なツール作成を通してRubyを学ぶ
- 第3回:【改善編】簡単なツール作成を通してPython3を学ぶ
- 第4回:【改善編】簡単なツール作成を通してGolangを学ぶ
- 第5回:【改善編】簡単なツール作成を通してJavaを学ぶ
- 第6回:【改善編】簡単なツール作成を通してScalaを学ぶ
- 第7回:簡単なツール作成を通してRustを学ぶ
実装・動作確認端末
# OS - Linux
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
# 言語バージョン
$ rustc --version
rustc 1.46.0 (04488afe3 2020-08-24)
$
$ cargo --version
cargo 1.46.0 (149022b1d 2020-07-17)
# IDE - IntelliJ IDEA
IntelliJ IDEA 2020.2.1 (Ultimate Edition)
Build #IU-202.6948.69, built on August 25, 2020
実践
要件
アプリを起動すると、キーバリュー形式でテキスト情報をJSONファイルに保存する機能を持つコンソールアプリ。
ソース全量
解説
全ソースファイル
Rustソース | 説明 |
---|---|
main.rs | アプリ起動エントリーポイント |
store_info.rs | キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。 |
commands.rs | キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。 コマンドの増減に関する影響は、このソースに閉じる。 |
command.rs | 各コマンドに共通のインタフェース |
save.rs | キーバリュー情報の保存を担う。 |
get.rs | 指定キーに対するバリューの取得を担う。 |
list.rs | 全キーバリュー情報の取得を担う。 |
remove.rs | 指定キーに対するバリューの削除を担う。 |
clear.rs | 全キーバリュー情報の削除を担う。 |
help.rs | ヘルプ情報の表示を担う。 |
end.rs | アプリの終了を担う。 |
注釈
-
use
,mod
は省略。
[main.rs]アプリ起動エントリーポイント
fn main() {
// オンメモリ用のキーバリューストア
// 前回アプリ起動時の読み書き情報をJSONファイルから読み込む。
let mut si = StoreInfo::new();
loop {
// 標準入力の受け皿
let mut input = String::new();
// 標準入力から input へ
stdin().read_line(&mut input).unwrap();
input.retain(|c| c != '\n'); // 改行コードの除去
// 半角スペースで分割
let seps: Vec<&str> = input.split_ascii_whitespace().collect();
run(seps, &mut si);
}
}
簡易説明
前回と違い、コマンド群の管理構造体は作らない。
run()
の中で標準入力で支持されたコマンドに合致する処理を見つけてパラメータを渡して実行。
アプリ起動・終了時以外はJSONファイルへの読み書きは行わず、キーバリューストアはオンメモリ(変数名 si
)で管理。
コマンド実行の都度、si
に読み書き。
[store_info.rs]ストア情報の管理
use serde::{Deserialize, Serialize};
// アプリ起動・終了時にオンメモリのキーバリュー情報を読み書きするためのJSONファイル
const STORE_FILE: &str = "store.json";
// アトリビュートにてJSONパーサーのシリアライズ機能を持たせる
#[derive(Serialize, Deserialize, Debug)]
pub struct StoreInfo {
pub kvs: HashMap<String, String>,
}
impl StoreInfo {
pub fn new() -> StoreInfo {
// 未作成ならJSONファイルを作っておく。
if !Path::new(STORE_FILE).exists() {
File::create(STORE_FILE).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
process::exit(-1);
});
}
// 前回アプリ起動時のキーバリュー情報をオンメモリ保持
let previous = fs::read_to_string(STORE_FILE).unwrap();
if previous.is_empty() {
return StoreInfo {
kvs: HashMap::new(),
};
}
serde_json::from_str(&previous).unwrap()
}
}
pub fn write_store(si: &StoreInfo) {
let json_str = serde_json::to_string(&si).unwrap();
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(STORE_FILE)
.unwrap_or_else(|e| {
eprintln!("{:#?}", e);
process::exit(-1);
});
file.write(json_str.as_bytes()).unwrap_or_else(|e| {
eprintln!("{:#?}", e);
process::exit(-1);
});
}
簡易説明
JSONの読み書きに Serde
というライブラリ(Rustではクレートと言うらしい)を使用。
https://crates.io/crates/serde
https://serde.rs/
アプリ起動・終了時に使う、JSON文字列のファイル読み書きを行う関数を用意。
[commands.rs]各コマンドの管理
// 各コマンド生成時に重い処理をしているわけではないので、
// いったん構造体にコマンド群を保持するのは止めて、標準入力を受け取るたびに
// 都度、実行コマンド判定、及びコマンド実行させる。
pub fn run(args: Vec<&str>, si: &mut StoreInfo) {
// 標準入力から何かしらの指示があれば発動
if let Some(order) = args.get(0) {
// 指示に応じたコマンドのピックアップ
let mut c: Box<dyn Command> = match *order {
"end" => Box::new(End::new(si)),
"clear" => Box::new(Clear::new(si)),
"save" => Box::new(Save::new(si)),
"get" => Box::new(Get::new(si)),
"remove" => Box::new(Remove::new(si)),
"list" => Box::new(List::new(si)),
// 規定外の指示にはヘルプ表示
_ => Box::new(Help {}), // ヘルプ表示にはキーバリューストアは不要
};
c.exec(args)
}
}
[command.rs]各コマンドの親クラス
// 各種コマンドの処理を統一的に扱うI/Oを規定
pub trait Command {
// args ... 標準入力から受け取ったコマンド及びパラメータ群
fn exec(&mut self, args: Vec<&str>);
}
各コマンドクラス
■保存
pub struct Save<'a> {
si: &'a mut StoreInfo,
}
impl<'a> Save<'a> {
pub fn new(si: &'a mut StoreInfo) -> Save<'a> {
Save { si }
}
}
impl<'a> Command for Save<'a> {
fn exec(&mut self, args: Vec<&str>) {
if let (Some(key), Some(val)) = (args.get(1), args.get(2)) {
self.si.kvs.insert(key.to_string(), val.to_string());
}
}
}
■1件取得
pub struct Get<'a> {
si: &'a mut StoreInfo,
}
impl<'a> Get<'a> {
pub fn new(si: &'a mut StoreInfo) -> Get<'a> {
Get { si }
}
}
impl<'a> Command for Get<'a> {
fn exec(&mut self, args: Vec<&str>) {
if let Some(key) = args.get(1) {
// clone() しないと以下のようにRustコンパイラに怒られるが、理屈がわかっていない。
// 頻繁に出会うコンパイルエラーの内容はそのうちまとめてみよう。
// 「[E0277]: the trait bound `std::string::String: std::borrow::Borrow<&str>` is not satisfied」
if let Some(v) = self.si.kvs.get(key.clone()) {
println!("{}", v);
}
}
}
}
■全件取得
pub struct List<'a> {
si: &'a mut StoreInfo,
}
impl<'a> List<'a> {
pub fn new(si: &'a mut StoreInfo) -> List<'a> {
List { si }
}
}
impl<'a> Command for List<'a> {
fn exec(&mut self, _: Vec<&str>) {
for (k, v) in &self.si.kvs {
println!("{{ {}, {} }}", k, v);
}
}
}
■1件削除
pub struct Remove<'a> {
si: &'a mut StoreInfo,
}
impl<'a> Remove<'a> {
pub fn new(si: &'a mut StoreInfo) -> Remove<'a> {
Remove { si }
}
}
impl<'a> Command for Remove<'a> {
fn exec(&mut self, args: Vec<&str>) {
if let Some(key) = args.get(1) {
self.si.kvs.remove(key.clone());
}
}
}
■全件削除
pub struct Clear<'a> {
si: &'a mut StoreInfo,
}
impl<'a> Clear<'a> {
pub fn new(si: &'a mut StoreInfo) -> Clear<'a> {
Clear { si }
}
}
impl<'a> Command for Clear<'a> {
fn exec(&mut self, _: Vec<&str>) {
self.si.kvs = HashMap::new();
}
}
■ヘルプ
pub struct Help {}
impl Command for Help {
fn exec(&mut self, _: Vec<&str>) {
let msg = r#"
[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。
save ... keyとvalueを渡して保存します。
get ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
list ... 保存済みの内容を一覧表示します。
clear ... 保存内容を全て削除します。
help ... ヘルプ情報(当内容と同じ)を表示します。
end ... プログラムを終了します。
"#;
println!("{}", msg);
}
}
■アプリ終了
pub struct End<'a> {
si: &'a StoreInfo,
}
impl<'a> End<'a> {
pub fn new(si: &'a StoreInfo) -> End<'a> {
End { si }
}
}
impl<'a> Command for End<'a> {
fn exec(&mut self, _: Vec<&str>) {
// キーバリュー読み書き結果をJSONファイルに保存
write_store(&self.si);
// アプリ終了
process::exit(0);
}
}
動作確認
前回と変わっていないので省略。
https://qiita.com/sky0621/items/86d9159bc1930618162c#動作確認
まとめ
モダンなだけに書いていて楽しいRust。でも、書けば書くほど、Rustビギナーが必ずハマるという「借用チェッカとの戦い」に明け暮れることになる。
深く理解せぬままRustコンパイラのメッセージに対処していくと、気づくとモグラ叩きのようになっているという・・・。
座学で理解した気分になり、実践で打ちのめされ、再び座学という流れを、ここ数日繰り返している気がする。
でも、楽しい。