inFablic | Fablic, inc. Developer's Blog.

フリマアプリ フリル (FRIL) を運営する Fablic の公式開発者ブログです。Fablic のデザイナー・エンジニア・ディレクターが情報発信していきます。

Rustで文字列を操作する

f:id:kazu9su:20171001213822p:plain

こんにちは、サーバーサイドエンジニアのtommyです。

この記事では、Rustで文字列操作する実装例を紹介したいと思います。

はじめに

Fablicでは、一部リポジトリでのプルリクエストをテンプレート化し、git tagを切るのを自動化しています。

in.fablic.co.jp

テンプレートをパースし、新しいバージョンを指定してgitタグを切るという一連の処理をRubyで書いていましたが、Rustで書き直しました。
その理由としては、herokuで上記スクリプトを実行すると、gemを使用していたため bundle install を行う必要があり、その分時間がかかってしまっていたということが挙げられます。
この問題はコンパイル済みのバイナリファイルを用意すれば解決できます。
個人的にRustを使ってみていた*1ということと、
FRILサービス本体とは独立して存在するコンポーネントであったので試験的に導入するには適しており、かつチームでの同意も一瞬で取れたのでRustを使用することにしました 。

操作する文字列

対象の文字列は下記のようななものです。

## Changelog

changelog format

### Content

* Modify: hogehoge

### Change level

Choose one:

* [ ] MAJOR change
* [x] MINOR change
* [ ] PATCH change

---

標準入力から文字列を受け取る

標準入力から文字列を受け取るには、下記のようにします。

    let mut buffer = String::new();
    let stdin = std::io::stdin();
    let mut handle = stdin.lock();

    handle.read_to_string(&mut buffer).expect("something went wrong reading STDIN");

bufferという変数に標準入力の値を受け取っています。 読み込みがうまくいかなかった場合、expectで指定した文字列を基にpanicが生成されます。

パターンマッチしながら行をパースする

次に、パターンマッチを利用して必要な情報を変数に入れていきます。

    let mut lines = buffer.lines();
    let mut state = String::from("pre_changelog");
    let mut contents: Vec<&str> = Vec::new();
    let mut change_levels: Vec<&str> = Vec::new();

    while let Some(line) = lines.next() {
        match state.as_ref() {
            "pre_changelog" =>
                match line.as_ref() {
                    "## Changelog" =>
                        state = String::from("changelog"),
                    _ => panic!("something else hoge!"),
                },
            "changelog" =>
                match line.as_ref() {
                    "### Content" =>
                        state = String::from("content"),
                    "### Change level" =>
                        state = String::from("change_level"),
                     _ => continue,
                },
            "content" =>
                match line.as_ref() {
                    "### Change level" =>
                        state = String::from("change_level"),
                    _ => contents.push(line),
                },
            "change_level" =>
                match line.as_ref() {
                    "---" => break,
                    _ => change_levels.push(line),
                },
             _ => {
                panic!("state is wrong!");
             }
        }
    }

&str 型のVectorを定義し、任意の行(文字列)を挿入していっています。 実行後、 change_levelscontents には下記のような値が入っています。

change_levels = [
    "",
    "Choose one:",
    "* [ ] MAJOR change",
    "* [x] MINOR change",
    "* [ ] PATCH change",
    "",
];

contents = [
    "",
    "* Modify: hogehoge",
    "",
];

正規表現で特定の文字列を取り出す

次に正規表現を使って、上記の contents から特定の条件の値を取り出します。 Rustではregex*2というライブラリが提供されているのでそれを利用します。

    let parsed_content = contents.iter().cloned().filter(|x| {
        regex::Regex::new(r"^\*").unwrap().is_match(x)
    }).collect::<String>();

ここでは、 Vectorに含まれる文字列のうち、 * で始まるもののみを取得しています。
parsed_contentは次のようになります。

parsed_content = "* Modify: hogehoge";

