#[cfg(feature = "ini")]
use std::path::PathBuf;
#[cfg(feature = "ini")]
use figment::value::Map;
use figment::value::{Dict, Value};
#[cfg(feature = "ini")]
use figment::{Metadata, Profile, Provider};
#[cfg(feature = "ini")]
pub(crate) struct Ini {
text: Result<String, String>,
path: Option<PathBuf>,
}
#[cfg(feature = "ini")]
impl Ini {
pub(crate) fn file(path: impl Into<PathBuf>) -> Self {
let path = path.into();
Self {
text: std::fs::read_to_string(&path).map_err(|error| error.to_string()),
path: Some(path),
}
}
pub(crate) fn string(text: &str) -> Self {
Self {
text: Ok(text.to_owned()),
path: None,
}
}
}
#[cfg(feature = "ini")]
impl Provider for Ini {
fn metadata(&self) -> Metadata {
match &self.path {
Some(path) => Metadata::from("INI file", path.as_path()),
None => Metadata::named("INI source"),
}
}
fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
let text = match &self.text {
Ok(text) => text,
Err(error) => return Err(figment::Error::from(error.clone())),
};
let mut root = Dict::new();
let mut section: Vec<String> = Vec::new();
for (index, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
if let Some(header) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
section = header
.split('.')
.map(|part| part.trim().to_owned())
.collect();
if section.iter().any(String::is_empty) {
return Err(figment::Error::from(format!(
"line {}: an empty section name",
index + 1
)));
}
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
if key.is_empty() {
return Err(figment::Error::from(format!(
"line {}: `= value` with no key",
index + 1
)));
}
let mut path: Vec<&str> = section.iter().map(String::as_str).collect();
path.push(key);
insert(&mut root, &path, scalar(value.trim()), index + 1)
.map_err(figment::Error::from)?;
continue;
}
return Err(figment::Error::from(format!(
"line {} is neither a section header nor `key = value`",
index + 1
)));
}
Ok(Map::from([(Profile::Default, root)]))
}
}
pub(super) fn insert(
root: &mut Dict,
path: &[&str],
value: Value,
line: usize,
) -> Result<(), String> {
let (last, walk) = path.split_last().expect("a key is never empty");
let mut here = root;
for part in walk {
let slot = here
.entry((*part).to_owned())
.or_insert_with(|| Value::from(Dict::new()));
match slot {
Value::Dict(_, dict) => here = dict,
_ => {
return Err(format!(
"line {line}: `{part}` is already a value, so it cannot also \
hold `{last}`"
));
}
}
}
if let Some(Value::Dict(..)) = here.get(*last) {
return Err(format!(
"line {line}: `{last}` is already a table, so it cannot also be a value"
));
}
here.insert((*last).to_owned(), value);
Ok(())
}
pub(super) fn scalar(text: &str) -> Value {
if text.len() >= 2 && text.starts_with('"') && text.ends_with('"') {
return Value::from(&text[1..text.len() - 1]);
}
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)
}
}