toml

シリアライゼーションRustTOML設定ファイルCargo

シリアライゼーションライブラリ

toml

概要

toml crateは、Rust用のTOML(Tom's Obvious, Minimal Language)パーサーおよびシリアライザーです。TOMLは人間が読み書きしやすい設定ファイル形式で、RustコミュニティではCargo.tomlをはじめとする設定ファイルの標準フォーマットとして広く採用されています。Serdeとの完全な統合により、型安全な設定管理を実現します。

詳細

toml crateは、TOML形式のデータを読み書き、パースするための包括的な機能を提供します。TOMLは最小限でわかりやすい構文を持ち、人間にも機械にも扱いやすい設計となっています。

主な特徴:

  • 人間が読みやすい形式: 直感的なキー・値ペア構造
  • Serde統合: SerdeのSerialize/Deserializeトレイトとの完全な互換性
  • 豊富なデータ型: 文字列、数値、ブール値、日時、配列、テーブルをサポート
  • エラー処理: 詳細なエラーメッセージとパース位置情報
  • Cargoとの親和性: Rustエコシステムの標準設定フォーマット

TOML形式の特徴:

  • セクション(テーブル)による階層構造
  • コメントのサポート(#)
  • マルチライン文字列
  • 日付・時刻のネイティブサポート
  • インラインテーブルと配列

技術的詳細:

  • Table型: String から Value enum へのマッピング
  • Value enum: String、Integer、Float、Boolean、Datetime、Array、Table を含む
  • toml::from_str: TOML文字列からのデシリアライゼーション
  • toml::to_string: TOMLフォーマットへのシリアライゼーション

メリット・デメリット

メリット

  • 優れた可読性と編集のしやすさ
  • Rustコミュニティの標準フォーマット
  • 豊富なデータ型のサポート
  • セマンティックバージョニングとの親和性
  • コメント機能による設定の文書化
  • 構文エラーの分かりやすいエラーメッセージ

デメリット

  • JSONやYAMLと比較して採用例が少ない
  • 複雑なネスト構造には向かない
  • スキーマ検証機能は別途必要
  • 一部の高度な機能(参照、アンカーなど)は非対応

参考ページ

書き方の例

基本的な使用方法

use serde::{Serialize, Deserialize};
use toml;

#[derive(Serialize, Deserialize, Debug)]
struct Config {
    title: String,
    owner: Owner,
    database: Database,
}

#[derive(Serialize, Deserialize, Debug)]
struct Owner {
    name: String,
    email: String,
}

#[derive(Serialize, Deserialize, Debug)]
struct Database {
    server: String,
    port: u16,
    max_connections: u32,
    enabled: bool,
}

fn main() -> Result<(), toml::de::Error> {
    let toml_str = r#"
        title = "My Application"

        [owner]
        name = "John Doe"
        email = "[email protected]"

        [database]
        server = "192.168.1.1"
        port = 5432
        max_connections = 100
        enabled = true
    "#;
    
    // TOMLからデシリアライズ
    let config: Config = toml::from_str(toml_str)?;
    println!("Config: {:#?}", config);
    
    // TOMLへシリアライズ
    let toml_string = toml::to_string(&config)?;
    println!("Serialized:\n{}", toml_string);
    
    Ok(())
}

Cargo.toml風の設定ファイル

use serde::{Serialize, Deserialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug)]
struct Package {
    name: String,
    version: String,
    edition: String,
    authors: Vec<String>,
    description: Option<String>,
    license: Option<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Dependency {
    version: Option<String>,
    features: Option<Vec<String>>,
    default_features: Option<bool>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Manifest {
    package: Package,
    dependencies: HashMap<String, toml::Value>,
    #[serde(rename = "dev-dependencies")]
    dev_dependencies: Option<HashMap<String, toml::Value>>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let toml_content = r#"
        [package]
        name = "my-project"
        version = "0.1.0"
        edition = "2021"
        authors = ["Alice <[email protected]>", "Bob <[email protected]>"]
        description = "A sample Rust project"
        license = "MIT"

        [dependencies]
        serde = { version = "1.0", features = ["derive"] }
        tokio = { version = "1.0", features = ["full"] }
        reqwest = "0.11"

        [dev-dependencies]
        criterion = "0.5"
    "#;
    
    let manifest: Manifest = toml::from_str(toml_content)?;
    println!("Project name: {}", manifest.package.name);
    println!("Dependencies: {:#?}", manifest.dependencies);
    
    Ok(())
}

ファイルからの読み込みと書き込み

use serde::{Serialize, Deserialize};
use std::fs;

#[derive(Serialize, Deserialize, Debug)]
struct AppConfig {
    app: AppSettings,
    server: ServerSettings,
    features: Features,
}

#[derive(Serialize, Deserialize, Debug)]
struct AppSettings {
    name: String,
    version: String,
    debug: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct ServerSettings {
    host: String,
    port: u16,
    workers: usize,
    timeout: u64,
}

#[derive(Serialize, Deserialize, Debug)]
struct Features {
    authentication: bool,
    logging: bool,
    metrics: bool,
}

fn load_config(path: &str) -> Result<AppConfig, Box<dyn std::error::Error>> {
    let contents = fs::read_to_string(path)?;
    let config = toml::from_str(&contents)?;
    Ok(config)
}

fn save_config(config: &AppConfig, path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let toml_string = toml::to_string_pretty(config)?;
    fs::write(path, toml_string)?;
    Ok(())
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = AppConfig {
        app: AppSettings {
            name: "MyApp".to_string(),
            version: "1.0.0".to_string(),
            debug: false,
        },
        server: ServerSettings {
            host: "127.0.0.1".to_string(),
            port: 8080,
            workers: 4,
            timeout: 30,
        },
        features: Features {
            authentication: true,
            logging: true,
            metrics: false,
        },
    };
    
    // 設定を保存
    save_config(&config, "config.toml")?;
    
    // 設定を読み込み
    let loaded_config = load_config("config.toml")?;
    println!("Loaded config: {:#?}", loaded_config);
    
    Ok(())
}

複雑なデータ構造

use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};

#[derive(Serialize, Deserialize, Debug)]
struct Project {
    meta: Metadata,
    environments: Vec<Environment>,
    tasks: Vec<Task>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Metadata {
    created_at: DateTime<Utc>,
    updated_at: DateTime<Utc>,
    tags: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug)]
struct Environment {
    name: String,
    variables: toml::Table,
    active: bool,
}

#[derive(Serialize, Deserialize, Debug)]
struct Task {
    id: u32,
    name: String,
    command: String,
    args: Vec<String>,
    env: Option<String>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let toml_content = r#"
        [meta]
        created_at = 2024-01-01T00:00:00Z
        updated_at = 2024-01-15T12:00:00Z
        tags = ["rust", "project", "example"]

        [[environments]]
        name = "development"
        active = true
        
        [environments.variables]
        DATABASE_URL = "postgresql://localhost/dev_db"
        LOG_LEVEL = "debug"
        
        [[environments]]
        name = "production"
        active = false
        
        [environments.variables]
        DATABASE_URL = "postgresql://prod.example.com/prod_db"
        LOG_LEVEL = "info"

        [[tasks]]
        id = 1
        name = "build"
        command = "cargo"
        args = ["build", "--release"]
        
        [[tasks]]
        id = 2
        name = "test"
        command = "cargo"
        args = ["test"]
        env = "development"
    "#;
    
    let project: Project = toml::from_str(toml_content)?;
    
    println!("Project created at: {}", project.meta.created_at);
    println!("Environments:");
    for env in &project.environments {
        println!("  - {} (active: {})", env.name, env.active);
        for (key, value) in &env.variables {
            println!("    {}: {}", key, value);
        }
    }
    
    Ok(())
}

動的なTOML操作

use toml::{Table, Value};

fn main() {
    // 動的にTOMLを構築
    let mut root = Table::new();
    
    // 基本的な値を追加
    root.insert("title".to_string(), Value::String("Dynamic Config".to_string()));
    root.insert("version".to_string(), Value::Integer(1));
    root.insert("enabled".to_string(), Value::Boolean(true));
    
    // ネストしたテーブルを作成
    let mut database = Table::new();
    database.insert("host".to_string(), Value::String("localhost".to_string()));
    database.insert("port".to_string(), Value::Integer(5432));
    root.insert("database".to_string(), Value::Table(database));
    
    // 配列を追加
    let features = vec![
        Value::String("async".to_string()),
        Value::String("logging".to_string()),
        Value::String("metrics".to_string()),
    ];
    root.insert("features".to_string(), Value::Array(features));
    
    // TOMLとして出力
    let toml_string = toml::to_string_pretty(&root).unwrap();
    println!("{}", toml_string);
    
    // 値へのアクセス
    if let Some(Value::String(title)) = root.get("title") {
        println!("Title: {}", title);
    }
    
    if let Some(Value::Table(db)) = root.get("database") {
        if let Some(Value::Integer(port)) = db.get("port") {
            println!("Database port: {}", port);
        }
    }
}

エラーハンドリング

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize, Debug)]
struct StrictConfig {
    name: String,
    port: u16,
    timeout: u32,
}

fn parse_config(toml_str: &str) -> Result<StrictConfig, toml::de::Error> {
    toml::from_str(toml_str)
}

fn main() {
    // 正常なTOML
    let valid_toml = r#"
        name = "MyService"
        port = 8080
        timeout = 30
    "#;
    
    match parse_config(valid_toml) {
        Ok(config) => println!("Valid config: {:?}", config),
        Err(e) => eprintln!("Error: {}", e),
    }
    
    // 型エラーを含むTOML
    let invalid_toml = r#"
        name = "MyService"
        port = "not a number"
        timeout = 30
    "#;
    
    match parse_config(invalid_toml) {
        Ok(config) => println!("Config: {:?}", config),
        Err(e) => {
            eprintln!("Parse error: {}", e);
            // エラーの詳細情報
            if let Some(span) = e.span() {
                eprintln!("Error at line {}, column {}", 
                    span.start + 1, 
                    span.end + 1
                );
            }
        }
    }
}