Smile Engineering Blog

ジェイエスピーからTipsや技術特集、プロジェクト物語を発信します

Rustで設定ファイルを複数読み込む

Rustでの標準的・簡易的な設定ファイルの実装方法と問題点

開発したソフトの実行環境が変わるたびにコーディング・コンパイルをしなおすことのないよう、設定情報をソースコードとは別ファイルにしたり、実行時のOSの環境変数から取得したりする設計は常套手段です。

例えば、Rustの標準クレート「std::env」を使うと、「let var = env::var("TEST").unwrap();」などとするだけで、当プログラムを実行したときに環境変数「TEST」に値が設定されていれば、変数「var」に環境変数「TEST」の設定値が入ります。

また、クレート「dotenv」を使って、実行プログラムと同じディレクトリに「.env」というファイルを作成し、そこに設定情報を記載しておき、読み取るコード側では定数をまとめた構造体を作って解析させる、という方法が用いられていたりします。

ですが、これらの手法では、設定情報が多くなる場合(メッセージやSQLなども設定ファイルにする場合など)に、アプリの保守がしづらくなります。

実行前に大量に環境変数に値を設定するのは現実的ではありませんし、「dotenv」で「from_path」メソッドを使っても、複数のファイルを読み込ませることができないようにみえます。

自由度が高く保守しやすい設定ファイル設計に

そこで、今回は以下のような仕組みを作りましたので、紹介します。

  • 実行ファイルと同じディレクトリに、設定ファイル群を保存する「config」ディレクトリを生成しておく。
  • 「config」ディレクトリには、設定ファイルを任意のファイル名でいくつ保管してもよい。配下にディレクトリをいくつ生成してもよい。
  • 設定ファイルは「toml」形式とし、拡張子も「.toml」とする。(「config」ディレクトリ配下の「.toml」ファイルだけ読み込む)※Rustの依存ライブラリ定義ファイル(cargo.toml)も「toml」形式なので、合わせました。

実装

オリジナル設定ファイル例(config\config.toml)

[csv.output]
file_path = "{CUR}//data//output_%Y%m%d_%H%M%S.csv"
char_code = "shift-jis"

[calc]
value1 = 10
value2 = 20

Rust依存関係設定ファイル(cargo.toml)

[package]
name = "config-file-test"
version = "0.1.0"

[dependencies]
lazy_static = "1.4.0" 
toml = "0.5.6" 

Rustソース(main.rs)

以下のように設定値を呼び出します。

#[macro_use]
extern crate lazy_static;
extern crate toml;

mod initialize;
use initialize::file_config::CONFIG;

fn main() {
    // 「as_str()」メソッドにて、文字型として呼び出しています。
    // 初めて「CONFIG」が参照されるタイミングで「file_config.rs」が実行され、設定ファイルの内容がメモリに展開されます。
    let char_code = CONFIG["csv"]["output"]["char_code"].as_str().unwrap();
    println!("char_code = {}", char_code);  // 標準出力結果「char_code = shift_jis」

    // 「as_integer()」メソッドにて、数値型として呼び出しています。
    // このタイミングでは、すでにメモリに展開された値を参照しているだけです。
    let value1 = CONFIG["calc"]["value1"].as_integer().unwrap();
    let value2 = CONFIG["calc"]["value2"].as_integer().unwrap();
    println!("value1 + value2 = {}", value1 + value2);  // 標準出力結果「value1 + value2 = 30」
}

読み込む設定の定数の型を予め構造体(struct)で定義しておく手段もありますが、定数が増えるごとに構造体も増やしていく作業が面倒なので、上記のような参照方法を紹介しました。 ただ、参照の都度「as_~()」メソッドで型変換するので、処理性能を追求する必要がある場合は、構造体で型を定義しておいたほうがいいかもしれません。

Rustソース(initialize\mod.rs)

本編には関係ないですが、設定ファイルは一度読めばいいので、主処理と切り離しやすいようモジュールにしています。

pub mod file_config;

Rustソース(initialize\file_config.rs)

設定ファイル(.toml)を解析してメモリ上に展開し、上記のように呼び出せるように準備する処理です。 設定ファイルが存在しなければ、新たに生成します。 その場合の初期値は、以下の定数「DEFAULT_CONFIG」に書いています。

use std::io::{BufReader, Read, BufWriter, Write};
use std::fs;
use std::fs::{File, DirBuilder};
use std::path::Path;
use std::env;
use toml::Value;

