Skip to main content

cloudiful_config/
lib.rs

1mod env;
2mod file;
3mod format;
4mod paths;
5mod secret;
6mod sql;
7
8use std::io;
9
10#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
11pub struct ReadOptions<'a> {
12    pub env_prefix: Option<&'a str>,
13}
14
15impl<'a> ReadOptions<'a> {
16    pub const fn with_env_prefix(env_prefix: &'a str) -> Self {
17        Self {
18            env_prefix: Some(env_prefix),
19        }
20    }
21}
22
23pub trait ConfigSource {
24    fn source_name(&self) -> String;
25    fn read_value(&mut self) -> io::Result<Option<serde_json::Value>>;
26    fn write_config<T>(&mut self, config: &T) -> io::Result<()>
27    where
28        T: serde::Serialize;
29}
30
31impl<T> ConfigSource for &mut T
32where
33    T: ConfigSource + ?Sized,
34{
35    fn source_name(&self) -> String {
36        (**self).source_name()
37    }
38
39    fn read_value(&mut self) -> io::Result<Option<serde_json::Value>> {
40        (**self).read_value()
41    }
42
43    fn write_config<S>(&mut self, config: &S) -> io::Result<()>
44    where
45        S: serde::Serialize,
46    {
47        (**self).write_config(config)
48    }
49}
50
51impl ConfigSource for &str {
52    fn source_name(&self) -> String {
53        match paths::default_config_path(self) {
54            Ok(path) => path.display().to_string(),
55            Err(_) => (*self).to_string(),
56        }
57    }
58
59    fn read_value(&mut self) -> io::Result<Option<serde_json::Value>> {
60        let path = paths::default_config_path(self)?;
61        if path.is_file() {
62            file::read_config_value(&path).map(Some)
63        } else {
64            Ok(None)
65        }
66    }
67
68    fn write_config<T>(&mut self, config: &T) -> io::Result<()>
69    where
70        T: serde::Serialize,
71    {
72        let path = paths::default_config_path(self)?;
73        file::write_config(&path, config, file::FileType::TOML)
74    }
75}
76
77pub fn save<T>(mut source: impl ConfigSource, config: T) -> io::Result<()>
78where
79    T: serde::Serialize,
80{
81    source.write_config(&config)
82}
83
84pub fn read<T>(
85    mut source: impl ConfigSource,
86    options: Option<ReadOptions<'_>>,
87) -> Result<T, io::Error>
88where
89    T: serde::de::DeserializeOwned + Default + serde::Serialize,
90{
91    let source_name = source.source_name();
92    let config_value = match source.read_value()? {
93        Some(value) => value,
94        None => {
95            let default_config = T::default();
96                    let default_value = serde_json::to_value(&default_config).map_err(|e| {
97                        io::Error::new(
98                            io::ErrorKind::InvalidData,
99                    format!(
100                        "failed to serialize default config before applying overrides for {source_name}: {e}"
101                            ),
102                        )
103                    })?;
104                    source.write_config(&default_config)?;
105                    default_value
106                }
107            };
108
109    process_config_value(config_value, options.and_then(|opt| opt.env_prefix), &source_name)
110}
111
112fn process_config_value<T>(
113    mut config_value: serde_json::Value,
114    env_prefix: Option<&str>,
115    source: &str,
116) -> Result<T, io::Error>
117where
118    T: serde::de::DeserializeOwned,
119{
120    if let Some(prefix) = env_prefix {
121        config_value = env::apply_env_overrides(config_value, prefix)?;
122    }
123
124    secret::resolve_secret_refs(&mut config_value)?;
125
126    serde_json::from_value(config_value).map_err(|e| {
127        io::Error::new(
128            io::ErrorKind::InvalidData,
129            format!("failed to deserialize config {source} into requested type: {e}"),
130        )
131    })
132}
133
134pub use sql::postgres_store;
135pub use sql::postgres_store_with_table;
136pub use sql::DEFAULT_CONFIG_TABLE;
137pub use sql::PostgresConfigStore;
138
139#[cfg(test)]
140mod tests;