😋

RustでYAMLファイルを読み込む

に公開

背景

私は自動化できることを手作業で行うのが嫌いです。
タイミングの問題などで、手作業で行わなければならない場合などは仕方がないのですが、できるだけ自動化したいと考えています。
最近、APIの開発が多くなってきました。
APIの統合テストが面倒臭いので、テストを自動化するツールを作成しています。
このツールの開発を進める中で得た知見として、この記事に残しておこうと思います。

概要

この記事では、RustでYAMLファイルを読み込み、各要素を扱えるようにします。

詳細

  • YAMLファイルを読み込むために、serdeとserde_yamlを使用します
  • ファイルを読み込む前の前提として、YAMLファイルの構造に合わせて、Structを定義し、#[derive(Deserialize)] を指定します
  • 処理の流れは、
    1. std::fs::File でファイルを開く
    2. serde_yaml::from_reader に渡す
  • 前提として、#[derive(Deserialize)] を定義しているので、serde_yaml::from_reader にデータを渡すと、そのままマッピングされます
  • serde_yaml::from_readerserde_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