lazy_static! {
    pub static ref CONFIG:Value = {
        return load_config();
    };
}

const DEFAULT_CONFIG: &'static str = r#"
[csv.output]
file_path = "{CUR}//data//output_%Y%m%d_%H%M%S.csv"
char_code = "shift-jis"

[calc]
value1 = 10
value2 = 20
"#;

// toml形式の設定ファイルを読み込む
fn load_config() -> Value {

    // 当プログラムのディレクトリ
    let cur_path_str = env::current_exe().unwrap().clone();
    let cur_path = Path::new(&cur_path_str);
    let cur_dir = cur_path.parent().unwrap().display();

    // 当プログラムのディレクトリ配下に存在する該当拡張子のファイルパスを取得
    let file_paths = get_path(Vec::new(), &cur_dir.to_string(), "toml");

    // toml形式変換前の設定文字列
    let mut conf_toml_str = String::new();
    // 全ファイルをテキストで読み込み
    for path in file_paths.iter() {
        conf_toml_str = format!("{}{}", conf_toml_str, get_text_file(Path::new(&path), "toml"));
    }

    // 設定を1件も取得できていなければ
    if conf_toml_str.len() < 1 {
        // 設定ファイルを保存するディレクトリを生成
        let path_str = format!("{}//config//config.toml", &cur_dir);
        let path = Path::new(&path_str);
        let dir = path.parent().unwrap();
        DirBuilder::new().recursive(true).create(&dir).unwrap();

        // 設定ファイルのパスを書き込みモードで開く。これは`io::Result<File>`を返す。
        let file = match File::create(&path) {
            Err(e) => panic!("couldn't create {}: {}", path_str, &e.to_string()),
            Ok(file) => file,
        };

        // バッファリングされたストリーム出力とする
        let mut f = BufWriter::new(file);

        // 設定ファイルへ記述
        conf_toml_str = DEFAULT_CONFIG.to_string();
        match f.write_all(conf_toml_str.as_bytes()) {
            Err(e) => panic!("couldn't write {}: {}", path_str, &e.to_string()),
            Ok(_) => println!("{} writes :{}\n", path_str, conf_toml_str),
        }
    }

    // 文字列内に「{CUR}」が存在すれば、当プログラムが存在するディレクトリとみなして、カレントディレクトリに置換
    conf_toml_str = conf_toml_str.replace("{CUR}", &format!("{}", &cur_dir));

    // 設定をtoml形式に変換して返す
    conf_toml_str.parse::<Value>().expect(&format!("couldn't parse config file to toml format.{}", &conf_toml_str))
}

fn get_path (mut files: Vec<String>, target:&str, path_ext:&'static str) -> Vec<String> {
    for file_path in fs::read_dir(target).unwrap() {
        let file_path = file_path.unwrap().path();
        if file_path.is_dir() {
            files = get_path(files, &file_path.display().to_string(), path_ext);
        } else {
            match file_path.extension() {
                _path_ext => files.push(file_path.display().to_string()),
            }
        }
    }
    files
}

fn get_text_file (path: &Path, extension: &'static str) -> String {
    let display = path.display();
    match path.extension() {
        None => return "".to_string(),
        Some(ext) => {
            if ext != extension {
                return "".to_string();
            }
        },
    };

    // pathを読み込み専用モードで開く
    let f = match File::open(&path) {
        Err(e) => panic!("couldn't open {}: {}", display, &e.to_string()),
        Ok(f) => f,
    };

    //  バッファリングされたストリーム入力とする
    let mut br = BufReader::new(f);

    // ファイルの中身を文字列に読み込み
    let mut conf_toml_str = String::new();
    match br.read_to_string(&mut conf_toml_str) {
        Err(e) => panic!("couldn't read {}: {}", display, &e.to_string()),
        Ok(_) => println!("{} contains:\n{}", display, conf_toml_str),
    }
    conf_toml_str
}

これで拡張子「.toml」のファイルを「config」フォルダに入れておけば、実行の都度全パラメータを自動で読みこみ、Rust内どのモジュールでも「use initialize::file_config::CONFIG;」だけ記述すれば参照できる状態になります。 もし設定ファイルがない場合でも、自動で「config」フォルダとその中に「config.toml」を生成し、ハードコードした初期値を書き込みます。

「rustc 1.42.0」でコンパイルできることを確認しています。