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;