Closed6

Rust入門メモ

kngnrkngnr

Enumでアルゴリズム管理

今回は例外部分はサボって標準出力してデフォルト値で丸めるようにしている。将来的にはResult型を使ってうまいことやりたい。

pub enum PasswordAlgorithm {
    Simple,
    Numeric,
    // 新しいPasswordGeneratorを追加したらこちらにも追加すること
}

impl PasswordAlgorithm {
    pub fn from_str(s: &str) -> Self {
        match s.to_lowercase().as_str() {
            "simple" => PasswordAlgorithm::Simple,
            "numeric" => PasswordAlgorithm::Numeric,
            // 新しいPasswordGeneratorを追加したらこちらにも追加すること
            _ => {
                 // 該当がなくてもエラーは出さずにデフォルト値で対応する
                eprintln!("Error: invalid password algorithm, using default simple algorithm ", );
                PasswordAlgorithm::Simple
            },
        }
    }
}

Resultでエラー処理ができると、from_strの外でPasswordGeneratorの利用者がエラーの対処を決められるので、将来的にはこんな感じで利用できるようにしたい。

match PasswordAlgorithm::from_str(algorithm_str) {
    Ok(algorithm) => {
        // 何かする
    },
    Err(e) => {
        eprintln!("Error: invalid password algorithm {:?}", e);
        // エラーに応じた何らかのアクションを取る
    }
}
kngnrkngnr

パスワードジェネレータの抽象化

今回はトレイトを使ってアルゴリズムに依存せずにパスワードジェネレータを管理できるようにした。

pub trait PasswordGenerator {
    fn generate_password(&self, length: usize) -> String;
}

PasswordGeneratorを満たす実装を実体に与えてあげることで、PasswordGeneratorとして機能できるようになる。

SimplePasswordGenerator

pub struct SimplePasswordGenerator;

impl SimplePasswordGenerator {
    pub fn new() -> Self {
        Self
    }
}

impl PasswordGenerator for SimplePasswordGenerator {
    fn generate_password(&self, length: usize) -> String {
        let mut rng = thread_rng();

        let password: String = (0..length) // length分だけ要素を生成してmapに回す
            .map(|_| {
              let idx = rng.gen_range(0..CHARSET.len());
              CHARSET[idx] as char
            }).collect();
    
        password
    }
}

NumericPasswordGenerator

Simple以外にも数値だけで生成とか、別のアルゴリズムを扱える。

pub struct NumericPasswordGenerator;

impl NumericPasswordGenerator {
    pub fn new() -> Self {
        Self
    }
}

impl PasswordGenerator for NumericPasswordGenerator {
    fn generate_password(&self, length: usize) -> String {
        let mut rng = thread_rng();
        let password = (0..length)
            .map(|_| {
                rng.gen_range(0..10).to_string().chars().next().unwrap()
            })
        .collect();
        password
    }
}

アルゴリズムを受けてジェネレータを返す関数

アルゴリズムによって動的に返すジェネレータを変える必要がある。
引数にPasswordAlgorithmを受けている時点でEnumに存在することが確定しているので、例外処理を入れていない。ここでPasswordAlgorithmの列挙に不足があるとちゃんと怒ってくれる。

pub fn new_password_generator(algorithm: PasswordAlgorithm) -> Box<dyn PasswordGenerator> {
    match algorithm {
        PasswordAlgorithm::Simple => Box::new(SimplePasswordGenerator::new()),
        PasswordAlgorithm::Numeric => Box::new(NumericPasswordGenerator::new()),
    }
}

考えてること

これらのアルゴリズムは文字数が指定できる時点で、本質的にはmapで回る部分は実装同じになりそう。mapの中だけ入れ替えればいいから、もうちょっと共通化してもいいかもしれない。
とはいえ、共通化しすぎるとフォーマットが固定のUUIDパスワードアルゴリズムとかを検討しようとした時に困ってしまうので、将来的な拡張性を考えると過度な共通化は好ましくないかもしれない。

kngnrkngnr

テスト

ユニットテストはテスト対象と同じファイルにテスト時だけ実行されるmoduleを追加する。

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_generate_password_length() { // パスワードのサイズが指定した通りになること
        let generator = SimplePasswordGenerator::new();
        let length = 10;
        let password = generator.generate_password(length);
        assert_eq!(password.len(), length);
    }

    #[test]
    fn test_generate_password_charset() { // 想定通りのキャラクターセットを利用すること
        let generator = SimplePasswordGenerator::new();
        let length = 1000;
        let password = generator.generate_password(length);
        for ch in password.chars() {
            assert!(CHARSET.contains(&(ch as u8)));
        }
    }
}

注意点

テストケース間でリソースを共有する場合、スレッドセーフでないといけない。テストケース間で状態を共有すること自体が危険なので、テストケースの外でgeneratorを定義するのは避けている。
generator自体はイミュータブルな参照しか受けないため、実は今回のケースでは問題ないと思う。今後困ることがないように一律でケースの中に入れてはいる。

このスクラップは2023/12/29にクローズされました