オウルです。
やろうやろうと思っていたRustの学習をはじめました。本記事は、どちらかというと自分用の備忘録です。なので、Rust初心者の方は、まず先にこんな記事より遥かに役立つThe Rust Programming Languageを是非ご覧ください。
The Rust Programming Languageを読んだものの、読んだだけではコードをかける気がしないので、実践あるのみ。簡単だけど、よく使うCSV読み込み、XMLパース、データベースにデータ登録あたりを実装してみます。
あわせて、見返したときに当時の実装の根拠となるThe Rust Programming LanguageやThe Rust community’s crate registryやGitHubのリンクを貼るようにします。まずは、動くアプリを目指して、その後にRust Performance Pitfallsにあるようなことを意識してパフォーマンス改善につなげていきます。
language | Rust 1.51.0 |
os | WSL Ubuntu 21.04 |
editor | VSCode |
コマンド引数と設定ファイル
コンソールアプリでは、コマンド引数、若しくは設定ファイルがつきものです。まずは、この2つから実装を進めていきます。Loggingはとても重要ですが、こちらは次回以降にします。
モジュールを複数のファイルに分割
コマンド引数、環境変数、設定ファイルをモジュール化して3ファイルに分割します。
ファイル構成
|- app_args.rs
|- app_env.rs
|- app_settings.rs
config.rs
lib.rs
main.rs
config.rs
3つのモジュールをスコープに持ち込み、かつ再公開しています。
1 2 3 |
pub mod app_args; pub mod app_env; pub mod app_settings; |
もし、app_argsでapp_envを使用する場合は、次のようにsuperを指定してスコープに持ち込みます。
1 2 |
// super: 現在のモジュールの親 use super::app_env; |
コマンド引数
コマンドライン引数を解析するclapというクールなCrateがあるので導入します。
因みにThe Rust Programming Languageでは、標準ライブラリを使用したコマンドラインプログラムを構築するがあります。
Cargo.toml
1 2 |
[dependencies] clap = "2.33.3" |
まずはAppArgs構造体を定義します。フィールドは、データベースのホスト、データベース名、ポートとします。データベース接続には、アカウントとパスワードが必要となりますが、この2つは学習を兼ねて環境変数から取得するようにします。
AppArgs構造体
コマンド引数の解析部分は、clapのQuick Exampleにある’builder pattern’で実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
use clap::{ App, Arg }; pub struct AppArgs { pub host: String, pub database: String, pub port: u32, } impl AppArgs { pub fn new() -> anyhow::Result<AppArgs> { let matches = App::new("My Command App") .version("1.0") .author("owl") .about("app to learn rust") .arg(Arg::with_name("host") .short("h") .long("host") .value_name("IP") .help("database server host name / IP address") .required(true) .takes_value(true)) .arg(Arg::with_name("database") .short("d") .long("database") .value_name("NAME") .help("database name") .required(true) .takes_value(true)) .arg(Arg::with_name("port") .short("p") .long("port") .value_name("PORT") .help("database server port") .takes_value(true)) .get_matches(); // "host"は、required(true)としているためunwrapを呼び出しても安全 let host = matches.value_of("host").unwrap(); let database = matches.value_of("database").unwrap(); let port = matches.value_of("port").unwrap_or("1433"); Ok( AppArgs { host: host.to_string(), database: database.to_string(), port: port.parse().unwrap_or(1433), } ) } } |
エラーハンドリングにanyhowを導入していますが、次回以降にエラーハンドリングという題目で取り上げたいと思っています。
環境変数
AppEnv構造体
環境変数の取得には標準ライブラリを使用します。The Rust Programming Languageの環境変数を取り扱うにあるように、env::var関数は、 環境変数がセットされていたら、環境変数の値を含むOk列挙子の成功値になるResultを返します。 環境変数がセットされていなければ、Err列挙子を返します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
use std::env; use anyhow::Context; pub struct AppEnv { pub account: String, pub password: String, } impl AppEnv { pub fn new() -> anyhow::Result<AppEnv> { let key_account = "ACCOUNT"; let account = env::var(key_account) .with_context(|| format!("not found key: {}", key_account))?; let key_pw = "PASSWORD"; let password = env::var(key_pw) .with_context(|| format!("not found key: {}", key_pw))?; Ok( AppEnv{ account, password } ) } } |
設定ファイル
設定ファイルはJSON形式とします。
AppSettings構造体
once_cellを使ってAppSettingsをグローバルデータとして初期化します。シリアライズ/デシリアライズには、serdeを使います。参照のみのためMutexはなしです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
use serde::{ Deserialize, Serialize }; use once_cell::sync::OnceCell; use std::fs::File; use std::io::BufReader; use serde_json; pub struct AppSettings {} impl AppSettings { pub fn get_instance() -> &'static DbSettings { static INSTANCE: OnceCell<DbSettings> = OnceCell::new(); INSTANCE.get_or_init(|| { let filename = "appsettings.json"; let file = File::open(filename).expect(&format!("{} not found", filename)); let reader = BufReader::new(file); let settings: DbSettings = serde_json::from_reader(reader).expect(&format!("{} not read", filename)); settings }) } } #[derive(Serialize, Deserialize)] pub struct DbSettings { pub host: String, pub database: String, pub port: u32, pub account: String, pub password: String, } |
おまけ
おまけで実装するにあたり、導入したライブラリの関数のシグニチャで初めてみたTrait、今後使いたいTraitなどを調べてみます。
std::convert::AsRef
Stringと&strの両方を引数として受け入れたい場合(&strに変換できるすべての参照を引数として受け入れたいこと)は、関数のシグニチャにAsRefを受け取るジェネリクスを配置します。次回以降で予定しているXMLパース、CSVの箇所(std::path::Path)で使います。
std::convert::AsRefにあるExamplesのコードです。
1 2 3 4 5 6 7 8 9 |
fn is_hello<T: AsRef<str>>(s: T) { assert_eq!("hello", s.as_ref()); } let s = "hello"; is_hello(s); let s = "hello".to_string(); is_hello(s); |
これが可能なのは、Stringと&strの両方がAsRef
他にも、AsRefとBorrowの特性の違い、注意事項として、コストのかかる変換を行う必要がある場合は、タイプ&Tを使用してFromを実装するか、カスタム関数を作成すること、変換が失敗する可能性がある場合は、Option