🚧

Rustでごく簡単なCLIバリデーションツールを作った

2024/11/22に公開

概要

Rustに入門したいなと思って公式のThe Bookを11章まで読みました。

ちょっとゴツすぎてそこで一旦読むのをやめたのですが、、、(全部で21章まである)
学んだ文法で何か作ってみたいなと思い、掲題のツールを作成しました。

作ったものの動画は以下です。
非常に簡単なレベルで、データの形式についてバリデーションっぽいことをしてくれます。

よい書き方・美しい書き方で書いた自信は全くなく、またChatGPTの力も多分に借りたので、初心者が初心者なりにちょっとしたものを作ってみたんだな、くらいの目で見ていただけますと幸いです。

Cargo.toml

Cargo.tomlは以下になります。

[package]
name = "validation-tool"
version = "0.1.0"
edition = "2021"

[dependencies]
dialoguer = "0.11"
ferris-says = "0.3.1"
regex = "1.8"

入門なので全てをスクラッチで書いてみたかったのですが、変に私にとって難しすぎる箇所でつまづいても嫌だなと思い、3つほどクレートを入れています。
それぞれ役割は以下です。

  • dialoguer
    • 「ターミナル上で複数の選択肢を提示し、上下キーを使って選択肢を選ぶUI」を作れる
    • フロントエンドのフレームワークをinitしたときによくみるやつで、やってみたかった
  • ferris-says
    • Rustコミュニティの非公式マスコットである、フェリスという 🦀 が何かセリフを言う風のUIを作れる
    • Rust公式サイトの「はじめに」で使われていたクレート
    • フェリスが可愛すぎたので入れた
  • regex
    • 正規表現を扱える
    • 電話番号の形式かを判定する所で使った

バリデーション用ファイル

main.rsとは別に、checker.rsという、今回バリデーションを担当するファイルを作成しています。全文は以下です。

checker.rs
use regex::Regex;
use std::fmt;

pub enum CheckerType {
    Number,
    Boolean,
    Japanese,
    PhoneNumber,
}

impl CheckerType {
    fn name(&self) -> String {
        match self {
            CheckerType::Number => String::from("数値"),
            CheckerType::Boolean => String::from("真偽値"),
            CheckerType::Japanese => String::from("日本語"),
            CheckerType::PhoneNumber => String::from("電話番号"),
        }
    }

    pub fn check(&self, input: &str) -> bool {
        match self {
            CheckerType::Number => check_number(input),
            CheckerType::Boolean => check_boolean(input),
            CheckerType::Japanese => check_japanese(input),
            CheckerType::PhoneNumber => check_phone_number(input),
        }
    }
}

impl fmt::Display for CheckerType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.name())
    }
}

fn check_number(input: &str) -> bool {
    input.parse::<f64>().is_ok()
}

fn check_boolean(input: &str) -> bool {
    input == "true" || input == "false"
}

fn check_japanese(input: &str) -> bool {
    input.chars().any(|c| matches!(c, '\u{30e0}'..='\u{9fcf}' | '\u{3005}'..='\u{3006}' | '\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}'))
}

fn check_phone_number(input: &str) -> bool {
    let phone_regex = Regex::new(r"^\d{2,4}-\d{2,4}-\d{4}$").unwrap();
    phone_regex.is_match(input)
}

上から、要点となる箇所を説明します。

pub enum CheckerType {
    Number,
    Boolean,
    Japanese,
    PhoneNumber,
}

ターミナルでユーザーに選択してもらう、バリデーションのタイプをenumで定義しています。
上から「数値」「真偽値」「日本語」「電話番号」です。

「文字列」はないし「配列」や「JSON」もないし、急な「日本語」ってどんな組み合わせやねん、という感じではあるのですが、遊びなので適当なチョイスでいっかという感じで選びました。

あと、「文字列」はもう入力全て該当するのでは?となり、「配列」と「JSON」は下記記事のような字句解析、構文解析が必要になりそうだ、私には無理だとなり、かんたんそうなものの寄せ集めにしています。

https://zenn.dev/sa2knight/articles/rust-json-formatter

次はCheckerTypeにメソッドを定義します。

fn name(&self) -> String {
    match self {
        CheckerType::Number => String::from("数値"),
        CheckerType::Boolean => String::from("真偽値"),
        CheckerType::Japanese => String::from("日本語"),
        CheckerType::PhoneNumber => String::from("電話番号"),
    }
}

nameメソッドを定義しています。
enumである自分自身(CheckerType)についてパターンマッチを行っており、それぞれのタイプに応じた日本語の名前を返してくれるメソッドです。

パターンマッチいいですね。これがしたかったと言っても過言ではない。
このnameメソッドはさらに少し下の部分で使っており、用途はその時に説明します。

pub fn check(&self, input: &str) -> bool {
    match self {
        CheckerType::Number => check_number(input),
        CheckerType::Boolean => check_boolean(input),
        CheckerType::Japanese => check_japanese(input),
        CheckerType::PhoneNumber => check_phone_number(input),
    }
}

次はcheckメソッドです。
こちらもCheckerTypeの種類に基づいて、数値なら数値用のバリデーション関数、電話番号なら電話番号のバリデーション関数、と種類に応じた関数の実行を行っています。

impl fmt::Display for CheckerType {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.name())
    }
}

