Skip to main content

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}