Skip to main content

cloudiful_config/
lib.rs

1mod env;
2mod file;
3mod format;
4mod paths;
5mod secret;
6mod sql;
7
8use std::io;
9use std::path::Path;
10
11#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
12pub enum DotenvOptions<'a> {
13    #[default]
14    Enabled,
15    Disabled,
16    Path(&'a Path),
17}
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
20pub struct ReadOptions<'a> {
21    pub env_prefix: Option<&'a str>,
22    pub dotenv: DotenvOptions<'a>,
23}
24
25impl Default for ReadOptions<'_> {
26    fn default() -> Self {
27        Self {
28            env_prefix: None,
29            dotenv: DotenvOptions::Enabled,
30        }
31    }
32}
33
34impl<'a> ReadOptions<'a> {
35    pub const fn with_env_prefix(env_prefix: &'a str) -> Self {
36        Self {
37            env_prefix: Some(env_prefix),
38            dotenv: DotenvOptions::Enabled,
39        }
40    }
41
42    pub const fn without_dotenv(mut self) -> Self {
43        self.dotenv = DotenvOptions::Disabled;
44        self
45    }
46
47    pub const fn with_dotenv_path(mut self, path: &'a Path) -> Self {
48        self.dotenv = DotenvOptions::Path(path);
49        self
50    }
51
52    pub const fn with_dotenv(mut self) -> Self {
53        self.dotenv = DotenvOptions::Enabled;
54        self
55    }
56}
57
58fn load_dotenv(options: DotenvOptions<'_>) -> io::Result<()> {
59    let result = match options {
60        DotenvOptions::Enabled => dotenvy::from_path(".env").map(|_| ()),
61        DotenvOptions::Disabled => return Ok(()),
62        DotenvOptions::Path(path) => dotenvy::from_path(path).map(|_| ()),
63    };
64
65    match result {
66        Ok(()) => Ok(()),
67        Err(dotenvy::Error::Io(err))
68            if matches!(options, DotenvOptions::Enabled)
69                && err.kind() == io::ErrorKind::NotFound =>
70        {
71            Ok(())
72        }
73        Err(err) => {
74            let source = match options {
75                DotenvOptions::Enabled => ".env".to_string(),
76                DotenvOptions::Disabled => unreachable!("disabled dotenv already returned"),
77                DotenvOptions::Path(path) => path.display().to_string(),
78            };
79
80            Err(io::Error::new(
81                io::ErrorKind::InvalidData,
82                format!("failed to load dotenv file {source}: {err}"),
83            ))
84        }
85    }
86}
87
88pub trait ConfigSource {
89    fn source_name(&self) -> String;
90    fn read_value(&mut self) -> io::Result<Option<serde_json::Value>>;
91    fn write_config<T>(&mut self, config: &T) -> io::Result<()>
92    where
93        T: serde::Serialize;
94}
95
96impl<T> ConfigSource for &mut T
97where
98    T: ConfigSource + ?Sized,
99{
100    fn source_name(&self) -> String {
101        (**self).source_name()
102    }
103
104    fn read_value(&mut self) -> io::Result<Option<serde_json::Value>> {
105        (**self).read_value()
106    }
107
108    fn write_config<S>(&mut self, config: &S) -> io::Result<()>
109    where
110        S: serde::Serialize,
111    {
112        (**self).write_config(config)
113    }
114}
115
116impl ConfigSource for &str {
117    fn source_name(&self) -> String {
118        match paths::default_config_path(self) {
119            Ok(path) => path.display().to_string(),
120            Err(_) => (*self).to_string(),
121        }
122    }
123
124    fn read_value(&mut self) -> io::Result<Option<serde_json::Value>> {
125        let path = paths::default_config_path(self)?;
126        if path.is_file() {
127            file::read_config_value(&path).map(Some)
128        } else {
129            Ok(None)
130        }
131    }
132
133    fn write_config<T>(&mut self, config: &T) -> io::Result<()>
134    where
135        T: serde::Serialize,
136    {
137        let path = paths::default_config_path(self)?;
138        file::write_config(&path, config, file::FileType::TOML)
139    }
140}
141
142pub fn save<T>(mut source: impl ConfigSource, config: T) -> io::Result<()>
143where
144    T: serde::Serialize,
145{
146    source.write_config(&config)
147}
148
149pub fn read<T>(
150    mut source: impl ConfigSource,
151    options: Option<ReadOptions<'_>>,
152) -> Result<T, io::Error>
153where
154    T: serde::de::DeserializeOwned + Default + serde::Serialize,
155{
156    let options = options.unwrap_or_default();
157    load_dotenv(options.dotenv)?;
158
159    let source_name = source.source_name();
160    let config_value = match source.read_value()? {
161        Some(value) => value,
162        None => {
163            let default_config = T::default();
164            let default_value = serde_json::to_value(&default_config).map_err(|e| {
165                io::Error::new(
166                    io::ErrorKind::InvalidData,
167                    format!(
168                        "failed to serialize default config before applying overrides for {source_name}: {e}"
169                    ),
170                )
171            })?;
172            source.write_config(&default_config)?;
173            default_value
174        }
175    };
176
177    process_config_value(config_value, options.env_prefix, &source_name)
178}
179
180fn process_config_value<T>(
181    mut config_value: serde_json::Value,
182    env_prefix: Option<&str>,
183    source: &str,
184) -> Result<T, io::Error>
185where
186    T: serde::de::DeserializeOwned,
187{
188    if let Some(prefix) = env_prefix {
189        config_value = env::apply_env_overrides(config_value, prefix)?;
190    }
191
192    secret::resolve_secret_refs(&mut config_value)?;
193
194    serde_json::from_value(config_value).map_err(|e| {
195        io::Error::new(
196            io::ErrorKind::InvalidData,
197            format!("failed to deserialize config {source} into requested type: {e}"),
198        )
199    })
200}
201
202pub use sql::postgres_store;
203pub use sql::postgres_store_with_table;
204pub use sql::DEFAULT_CONFIG_TABLE;
205pub use sql::PostgresConfigStore;
206
207#[cfg(test)]
208mod tests;