Skip to main content

cloudiful_config/
lib.rs

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