use std::path::PathBuf;
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum ConfigSource {
Defaults,
File(PathBuf),
MergeFile(PathBuf),
Env { prefix: Option<String> },
#[doc(hidden)]
#[cfg(feature = "config")]
TomlTable(NestedTomlTable),
#[doc(hidden)]
#[cfg(feature = "config")]
MergeTomlTable(NestedTomlTable),
}
#[doc(hidden)]
#[cfg(feature = "config")]
#[derive(Debug, Clone)]
pub struct NestedTomlTable(toml::Value);
#[cfg(feature = "config")]
impl NestedTomlTable {
pub fn from_value(value: toml::Value) -> Self {
NestedTomlTable(value)
}
pub fn get(&self, key: &str) -> Option<NestedTomlTable> {
self.0.get(key).cloned().map(NestedTomlTable)
}
pub fn deserialize<T: serde::de::DeserializeOwned>(
&self,
) -> Result<T, toml::de::Error> {
self.0.clone().try_into()
}
pub fn flatten_into(
&self,
out: &mut std::collections::HashMap<String, String>,
) {
flatten_toml("", &self.0, out);
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ConfigError {
MissingField { field: &'static str },
ParseError {
field: &'static str,
source: String,
message: String,
},
Io(std::io::Error),
Format { path: PathBuf, message: String },
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::MissingField { field } => {
write!(f, "missing required config field `{field}`")
}
ConfigError::ParseError {
field,
source,
message,
} => write!(
f,
"failed to parse config field `{field}` from {source}: {message}"
),
ConfigError::Io(e) => write!(f, "config I/O error: {e}"),
ConfigError::Format { path, message } => {
write!(f, "config format error in {}: {}", path.display(), message)
}
}
}
}
impl std::error::Error for ConfigError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ConfigError::Io(e) => Some(e),
_ => None,
}
}
}
impl From<std::io::Error> for ConfigError {
fn from(e: std::io::Error) -> Self {
ConfigError::Io(e)
}
}
#[derive(Debug, Clone)]
pub struct ConfigFieldMeta {
pub name: &'static str,
pub type_name: &'static str,
pub env_var: Option<&'static str>,
pub file_key: Option<&'static str>,
pub default: Option<&'static str>,
pub help: Option<&'static str>,
pub required: bool,
pub nested: Option<&'static [ConfigFieldMeta]>,
pub env_prefix: Option<&'static str>,
}
pub trait ConfigLoad: Sized {
fn load(sources: &[ConfigSource]) -> Result<Self, ConfigError>;
fn field_meta() -> &'static [ConfigFieldMeta];
}
#[cfg(feature = "config")]
pub fn load_toml_file_raw(
path: &std::path::Path,
) -> Result<Option<NestedTomlTable>, ConfigError> {
use std::io::ErrorKind;
let contents = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(ConfigError::Io(e)),
};
let value: toml::Value = contents.parse().map_err(|e: toml::de::Error| {
ConfigError::Format {
path: path.to_owned(),
message: e.to_string(),
}
})?;
Ok(Some(NestedTomlTable::from_value(value)))
}
#[cfg(feature = "config")]
pub fn load_toml_file(
path: &std::path::Path,
) -> Result<Option<std::collections::HashMap<String, String>>, ConfigError> {
use std::io::ErrorKind;
let contents = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(None),
Err(e) => return Err(ConfigError::Io(e)),
};
let value: toml::Value = contents.parse().map_err(|e: toml::de::Error| {
ConfigError::Format {
path: path.to_owned(),
message: e.to_string(),
}
})?;
let mut map = std::collections::HashMap::new();
flatten_toml("", &value, &mut map);
Ok(Some(map))
}
#[cfg(feature = "config")]
fn flatten_toml(
prefix: &str,
value: &toml::Value,
out: &mut std::collections::HashMap<String, String>,
) {
match value {
toml::Value::Table(table) => {
for (k, v) in table {
let key = if prefix.is_empty() {
k.clone()
} else {
format!("{prefix}.{k}")
};
flatten_toml(&key, v, out);
}
}
toml::Value::String(s) => {
out.insert(prefix.to_owned(), s.clone());
}
other => {
out.insert(prefix.to_owned(), other.to_string());
}
}
}