rcman/manager/builder.rs
1//! Builder for `SettingsManager`
2//!
3//! This module contains [`SettingsManagerBuilder`] which provides a fluent API
4//! for creating a [`SettingsManager`](super::SettingsManager).
5
6use crate::config::SettingsConfigBuilder;
7use crate::config::SettingsSchema;
8use crate::error::Result;
9use crate::storage::StorageBackend;
10use crate::sub_settings::SubSettingsConfig;
11use std::path::PathBuf;
12
13use super::SettingsManager;
14
15/// Builder for creating a [`SettingsManager`] with a fluent API.
16///
17/// This is the recommended way to create a `SettingsManager`. It allows you to
18/// configure all options and register sub-settings in a single chain of calls.
19///
20/// # Example
21///
22/// ```rust,no_run
23/// use rcman::{SettingsManager, SubSettingsConfig};
24///
25/// let manager = SettingsManager::builder("my-app", "1.0.0")
26/// .with_config_dir("~/.config/my-app")
27/// .with_credentials()
28/// .with_sub_settings(SubSettingsConfig::new("remotes"))
29/// .with_sub_settings(SubSettingsConfig::singlefile("backends"))
30/// .build()
31/// .unwrap();
32/// ```
33pub struct SettingsManagerBuilder<
34 S: StorageBackend = crate::storage::JsonStorage,
35 Schema: SettingsSchema = (),
36> {
37 config_builder: SettingsConfigBuilder<S, Schema>,
38 sub_settings: Vec<SubSettingsConfig>,
39}
40
41impl<S: StorageBackend, Schema: SettingsSchema> SettingsManagerBuilder<S, Schema> {
42 /// Create a new builder with required app name and version.
43 pub fn new(
44 app_name: impl Into<String>,
45 app_version: impl Into<String>,
46 ) -> SettingsManagerBuilder {
47 SettingsManagerBuilder {
48 config_builder: SettingsConfigBuilder::new(app_name, app_version),
49 sub_settings: Vec::new(),
50 }
51 }
52}
53
54impl<S: StorageBackend, Schema: SettingsSchema> SettingsManagerBuilder<S, Schema> {
55 /// Set the configuration directory path.
56 ///
57 /// If not set, uses the system config directory.
58 #[must_use]
59 pub fn with_config_dir(mut self, path: impl Into<PathBuf>) -> Self {
60 self.config_builder = self.config_builder.with_config_dir(path);
61 self
62 }
63
64 /// Set the settings filename (default: "settings.json").
65 #[must_use]
66 pub fn with_settings_file(mut self, filename: impl Into<String>) -> Self {
67 self.config_builder = self.config_builder.settings_file(filename);
68 self
69 }
70
71 /// Enable credential management for secret settings with default behavior.
72 ///
73 /// When enabled, settings marked as `secret: true` in metadata
74 /// will be stored in the OS keychain instead of the settings file.
75 #[must_use]
76 pub fn with_credentials(mut self) -> Self {
77 self.config_builder = self.config_builder.with_credentials();
78 self
79 }
80
81 /// Extensively configure how credential secrets should be stored, enabling
82 /// advanced scenarios like custom proxy backends or keychain fallbacks.
83 ///
84 /// # Example
85 /// ```rust,ignore
86 /// use rcman::{SettingsManager, CredentialConfig};
87 ///
88 /// let manager = SettingsManager::builder("my-app", "1.0.0")
89 /// .with_credential_config(CredentialConfig::WithFallback {
90 /// fallback_path: "/tmp/secrets.enc.json".into(),
91 /// encryption_key: [0u8; 32], // Use a derived key
92 /// })
93 /// .build()
94 /// .unwrap();
95 /// ```
96 #[must_use]
97 pub fn with_credential_config(mut self, config: crate::config::CredentialConfig) -> Self {
98 self.config_builder = self.config_builder.with_credential_config(config);
99 self
100 }
101
102 /// Enable credentials with a default environment variable password source (Keychain + Encrypted File fallback).
103 ///
104 /// The environment variable name is automatically derived from the app name
105 /// (e.g., "my-app" -> "`MY_APP_SECRET`").
106 #[cfg(all(feature = "keychain", feature = "encrypted-file"))]
107 #[must_use]
108 pub fn with_env_credentials(mut self) -> Self {
109 self.config_builder = self.config_builder.with_env_credentials();
110 self
111 }
112
113 /// Enable credentials with a custom environment variable password source (Keychain + Encrypted File fallback).
114 #[cfg(all(feature = "keychain", feature = "encrypted-file"))]
115 #[must_use]
116 pub fn with_custom_env_credentials(mut self, var_name: impl Into<String>) -> Self {
117 self.config_builder = self.config_builder.with_custom_env_credentials(var_name);
118 self
119 }
120
121 /// Enable credentials with file password source (Keychain + Encrypted File fallback).
122 #[cfg(all(feature = "keychain", feature = "encrypted-file"))]
123 #[must_use]
124 pub fn with_file_credentials(mut self, path: impl Into<std::path::PathBuf>) -> Self {
125 self.config_builder = self.config_builder.with_file_credentials(path);
126 self
127 }
128
129 /// Enable credentials with provided password string (Keychain + Encrypted File fallback).
130 #[cfg(all(feature = "keychain", feature = "encrypted-file"))]
131 #[must_use]
132 pub fn with_password_credentials(mut self, password: impl Into<String>) -> Self {
133 self.config_builder = self.config_builder.with_password_credentials(password);
134 self
135 }
136
137 /// Enable environment variable overrides.
138 ///
139 /// When set, settings can be overridden by environment variables.
140 /// The format is: `{PREFIX}_{CATEGORY}_{KEY}` (all uppercase)
141 ///
142 /// # Example
143 ///
144 /// ```rust,no_run
145 /// use rcman::SettingsManager;
146 ///
147 /// let manager = SettingsManager::builder("my-app", "1.0.0")
148 /// .with_env_prefix("MYAPP")
149 /// .build()
150 /// .unwrap();
151 ///
152 /// // Now MYAPP_UI_THEME=dark will override the "ui.theme" setting
153 /// ```
154 #[must_use]
155 pub fn with_env_prefix(mut self, prefix: impl Into<String>) -> Self {
156 self.config_builder = self.config_builder.with_env_prefix(prefix);
157 self
158 }
159
160 /// Allow environment variables to override secret settings.
161 ///
162 /// By default, secrets stored in the OS keychain are NOT affected by env vars.
163 /// Enable this for Docker/CI environments where secrets are passed via env.
164 #[must_use]
165 pub fn env_overrides_secrets(mut self, allow: bool) -> Self {
166 self.config_builder = self.config_builder.env_overrides_secrets(allow);
167 self
168 }
169
170 /// Set a migration function for schema changes (lazy migration).
171 ///
172 /// The migrator function is called automatically when loading settings.
173 /// If the function modifies the value, the migrated version is saved back.
174 ///
175 /// Use this to upgrade old data formats to new ones transparently.
176 #[must_use]
177 pub fn with_migrator<F>(mut self, migrator: F) -> Self
178 where
179 F: Fn(serde_json::Value) -> serde_json::Value + Send + Sync + 'static,
180 {
181 self.config_builder = self.config_builder.with_migrator(migrator);
182 self
183 }
184
185 /// Enable hot-reload with default watcher configuration.
186 #[cfg(feature = "hot-reload")]
187 #[must_use]
188 pub fn with_hot_reload(mut self) -> Self {
189 self.config_builder = self.config_builder.with_hot_reload();
190 self
191 }
192
193 /// Enable hot-reload with a custom watcher configuration.
194 #[cfg(feature = "hot-reload")]
195 #[must_use]
196 pub fn with_hot_reload_config(mut self, config: crate::config::HotReloadConfig) -> Self {
197 self.config_builder = self.config_builder.with_hot_reload_config(config);
198 self
199 }
200
201 /// Register an external configuration file for backup.
202 ///
203 /// External configs are files managed outside of rcman (like rclone.conf)
204 /// that can be included in backups.
205 #[cfg(feature = "backup")]
206 #[must_use]
207 pub fn with_external_config(mut self, config: crate::backup::ExternalConfig) -> Self {
208 self.config_builder = self.config_builder.with_external_config(config);
209 self
210 }
211
212 /// Specify the schema type for the settings.
213 ///
214 /// This transforms the builder to use a typed schema instead of dynamic.
215 ///
216 /// # Example
217 ///
218 /// ```rust,no_run
219 /// use rcman::{SettingsManager, SettingsSchema, SettingMetadata, settings};
220 /// use serde::{Deserialize, Serialize};
221 /// use std::collections::HashMap;
222 ///
223 /// #[derive(Debug, Clone, Serialize, Deserialize, Default)]
224 /// struct AppSettings {
225 /// theme: String,
226 /// }
227 ///
228 /// impl SettingsSchema for AppSettings {
229 /// fn get_metadata() -> HashMap<String, SettingMetadata> {
230 /// settings! {
231 /// "ui.theme" => SettingMetadata::text("dark").meta_str("label", "Theme")
232 /// }
233 /// }
234 /// }
235 ///
236 /// let manager = SettingsManager::builder("my-app", "1.0.0")
237 /// .with_schema::<AppSettings>()
238 /// .build()
239 /// .unwrap();
240 /// ```
241 #[must_use]
242 pub fn with_schema<NewSchema: SettingsSchema>(self) -> SettingsManagerBuilder<S, NewSchema> {
243 SettingsManagerBuilder {
244 config_builder: self.config_builder.with_schema::<NewSchema>(),
245 sub_settings: self.sub_settings,
246 }
247 }
248
249 /// Specify the storage backend type.
250 ///
251 /// This transforms the builder to use the specified storage backend.
252 ///
253 /// # Example
254 /// ```rust,no_run
255 /// use rcman::{SettingsManager, JsonStorage};
256 ///
257 /// let manager = SettingsManager::builder("my-app", "1.0.0")
258 /// .with_storage::<JsonStorage>()
259 /// .build()
260 /// .unwrap();
261 /// ```
262 #[must_use]
263 pub fn with_storage<NewS: StorageBackend + Default>(
264 self,
265 ) -> SettingsManagerBuilder<NewS, Schema> {
266 SettingsManagerBuilder {
267 config_builder: self.config_builder.with_storage::<NewS>(),
268 sub_settings: self.sub_settings,
269 }
270 }
271
272 /// Enable profiles for main settings.
273 ///
274 /// When enabled, the main settings file is stored per-profile, allowing
275 /// completely different app configurations (e.g., "work" vs "personal").
276 ///
277 /// # Example
278 ///
279 /// ```rust,no_run
280 /// use rcman::SettingsManager;
281 ///
282 /// let manager = SettingsManager::builder("my-app", "1.0.0")
283 /// .with_profiles() // Enable profiles
284 /// .build()?;
285 ///
286 /// // Switch the entire app to work profile
287 /// manager.switch_profile("work")?;
288 /// # Ok::<(), rcman::Error>(())
289 /// ```
290 #[cfg(feature = "profiles")]
291 #[must_use]
292 pub fn with_profiles(mut self) -> Self {
293 self.config_builder = self.config_builder.with_profiles();
294 self
295 }
296
297 /// Register a sub-settings type for per-entity configuration.
298 ///
299 /// Sub-settings allow you to manage separate config files for each entity
300 /// (e.g., one file per remote, per profile, etc.).
301 ///
302 /// # Example
303 ///
304 /// ```rust,no_run
305 /// use rcman::{SettingsManager, SubSettingsConfig};
306 ///
307 /// let manager = SettingsManager::builder("my-app", "1.0.0")
308 /// .with_sub_settings(SubSettingsConfig::new("remotes"))
309 /// .with_sub_settings(SubSettingsConfig::singlefile("backends"))
310 /// .build()
311 /// .unwrap();
312 /// ```
313 #[must_use]
314 pub fn with_sub_settings(mut self, config: SubSettingsConfig) -> Self {
315 self.sub_settings.push(config);
316 self
317 }
318
319 /// Build the [`SettingsManager`].
320 ///
321 /// This creates the config directory if it doesn't exist, initializes
322 /// the manager, and registers all sub-settings.
323 ///
324 /// # Errors
325 ///
326 /// Returns an error if the config directory cannot be created.
327 pub fn build(self) -> Result<SettingsManager<S, Schema>>
328 where
329 S: Default + 'static,
330 {
331 let config = self.config_builder.build();
332 let manager = SettingsManager::new(config)?;
333
334 for sub_config in self.sub_settings {
335 manager.register_sub_settings(sub_config)?;
336 }
337
338 Ok(manager)
339 }
340}
341
342#[cfg(test)]
343mod tests {
344 use super::*;
345
346 #[test]
347 fn test_builder_with_credentials_config() {
348 let manager = SettingsManager::builder("my-app", "1.0.0")
349 .with_config_dir("/tmp/my-app")
350 .with_credential_config(crate::config::CredentialConfig::Default)
351 .build()
352 .unwrap();
353
354 #[cfg(any(feature = "keychain", feature = "encrypted-file"))]
355 {
356 assert!(manager.credentials.is_some());
357 }
358
359 #[cfg(not(any(feature = "keychain", feature = "encrypted-file")))]
360 {
361 let _ = manager;
362 }
363 }
364}