Skip to main content

cloudiful_config/
lib.rs

1mod env;
2mod file;
3mod paths;
4mod secret;
5
6use std::io;
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
9pub struct ReadOptions<'a> {
10    pub env_prefix: Option<&'a str>,
11}
12
13impl<'a> ReadOptions<'a> {
14    pub const fn with_env_prefix(env_prefix: &'a str) -> Self {
15        Self {
16            env_prefix: Some(env_prefix),
17        }
18    }
19}
20
21/// Save config to the platform-default `config.toml` for `app_name`.
22///
23/// On macOS, `stock` resolves to
24/// `~/Library/Application Support/stock/config.toml`.
25///
26/// ```rust,no_run
27/// use cloudiful_config::save;
28/// use serde::Serialize;
29///
30/// #[derive(Serialize)]
31/// struct AppConfig {
32///     port: u16,
33/// }
34///
35/// save("stock", AppConfig { port: 8080 }).unwrap();
36/// ```
37pub fn save<T>(app_name: &str, config: T) -> io::Result<()>
38where
39    T: serde::Serialize,
40{
41    let path = paths::default_config_path(app_name)?;
42    file::write_config(&path, &config, file::FileType::TOML)
43}
44
45/// Read config from the platform-default `config.toml` for `app_name`,
46/// creating the file from `T::default()` when it does not already exist.
47///
48/// Use [`ReadOptions`] to apply environment variable overrides after the file
49/// is loaded.
50///
51/// ```rust,no_run
52/// use cloudiful_config::{ReadOptions, read};
53/// use serde::{Deserialize, Serialize};
54///
55/// #[derive(Default, Deserialize, Serialize)]
56/// struct AppConfig {
57///     port: u16,
58/// }
59///
60/// let _config: AppConfig = read("stock", Some(ReadOptions::with_env_prefix("STOCK_"))).unwrap();
61/// ```
62pub fn read<T>(app_name: &str, options: Option<ReadOptions<'_>>) -> Result<T, io::Error>
63where
64    T: serde::de::DeserializeOwned + Default + serde::Serialize,
65{
66    let path = paths::default_config_path(app_name)?;
67    let mut config_value = if !path.is_file() {
68        let default_config = T::default();
69        file::write_config(&path, &default_config, file::FileType::TOML)?;
70        serde_json::to_value(default_config).map_err(|e| {
71            io::Error::new(
72                io::ErrorKind::InvalidData,
73                format!(
74                    "failed to serialize default config before applying overrides for {}: {e}",
75                    path.display()
76                ),
77            )
78        })?
79    } else {
80        file::read_config_value(&path)?
81    };
82
83    if let Some(prefix) = options.and_then(|options| options.env_prefix) {
84        config_value = env::apply_env_overrides(config_value, prefix)?;
85    }
86
87    secret::resolve_secret_refs(&mut config_value)?;
88
89    serde_json::from_value(config_value).map_err(|e| {
90        io::Error::new(
91            io::ErrorKind::InvalidData,
92            format!(
93                "failed to deserialize config {} into requested type: {e}",
94                path.display()
95            ),
96        )
97    })
98}
99
100#[cfg(test)]
101mod tests;