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;