同様にして、change_levelsから「チェックされている」levelの表現(PATCH/MINOR/MAJOR)を取り出します。

    let parsed_change_level = match change_levels.iter().cloned().filter(|x| {
        regex::Regex::new(r"^\*\s*\[\S\]").unwrap().is_match(x)
    }).nth(0) {
        Some (x) => {
            let captures = regex::Regex::new(r"MAJOR|MINOR|PATCH").unwrap().captures(x).unwrap();
            captures.get(0).map_or("", |m| m.as_str())
        },
        None => panic!("please specify correct change level"),
    };

parsed_change_levelには以下のような値が入っています。

parsed_change_level = "MINOR";

git tagのバージョンを取得する

さきほど取得したparsed_change_levelをもとに、tagのバージョンを更新します。
現在のgit tagのバージョンの文字列はgit2ライブラリ*3で下記のように取得できます。

    let repo = git2::Repository::open("/").unwrap();
    let mut current_version = "".to_string();
    match get_curreunt_version(&repo) {
        Ok(version) => {
            current_version = version;
        },
        Err(e) => {
            panic!("describe failed. {}", e);
        }
    }

    fn get_curreunt_version(repo: &git2::Repository) -> Result<String, git2::Error> {
        let mut describe_ops = git2::DescribeOptions::new();
        describe_ops.describe_tags();
        let result = repo.describe(&describe_ops).unwrap();
        let mut describe_format_ops = git2::DescribeFormatOptions::new();
        describe_format_ops.abbreviated_size(0);

        result.format(Some(&describe_format_ops))
    }

ここで、current_versionには以下のような値が入っています。

current_version = "1.1.1";

バージョンを更新する

さきほど取得した parsed_change_level を基に、バージョンを更新します。 levelによって下記のように更新されるものとします。

new_version = "1.1.2"; // PATCH
new_version = "1.2.0"; // MINOR
new_version = "2.0.0"; // MAJOR

下記のように整形します。

    let mut numbers: Vec<String> = current_version.split('.').map(|x| x).collect();
    let patch = numbers.pop().unwrap();
    let minor = numbers.pop().unwrap();
    let major = numbers.pop().unwrap();
    let new_version = "".to_string();

    match parsed_change_level {
        "MAJOR" => {
            numbers.push((major.parse::<i32>().unwrap() + 1).to_string());
            numbers.push("0".to_string());
            numbers.push("0".to_string());
            new_version = numbers.join(".");
        },
        "MINOR" => {
            numbers.push(major);
            numbers.push((minor.parse::<i32>().unwrap() + 1).to_string());
            numbers.push("0".to_string());
            new_version = numbers.join(".");
        },
        "PATCH" => {
            numbers.push(major);
            numbers.push(minor);
            numbers.push((patch.parse::<i32>().unwrap() + 1).to_string());
            new_version = numbers.join(".");
        },
        _ => panic!("change level is wrong. please specify correct change level"),
    }

Rustでは、型を明確に扱う必要があります。
ここでは、 1.1.1 という文字列をVectorにしていますが、その値も文字列なので、i32型にcastして乗算を行っています。
また、対象が &str 型か、 String 型かも気にする必要があります。 文字列に対して「操作」を行うときは、 String 型として扱いましょう。 Rustでは、"hoge" と文字列を定義すると、デフォルトで &str 型として扱われます。
String 型として扱うためには、明示的な宣言を行いましょう。

let hoge = "hoge".to_string();
//or
let hoge = String::from("hoge");

おわりに

今回は、Rustで文字列を扱う具体的なコード例を紹介しました。
Rustはまだまだ開発が活発な言語であり、RubyやPHPを始めとする動的スクリプト言語とは対極と呼べるほど言語設計の考え方が違うので、触ってみると新たな発見があって面白いかもしれません。
新しく学ぶ言語の選択肢の1つとして検討してみてはいかかでしょうか。