use std::path::{Path, PathBuf};
use dynamic_config::figment::value::{Dict, Map, Value};
use dynamic_config::figment::{self, Metadata, Profile, Provider};
use dynamic_config::{load, source_of, Format, LoadSpec, Origin, Source};
use serde::Deserialize;
#[allow(dead_code)]
#[derive(Debug, Deserialize)]
struct Database {
host: String,
port: u16,
pool: u32,
#[serde(default)]
tls: bool,
}
struct Ini {
path: PathBuf,
text: String,
}
impl Ini {
fn read(path: impl AsRef<Path>) -> std::io::Result<Self> {
let path = path.as_ref().to_path_buf();
Ok(Self {
text: std::fs::read_to_string(&path)?,
path,
})
}
}
impl Provider for Ini {
fn metadata(&self) -> Metadata {
Metadata::from("INI file", self.path.as_path())
}
fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
let mut sections: Map<Profile, Dict> = Map::new();
let mut section = Profile::Default;
for (index, line) in self.text.lines().enumerate() {
let line = line.trim();
if line.is_empty() || line.starts_with(['#', ';']) {
continue;
}
if let Some(header) = line
.strip_prefix('[')
.and_then(|rest| rest.strip_suffix(']'))
{
section = Profile::from(header.trim());
} else if let Some((key, value)) = line.split_once('=') {
sections
.entry(section.clone())
.or_default()
.insert(key.trim().to_owned(), scalar(value.trim()));
} else {
return Err(figment::Error::from(format!(
"line {} is neither a section header nor `key = value`",
index + 1
)));
}
}
Ok(sections)
}
}
fn scalar(text: &str) -> Value {
let text = text.trim_matches('"');
if let Ok(flag) = text.parse::<bool>() {
Value::from(flag)
} else if let Ok(whole) = text.parse::<i64>() {
Value::from(whole)
} else if let Ok(real) = text.parse::<f64>() {
Value::from(real)
} else {
Value::from(text)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let base = r#"{"db": {"host": "localhost", "port": 5432, "pool": 8}}"#;
let ini = Ini::read("dynamic-config/examples/database.ini")?;
let sources = [Source::inline(base, Format::Json), Source::provider(&ini)];
let spec = LoadSpec::new("db", &sources);
println!("{:?}\n", load::<Database>(&spec)?);
for key in ["host", "port", "tls"] {
let origin = source_of(&spec, key)?.unwrap_or(Origin::Unknown);
println!("{key:<5} {origin}");
}
let broken = Ini {
path: PathBuf::from("broken.ini"),
text: String::from("[db]\nport\n"),
};
let sources = [Source::provider(&broken)];
let error = load::<Database>(&LoadSpec::new("db", &sources)).unwrap_err();
println!("\n{error}");
Ok(())
}