toml
シリアライゼーションライブラリ
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 からValueenum へのマッピングValueenum: String、Integer、Float、Boolean、Datetime、Array、Table を含むtoml::from_str: TOML文字列からのデシリアライゼーションtoml::to_string: TOMLフォーマットへのシリアライゼーション
メリット・デメリット
メリット
- 優れた可読性と編集のしやすさ
- Rustコミュニティの標準フォーマット
- 豊富なデータ型のサポート
- セマンティックバージョニングとの親和性
- コメント機能による設定の文書化
- 構文エラーの分かりやすいエラーメッセージ
デメリット
- JSONやYAMLと比較して採用例が少ない
- 複雑なネスト構造には向かない
- スキーマ検証機能は別途必要
- 一部の高度な機能(参照、アンカーなど)は非対応
参考ページ
- TOML公式サイト: https://toml.io/
- crates.io: https://crates.io/crates/toml
- ドキュメント: https://docs.rs/toml/
- Cargo Book: https://doc.rust-lang.org/cargo/
書き方の例
基本的な使用方法
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
);
}
}
}
}