簡単なツール作成を通してRustを学ぶ

# お題
以前、１つのお題に対して、いくつかのプログラミング言語で実装してみるという試みをした。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff
また、その改善編として、言語別にKeyValueストアもどきを作ってみた。
例えば、以下。

- [Ruby](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)
- [Scala](https://qiita.com/sky0621/items/a68ae8b64d4856d1a195)
- [Golang](https://qiita.com/sky0621/items/cb14aa67ef327922c0a1)

今回はRustを追加。

# 前提
- Rustについてはビギナーレベル（※）の人間が書いています。

※ 以下のチュートリアルを第１８章まで写経してみたレベル
https://doc.rust-jp.rs/book/second-edition/

- Rustで書いてみたという記事であり、Rustについていろいろ説明したものではないです。（そもそもまだ説明なんてできるレベルじゃない。）
- 別の言語と比較して、書きっぷりにちょっとした気付きくらいはあるかもしれません。

# 試行Index
- 第１回：[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)
- 第２回：[【改善編】簡単なツール作成を通してRubyを学ぶ](https://qiita.com/sky0621/items/5868b19934187571d0cd)
- 第３回：[【改善編】簡単なツール作成を通してPython3を学ぶ](https://qiita.com/sky0621/items/27c610e08391c7cc83ea)
- 第４回：[【改善編】簡単なツール作成を通してGolangを学ぶ](https://qiita.com/sky0621/items/cb14aa67ef327922c0a1)
- 第５回：[【改善編】簡単なツール作成を通してJavaを学ぶ](https://qiita.com/sky0621/items/f19f5191754f61a4d4c3)
- 第６回：[【改善編】簡単なツール作成を通してScalaを学ぶ](https://qiita.com/sky0621/items/a68ae8b64d4856d1a195)
- 第７回：[簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/86d9159bc1930618162c)
- 第８回：[【改善編】簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/a70cc7a9453795e0c9e9)

# 実装・動作確認端末
### # 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ファイルに保存する機能を持つコンソールアプリ。

### ソース全量
https://github.com/sky0621/book_rust/tree/v0.2.0

## 解説

### 全ソースファイル

|Rustソース|説明|
|---|---|
|main.rs|アプリ起動エントリーポイント|
|store_info.rs|キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。<br>現状は「ファイル名」だけ保持|
|commands.rs|キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。<br>コマンドの増減に関する影響は、このソースに閉じる。|
|command.rs|各コマンドに共通のインタフェース|
|save.rs|キーバリュー情報の保存を担う。|
|get.rs|指定キーに対するバリューの取得を担う。|
|list.rs|全キーバリュー情報の取得を担う。|
|remove.rs|指定キーに対するバリューの削除を担う。|
|clear.rs|全キーバリュー情報の削除を担う。|
|help.rs|ヘルプ情報の表示を担う。|
|end.rs|アプリの終了を担う。|

### 注釈
* `use`, `mod` は省略。

### [main.rs]アプリ起動エントリーポイント
```rust:main.rs
fn main() {
    let commands: Commands = Commands::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();

        commands.exec(seps);
    }
}
```

#### 簡易説明
`save`, `get`, `help` といったコマンド群は `Commands` で管理させている。

`loop` の無限ループ中で都度つど標準入力からの文字列インプット待ち。

受け取った文字列（例： `save key01 value01` ）を半角スペースで分割してコマンド処理役（= `Commands`）に渡す。

### [store_info.rs]ストア情報の管理
```rust:store_info.rs
use serde::{Deserialize, Serialize};

const STORE_FILE: &str = "store.json";

// アトリビュートにてJSONパーサーのシリアライズ機能を持たせる
#[derive(Serialize, Deserialize, Debug)]
pub struct StoreInfo {
    pub kvs: HashMap<String, String>,
}

pub fn re_write_store(json_str: String) {
    let mut file = match OpenOptions::new()
        .write(true)
        .truncate(true)
        .open(STORE_FILE)
    {
        Ok(f) => f,
        Err(e) => {
            eprintln!("{}", e);
            process::exit(-1);
        }
    };
    file.write(json_str.as_bytes()).unwrap();
}

pub fn read_store() -> String {
    fs::read_to_string(STORE_FILE).unwrap()
}

pub fn read_store_info() -> StoreInfo {
    let previous = read_store();
    if previous.is_empty() {
        return StoreInfo {
            kvs: HashMap::new(),
        };
    }
    serde_json::from_str(&previous).unwrap()
}

pub fn get_serialized(si: &StoreInfo) -> String {
    serde_json::to_string(si).unwrap()
}
```

#### 簡易説明
JSONの読み書きに `Serde` というライブラリ（Rustではクレートと言うらしい）を使用。
https://crates.io/crates/serde
https://serde.rs/

各種コマンドの中で使う、JSON文字列のファイル読み書きを行う関数を用意。

### [commands.rs]各コマンドの管理
```rust:commands.rs
pub struct Commands {
    pub commands: HashMap<&'static str, Box<dyn Command>>,
}

impl Commands {
    pub fn new() -> Commands {
        let mut commands: HashMap<&'static str, Box<dyn Command>> = HashMap::new();
        commands.insert(END, Box::new(End {}));
        commands.insert(HELP, Box::new(Help {}));
        commands.insert(CLEAR, Box::new(Clear {}));
        commands.insert(SAVE, Box::new(Save {}));
        commands.insert(GET, Box::new(Get {}));
        commands.insert(REMOVE, Box::new(Remove {}));
        commands.insert(LIST, Box::new(List {}));
        Commands { commands }
    }

    pub fn exec(&self, args: Vec<&str>) {
        if let Some(order) = args.get(0) {
            if let Some(cmd) = self.commands.get(order) {
                cmd.exec(args)
            }
        }
    }
}
```

#### 簡易説明
事前に各種コマンドをハッシュマップに保持しておいて、コマンド実行文字列が届いたら対応するコマンドを拾って実行。
まあ、よくあるコマンドパターン。

### [command.rs]各コマンドの親クラス
```rust:command.rs
pub trait Command {
    fn exec(&self, args: Vec<&str>);
}
```

### 各コマンドクラス

#### ■保存

```rust:save.rs
pub const SAVE: &str = "save";

pub struct Save {}

impl Command for Save {
    fn exec(&self, args: Vec<&str>) {
        if args.len() != 3 {
            return;
        }

        let mut kvs: HashMap<String, String> = HashMap::new();
        // 今回分をKVSに格納
        kvs.insert(
            args.get(1).unwrap().to_string(),
            args.get(2).unwrap().to_string(),
        );

        // 既存分を今回分に続けてKVSに格納
        for (k, v) in read_store_info().kvs {
            kvs.insert(k, v);
        }

        // JSONファイルに書き込み
        re_write_store(get_serialized(&StoreInfo { kvs }));
    }
}
```

#### ■１件取得

```rust:get.rs
pub const GET: &str = "get";

pub struct Get {}

impl Command for Get {
    fn exec(&self, args: Vec<&str>) {
        if args.len() != 2 {
            return;
        }

        // JSONファイルから既存分を取得
        let saved_si = read_store_info();

        if let Some(v) = saved_si.kvs.get(*args.get(1).unwrap()) {
            println!("{}", v);
        }
    }
}
```

#### ■全件取得

```rust:list.rs
pub const LIST: &str = "list";

pub struct List {}

impl Command for List {
    fn exec(&self, _: Vec<&str>) {
        // JSONファイルから既存分を取得
        let saved_si = read_store_info();

        for (k, v) in saved_si.kvs {
            println!("{{ {}, {} }}", k, v);
        }
    }
}
```

#### ■１件削除

```rust:remove.rs
pub const REMOVE: &str = "remove";

pub struct Remove {}

impl Command for Remove {
    fn exec(&self, args: Vec<&str>) {
        if args.len() != 2 {
            return;
        }

        // JSONファイルから既存分を取得
        let mut saved_si = read_store_info();

        // 既存分から指定された要素を削除
        saved_si.kvs.remove(*args.get(1).unwrap());

        // JSONファイルに書き込み
        re_write_store(get_serialized(&saved_si));
    }
}
```

#### ■全件削除

```rust:clear.rs
pub const CLEAR: &str = "clear";

pub struct Clear {}

impl Command for Clear {
    fn exec(&self, _: Vec<&str>) {
        re_write_store(String::from(""));
    }
}
```

#### ■ヘルプ

```rust:help.rs
pub const HELP: &str = "help";

pub struct Help {}

impl Command for Help {
    fn exec(&self, _: Vec<&str>) {
        let msg = r#"

[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。

save   ... keyとvalueを渡して保存します。
get    ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
list   ... 保存済みの内容を一覧表示します。
clear  ... 保存内容を全て削除します。
help   ... ヘルプ情報（当内容と同じ）を表示します。
end    ... プログラムを終了します。

    "#;
        println!("{}", msg);
    }
}
```

#### ■アプリ終了

```rust:end.rs
pub const END: &str = "end";

pub struct End {}

impl Command for End {
    fn exec(&self, _: Vec<&str>) {
        println!("End");
        process::exit(0);
    }
}
```

## 動作確認

### 1. 起動
```
$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.11s
     Running `target/debug/book_rust`

```

### 2. ヘルプ
```
help


[usage]
キーバリュー形式で文字列情報を管理するコマンドです。
以下のサブコマンドが利用可能です。

save   ... keyとvalueを渡して保存します。
get    ... keyを渡してvalueを表示します。
remove ... keyを渡してvalueを削除します。
list   ... 保存済みの内容を一覧表示します。
clear  ... 保存内容を全て削除します。
help   ... ヘルプ情報（当内容と同じ）を表示します。
end    ... プログラムを終了します。

```

### 3. 一覧表示　〜　１件追加　〜　一覧表示
```    
list

save key01 val01

list
{ key01, val01 }
```

### 4. キー指定して取得
```
get key01
val01

get key02

```

### 5. キー指定して削除
```
remove key01

get key01

list

```

### 6. 全件削除
```
save keyAAA valBBB

save キー１ バリュー２

list
{ keyAAA, valBBB }
{ キー１, バリュー２ }

clear

list

```

### 7. アプリ終了
```
end
End

$
```


# まとめ
いろいろ雑。ただ、これでも、そもそもコンパイル通すのだけでも一苦労で、かなり苦戦した。
チュートリアルを１回だけ写経しただけだと、「こういうふうに書いたらいけるだろう」がことごとくRustコンパイラにダメ出しされる。
でも、Rustコンパイラの出すメッセージ、けっこうやさしいので、それは嬉しい。

あと、それなりにリファクタはしたけど、Rustらしさはまだ全然わからない。。。