Rust入門メモ
Tour of RustのChapter 7までDone
ノリはわかった
入門用としてCLIでパスワードジェネレータを作成
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);
// エラーに応じた何らかのアクションを取る
}
}
パスワードジェネレータの抽象化
今回はトレイトを使ってアルゴリズムに依存せずにパスワードジェネレータを管理できるようにした。
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パスワードアルゴリズムとかを検討しようとした時に困ってしまうので、将来的な拡張性を考えると過度な共通化は好ましくないかもしれない。
モジュール・ディレクトリの切り方よくわからない問題
ノリでモジュールを切ってしまったので結構まずい。これがプロダクトだったら将来的に困ると思うので、しっかりベストプラクティスを学んでいく必要がある。
↓これ読むよ
テスト
ユニットテストはテスト対象と同じファイルにテスト時だけ実行される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自体はイミュータブルな参照しか受けないため、実は今回のケースでは問題ないと思う。今後困ることがないように一律でケースの中に入れてはいる。