Skip to main content

cloudiful_config/
lib.rs

1mod env;
2mod file;
3
4use std::io;
5use std::path::Path;
6
7pub use file::FileType;
8
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub enum ConfigSource<P, S> {
11    File(P),
12    Env { prefix: S },
13    FileWithEnv { path: P, prefix: S },
14}
15
16/// Save the current config to a TOML or JSON file.
17///
18/// The `file_type` must match the file extension inferred from `path`.
19/// `.jsonc` paths are written as standard JSON and therefore require
20/// [`FileType::JSON`].
21///
22/// ```rust,no_run
23/// use cloudiful_config::{FileType, save};
24/// use serde::Serialize;
25///
26/// #[derive(Serialize)]
27/// struct AppConfig {
28///     port: u16,
29/// }
30///
31/// let path = std::env::temp_dir().join("config-crate-save-example.toml");
32/// save(&path, AppConfig { port: 8080 }, FileType::TOML).unwrap();
33/// ```
34pub fn save<P, T>(path: P, config: T, file_type: FileType) -> io::Result<()>
35where
36    P: AsRef<Path>,
37    T: serde::Serialize,
38{
39    file::write_config(path.as_ref(), &config, file_type)
40}
41
42/// Save the current config by inferring the format from the file extension.
43///
44/// `.toml` writes TOML. `.json` and `.jsonc` write standard JSON.
45///
46/// ```rust,no_run
47/// use cloudiful_config::save_inferred;
48/// use serde::Serialize;
49///
50/// #[derive(Serialize)]
51/// struct AppConfig {
52///     debug: bool,
53/// }
54///
55/// let path = std::env::temp_dir().join("config-crate-save-inferred.jsonc");
56/// save_inferred(&path, AppConfig { debug: true }).unwrap();
57/// ```
58pub fn save_inferred<P, T>(path: P, config: T) -> io::Result<()>
59where
60    P: AsRef<Path>,
61    T: serde::Serialize,
62{
63    file::write_config_inferred(path.as_ref(), &config)
64}
65
66/// Read config from an existing TOML, JSON, or JSONC file.
67///
68/// `.jsonc` accepts both `//` line comments and `/* ... */` block comments.
69///
70/// ```rust,no_run
71/// use cloudiful_config::read_existing;
72/// use serde::Deserialize;
73///
74/// #[derive(Deserialize)]
75/// struct AppConfig {
76///     port: u16,
77/// }
78///
79/// let path = std::env::temp_dir().join("config-crate-read-existing.toml");
80/// let _config: AppConfig = read_existing(&path).unwrap();
81/// ```
82pub fn read_existing<P, T>(path: P) -> Result<T, io::Error>
83where
84    P: AsRef<Path>,
85    T: serde::de::DeserializeOwned,
86{
87    file::read_config(path.as_ref())
88}
89
90/// Read config from a TOML, JSON, or JSONC file, creating the file with
91/// `T::default()` when it does not already exist.
92///
93/// ```rust,no_run
94/// use cloudiful_config::read_or_create_default;
95/// use serde::{Deserialize, Serialize};
96///
97/// #[derive(Default, Deserialize, Serialize)]
98/// struct AppConfig {
99///     port: u16,
100/// }
101///
102/// let path = std::env::temp_dir().join("config-crate-read-or-create.toml");
103/// let _config: AppConfig = read_or_create_default(&path).unwrap();
104/// ```
105pub fn read_or_create_default<P, T>(path: P) -> Result<T, io::Error>
106where
107    P: AsRef<Path>,
108    T: serde::de::DeserializeOwned + Default + serde::Serialize,
109{
110    let path = path.as_ref();
111    let file_type = file::infer_file_type(path)?;
112
113    if !path.is_file() {
114        let default_config = T::default();
115        file::write_config(path, &default_config, file_type)?;
116        return Ok(default_config);
117    }
118
119    file::read_config(path)
120}
121
122/// Read config from an explicit source.
123///
124/// `ConfigSource::File` keeps the historical behavior of creating the file from
125/// `T::default()` when it does not exist.
126///
127/// Environment variables map to fields by lowercasing the suffix after
128/// `prefix`. Use `__` for nested fields and provide JSON literals for typed
129/// values such as arrays and booleans.
130///
131/// ```rust,no_run
132/// use cloudiful_config::{ConfigSource, read};
133/// use serde::{Deserialize, Serialize};
134///
135/// #[derive(Default, Deserialize, Serialize)]
136/// struct DatabaseConfig {
137///     url: String,
138/// }
139///
140/// #[derive(Default, Deserialize, Serialize)]
141/// struct AppConfig {
142///     database: DatabaseConfig,
143/// }
144///
145/// let path = std::env::temp_dir().join("config-crate-read-with-env.toml");
146/// unsafe {
147///     std::env::set_var("APP_DATABASE__URL", "\"postgres://db/service\"");
148/// }
149/// let _config: AppConfig = read(ConfigSource::FileWithEnv {
150///     path: &path,
151///     prefix: "APP_",
152/// })
153/// .unwrap();
154/// unsafe {
155///     std::env::remove_var("APP_DATABASE__URL");
156/// }
157/// ```
158pub fn read<P, S, T>(source: ConfigSource<P, S>) -> Result<T, io::Error>
159where
160    P: AsRef<Path>,
161    S: AsRef<str>,
162    T: serde::de::DeserializeOwned + Default + serde::Serialize,
163{
164    match source {
165        ConfigSource::File(path) => read_or_create_default(path),
166        ConfigSource::Env { prefix } => env::apply_env_overrides(T::default(), prefix.as_ref()),
167        ConfigSource::FileWithEnv { path, prefix } => {
168            let config = read_or_create_default(path)?;
169            env::apply_env_overrides(config, prefix.as_ref())
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests;