5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-08

お題

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

今回はRustを追加。

前提

  • Rustについてはビギナーレベル(※)の人間が書いています。

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

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

試行Index

実装・動作確認端末

# 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]アプリ起動エントリーポイント

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]ストア情報の管理

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]各コマンドの管理

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]各コマンドの親クラス

command.rs
pub trait Command {
    fn exec(&self, args: Vec<&str>);
}

各コマンドクラス

■保存

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 }));
    }
}

■1件取得

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);
        }
    }
}

■全件取得

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);
        }
    }
}

■1件削除

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));
    }
}

■全件削除

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

pub struct Clear {}

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

■ヘルプ

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);
    }
}

■アプリ終了

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. 一覧表示 〜 1件追加 〜 一覧表示

list

save key01 val01

list
{ key01, val01 }

4. キー指定して取得

get key01
val01

get key02

5. キー指定して削除

remove key01

get key01

list

6. 全件削除

save keyAAA valBBB

save キー1 バリュー2

list
{ keyAAA, valBBB }
{ キー1, バリュー2 }

clear

list

7. アプリ終了

end
End

$

まとめ

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

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

5
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?