😋
RustでYAMLファイルを読み込む
背景
私は自動化できることを手作業で行うのが嫌いです。
タイミングの問題などで、手作業で行わなければならない場合などは仕方がないのですが、できるだけ自動化したいと考えています。
最近、APIの開発が多くなってきました。
APIの統合テストが面倒臭いので、テストを自動化するツールを作成しています。
このツールの開発を進める中で得た知見として、この記事に残しておこうと思います。
概要
この記事では、RustでYAMLファイルを読み込み、各要素を扱えるようにします。
詳細
- YAMLファイルを読み込むために、serdeとserde_yamlを使用します
- ファイルを読み込む前の前提として、YAMLファイルの構造に合わせて、Structを定義し、
#[derive(Deserialize)]を指定します - 処理の流れは、
-
std::fs::Fileでファイルを開く -
serde_yaml::from_readerに渡す
-
- 前提として、
#[derive(Deserialize)]を定義しているので、serde_yaml::from_readerにデータを渡すと、そのままマッピングされます -
serde_yaml::from_readerやserde_yaml::from_strはResultを返すため、エラーを考慮して実装する必要があります- 主なエラーは以下です
- IOエラー: ファイルが見つからない、読み取り権限がないなど(from_readerの場合)
- デシリアライズエラー: YAMLの構文が間違っている
- インデントがずれているなど
- YAMLの構造がstructの定義と一致しない
- 必須フィールドがない、型が違うなど
- 主なエラーは以下です
実装
実際に以下のYAMLファイルを読み込むことを考えます。
APIのテスト自動化を目的にしているので、APIエンドポイントのURLなどの情報を持つ以下のファイルを読み込むことを想定します。
# config/api_test.yaml
# テスト対象のベースURL
base_url: "https://api.example.com/v1"
# テストするエンドポイントの定義
endpoints:
- name: "Get user list" # テストの識別名
method: "GET" # HTTPメソッド
path: "/users" # ベースURL以降のパス
expected_status: 200 # 期待するHTTPステータスコード
- name: "Get specific user"
method: "GET"
path: "/users/123"
expected_status: 200
Structの定義
YAMLファイルの内容と、structの関係は以下で、
YAMLのオブジェクト(キー: 値 の集まり)は、Rustの struct にマッピングされ、YAMLのリスト/配列(- で始まるアイテムの並び)は、Rustの Vec<T> (ベクタ)にマッピングされます。
use serde::Deserialize;
/// YAMLファイル全体の構造に対応するstruct
#[derive(Debug, Deserialize)]
pub struct ApiTestConfig {
pub base_url: String,
pub endpoints: Vec<Endpoint>, // "endpoints"キーの配列をVecとして定義
}
/// "endpoints"リスト内の各要素(オブジェクト)に対応するstruct
#[derive(Debug, Deserialize)]
pub struct Endpoint {
pub name: String,
pub method: String,
pub path: String,
pub expected_status: u16, // HTTPステータスコードは u16 が適切です
}
ファイル読み込み処理の実装
ファイルを読み込んで、各要素を出力する処理
use serde::Deserialize;
use std::fs::File;
// --- 上記で定義したstruct ---
// #[derive(Debug, Deserialize)]
// pub struct ApiTestConfig { ... }
// #[derive(Debug, Deserialize)]
// pub struct Endpoint { ... }
// --------------------------
fn main() -> Result<(), Box<dyn std::error::Error>> {
// ファイルを開く (ファイル名は 'config.yaml' と仮定)
let f = File::open("config.yaml")?;
// serde_yaml::from_reader を使ってデシリアライズ
let config: ApiTestConfig = serde_yaml::from_reader(f)?;
// 読み込んだデータを表示
println!("--- 読み込み成功 ---");
println!("ベースURL: {}", config.base_url);
println!("\n--- テスト対象エンドポイント ---");
for (i, endpoint) in config.endpoints.iter().enumerate() {
println!("\n テスト {}: {}", i + 1, endpoint.name);
println!(" Path: {}{}", config.base_url, endpoint.path);
println!(" Method: {}", endpoint.method);
println!(" Expect: {}", endpoint.expected_status);
}
Ok(())
}
エラー処理を追加
上記のコードに、エラー処理を追加し、より堅牢な実装にします。
以下の実装例では、以下2つの観点のエラー処理を実装しています。
- IOエラー
- ファイルが見つからない、ファイルにアクセスできない場合のエラー
- デシリアライズエラー
- YAMLの構造がStructと一致していないなどの場合のエラー
これ以外にもエラーとして、YAMLファイルの内容に構文エラーがあるなども考えられます。
use serde::Deserialize;
use std::fs::File;
use std::io::ErrorKind; // IOエラーの種類を判定するために追加
// --- 上記で定義したstruct ---
// #[derive(Debug, Deserialize)]
// pub struct ApiTestConfig { ... }
// #[derive(Debug, Deserialize)]
// pub struct Endpoint { ... }
// --------------------------
fn main() -> Result<(), Box<dyn std::error::Error>> {
// ファイルを開く
let f = match File::open("config.yaml") {
Ok(file) => file, // 成功すればファイルハンドルを返す
Err(e) => {
// 失敗した場合、IOエラーの処理
if e.kind() == ErrorKind::NotFound {
// ファイルが見つからない場合のエラー
eprintln!("エラー: 設定ファイル 'config.yaml' が見つかりませんでした。");
} else {
// その他のIOエラー (権限不足など)
eprintln!("エラー: ファイル 'config.yaml' を開けませんでした。");
eprintln!("詳細: {}", e);
}
// エラーをBox化してmainから返す
return Err(e.into());
}
};
// serde_yaml::from_reader
let config: ApiTestConfig = match serde_yaml::from_reader(f) {
Ok(config_data) => config_data, // 成功すればパース結果を返す
Err(e) => {
// 失敗した場合、YAMLの構造エラー (デシリアライズエラー)
eprintln!("エラー: 'config.yaml' の解析に失敗しました。");
eprintln!("YAMLの構造がstruct定義と一致しているか確認してください。");
eprintln!("(例: インデントミス、必須項目の不足、型の間違いなど)");
eprintln!("\n--- パーサーエラー詳細 ---");
eprintln!("{}", e);
eprintln!("--------------------------");
// エラーをBox化してmainから返す
return Err(e.into());
}
};
// 読み込んだデータを表示
println!("--- 読み込み成功 ---");
println!("ベースURL: {}", config.base_url);
println!("\n--- テスト対象エンドポイント ---");
for (i, endpoint) in config.endpoints.iter().enumerate() {
println!("\n テスト {}: {}", i + 1, endpoint.name);
println!(" Path: {}{}", config.base_url, endpoint.path);
println!(" Method: {}", endpoint.method);
println!(" Expect: {}", endpoint.expected_status);
}
Ok(())
}
まとめ
RustでYAMLファイルを読み込む実装を整理しました。
serdeはよく使うので、他のファイル形式でも使えるようにしておくと便利です。
Discussion