mod env;
mod file;
mod format;
mod paths;
mod secret;
mod sql;
use std::io;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct ReadOptions<'a> {
pub env_prefix: Option<&'a str>,
}
impl<'a> ReadOptions<'a> {
pub const fn with_env_prefix(env_prefix: &'a str) -> Self {
Self {
env_prefix: Some(env_prefix),
}
}
}
pub trait ConfigSource {
fn source_name(&self) -> String;
fn read_value(&mut self) -> io::Result<Option<serde_json::Value>>;
fn write_config<T>(&mut self, config: &T) -> io::Result<()>
where
T: serde::Serialize;
}
impl<T> ConfigSource for &mut T
where
T: ConfigSource + ?Sized,
{
fn source_name(&self) -> String {
(**self).source_name()
}
fn read_value(&mut self) -> io::Result<Option<serde_json::Value>> {
(**self).read_value()
}
fn write_config<S>(&mut self, config: &S) -> io::Result<()>
where
S: serde::Serialize,
{
(**self).write_config(config)
}
}
impl ConfigSource for &str {
fn source_name(&self) -> String {
match paths::default_config_path(self) {
Ok(path) => path.display().to_string(),
Err(_) => (*self).to_string(),
}
}
fn read_value(&mut self) -> io::Result<Option<serde_json::Value>> {
let path = paths::default_config_path(self)?;
if path.is_file() {
file::read_config_value(&path).map(Some)
} else {
Ok(None)
}
}
fn write_config<T>(&mut self, config: &T) -> io::Result<()>
where
T: serde::Serialize,
{
let path = paths::default_config_path(self)?;
file::write_config(&path, config, file::FileType::TOML)
}
}
pub fn save<T>(mut source: impl ConfigSource, config: T) -> io::Result<()>
where
T: serde::Serialize,
{
source.write_config(&config)
}
pub fn read<T>(
mut source: impl ConfigSource,
options: Option<ReadOptions<'_>>,
) -> Result<T, io::Error>
where
T: serde::de::DeserializeOwned + Default + serde::Serialize,
{
let source_name = source.source_name();
let config_value = match source.read_value()? {
Some(value) => value,
None => {
let default_config = T::default();
let default_value = serde_json::to_value(&default_config).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"failed to serialize default config before applying overrides for {source_name}: {e}"
),
)
})?;
source.write_config(&default_config)?;
default_value
}
};
process_config_value(config_value, options.and_then(|opt| opt.env_prefix), &source_name)
}
fn process_config_value<T>(
mut config_value: serde_json::Value,
env_prefix: Option<&str>,
source: &str,
) -> Result<T, io::Error>
where
T: serde::de::DeserializeOwned,
{
if let Some(prefix) = env_prefix {
config_value = env::apply_env_overrides(config_value, prefix)?;
}
secret::resolve_secret_refs(&mut config_value)?;
serde_json::from_value(config_value).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("failed to deserialize config {source} into requested type: {e}"),
)
})
}
pub use sql::postgres_store;
pub use sql::postgres_store_with_table;
pub use sql::DEFAULT_CONFIG_TABLE;
pub use sql::PostgresConfigStore;
#[cfg(test)]
mod tests;