さらに、CheckerTypeについて、文字列化(ToString)を実装します。
急に文字列化という意味わからない概念を持ち出したのですが、これは後でdialoguerの関数に渡す、選択肢たちのデータが、文字列として表示できるデータでないと受け付けない(ToStringトレイトが実装されている型でないと通さない)形式だったために行なっていることです。

ToStringトレイトはDisplayトレイトを元に実装されているようだったので、DisplayトレイトをCheckerTypeに実装して要件を満たしたということです。

こちらの実装が参考になりました。

https://zenn.dev/qnighy/articles/8ea875cae10d42

fn check_number(input: &str) -> bool {
    input.parse::<f64>().is_ok()
}

fn check_boolean(input: &str) -> bool {
    input == "true" || input == "false"
}

fn check_japanese(input: &str) -> bool {
    input.chars().any(|c| matches!(c, '\u{30e0}'..='\u{9fcf}' | '\u{3005}'..='\u{3006}' | '\u{3040}'..='\u{309F}' | '\u{30A0}'..='\u{30FF}'))
}

fn check_phone_number(input: &str) -> bool {
    let phone_regex = Regex::new(r"^\d{2,4}-\d{2,4}-\d{4}$").unwrap();
    phone_regex.is_match(input)
}

あとはそれぞれのバリデーションタイプに紐づく、具体的なバリデーション関数です。
厳密なチェックではないかもしれないですが、それぞれシンプルに実装しました。

main

バリデーション側の準備が終わったので、main関数を実装していきます。
こちらがmain.rsの全文です。

main.rs
mod checker;
use checker::CheckerType;
use dialoguer::{console::Term, theme::ColorfulTheme, Select};
use ferris_says::say;
use std::io::{self, stdout, BufWriter, Write};

fn main() {
    let validation_types = vec![
        CheckerType::Number,
        CheckerType::Boolean,
        CheckerType::Japanese,
        CheckerType::PhoneNumber,
    ];

    let selection = Select::with_theme(&ColorfulTheme::default())
        .with_prompt("バリデーションの種類を選んでください")
        .items(&validation_types)
        .default(0)
        .interact_on_opt(&Term::stdout())
        .unwrap();

    let validation_type = &validation_types[selection.unwrap()];

    print!("対象のデータを入力してください: ");
    io::stdout().flush().unwrap();

    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    let input = input.trim();

    let result = validation_type.check(input);

    let stdout = stdout();
    let message = if result {
        format!("チェックした結果、{}は{}でした", input, validation_type)
    } else {
        format!(
            "チェックした結果、{}は{}ではありませんでした",
            input, validation_type
        )
    };
    let width = message.chars().count() + 4;
    let mut writer = BufWriter::new(stdout.lock());
    say(&message, width, &mut writer).unwrap();
}

こちらもそれぞれ要点となる箇所を説明します。

let validation_types = vec![
    CheckerType::Number,
    CheckerType::Boolean,
    CheckerType::Japanese,
    CheckerType::PhoneNumber,
];

let selection = Select::with_theme(&ColorfulTheme::default())
    .with_prompt("バリデーションの種類を選んでください")
    .items(&validation_types)
    .default(0)
    .interact_on_opt(&Term::stdout())
    .unwrap();

まず、先ほどのchecker.rsから引っ張ってきたCheckerTypeを使って、ベクタを作成しています。
そして、dialoguerの構造体であるSelectをチェインしていき、itemsメソッドにそのベクタを渡します。

ここでさっきの文字列化が効きます。あれをやっていない場合はここで型エラーが出ます。

default(0)は矢印の最初の位置を設定するため、interact_on_opt(&Term::stdout())は、ターミナル上で選択肢をインタラクティブに表示した上で選択を行えるようにするためのものです。
interact_on_opt(&Term::stdout())はResult型を返すので、unwrapしてその中のデータを取り出します。

let validation_type = &validation_types[selection.unwrap()];

selectionOption<usize>という型なので、さらにunwrapして最終的な整数を取り出します。それがユーザーが選んだ選択肢のインデックスになっているので、上で定義したベクタに整数を渡して、選ばれたバリデーションのタイプを取得します。

let mut input = String::new();
io::stdin().read_line(&mut input).unwrap();

io::stdin().read_line(&mut input)で、ターミナルからユーザーに対して入力を要求することができます。これがバリデーションの対象となるデータです。

let stdout = stdout();
let message = if result {
    format!("チェックした結果、{}は{}でした", input, validation_type)
} else {
    format!(
        "チェックした結果、{}は{}ではありませんでした",
        input, validation_type
    )
};
let width = message.chars().count() + 4;
let mut writer = BufWriter::new(stdout.lock());
say(&message, width, &mut writer).unwrap();

選んだタイプのバリデーション関数によってboolが返され、それに基づき最終的に出力するメッセージを変えます。
そして最後にそれをferris_says::say関数に渡せば、🦀 が成否の判定をメッセージで教えてくれます。

これで完成です!🦀

まとめ

今回作成したコードのリポジトリは以下になります。
https://github.com/usk94/checker

Rustおもしろいです。
あとテスト書くの忘れていた。。。

参考記事

https://doc.rust-jp.rs/book-ja
https://qiita.com/graminume/items/2ac8dd9c32277fa9da64

Discussion