turbovault_core/
config.rs

1//! Configuration types for the Obsidian server.
2//!
3//! Follows a builder pattern for complex configuration with validation.
4
5use crate::error::{Error, Result};
6use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8use std::path::Path;
9use std::path::PathBuf;
10
11/// Configuration for a single vault
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct VaultConfig {
14    /// Unique identifier for this vault
15    pub name: String,
16    /// Path to the vault directory
17    pub path: PathBuf,
18    /// Whether this is the default vault
19    pub is_default: bool,
20
21    // Optional overrides
22    pub watch_for_changes: Option<bool>,
23    pub max_file_size: Option<u64>,
24    pub allowed_extensions: Option<HashSet<String>>,
25    pub excluded_paths: Option<HashSet<String>>,
26    pub enable_caching: Option<bool>,
27    pub cache_ttl: Option<u64>,
28    pub template_dirs: Option<Vec<PathBuf>>,
29    pub allowed_operations: Option<HashSet<String>>,
30}
31
32impl VaultConfig {
33    /// Create a new vault config with builder
34    pub fn builder(name: impl Into<String>, path: impl Into<PathBuf>) -> VaultConfigBuilder {
35        VaultConfigBuilder::new(name, path)
36    }
37
38    /// Validate the vault configuration
39    pub fn validate(&self) -> Result<()> {
40        if self.name.is_empty() {
41            return Err(Error::config_error("Vault name cannot be empty"));
42        }
43
44        if !self.path.exists() {
45            return Err(Error::config_error(format!(
46                "Vault path does not exist: {}",
47                self.path.display()
48            )));
49        }
50
51        if !self.path.is_dir() {
52            return Err(Error::config_error(format!(
53                "Vault path is not a directory: {}",
54                self.path.display()
55            )));
56        }
57
58        Ok(())
59    }
60}
61
62/// Builder for VaultConfig
63pub struct VaultConfigBuilder {
64    name: String,
65    path: PathBuf,
66    is_default: bool,
67    watch_for_changes: Option<bool>,
68    max_file_size: Option<u64>,
69    allowed_extensions: Option<HashSet<String>>,
70    excluded_paths: Option<HashSet<String>>,
71    enable_caching: Option<bool>,
72    cache_ttl: Option<u64>,
73    template_dirs: Option<Vec<PathBuf>>,
74    allowed_operations: Option<HashSet<String>>,
75}
76
77impl VaultConfigBuilder {
78    /// Create a new builder
79    pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
80        Self {
81            name: name.into(),
82            path: path.into(),
83            is_default: false,
84            watch_for_changes: None,
85            max_file_size: None,
86            allowed_extensions: None,
87            excluded_paths: None,
88            enable_caching: None,
89            cache_ttl: None,
90            template_dirs: None,
91            allowed_operations: None,
92        }
93    }
94
95    /// Mark as default vault
96    pub fn as_default(mut self) -> Self {
97        self.is_default = true;
98        self
99    }
100
101    /// Set watch_for_changes
102    pub fn watch_for_changes(mut self, watch: bool) -> Self {
103        self.watch_for_changes = Some(watch);
104        self
105    }
106
107    /// Build and validate
108    pub fn build(self) -> Result<VaultConfig> {
109        let config = VaultConfig {
110            name: self.name,
111            path: self.path,
112            is_default: self.is_default,
113            watch_for_changes: self.watch_for_changes,
114            max_file_size: self.max_file_size,
115            allowed_extensions: self.allowed_extensions,
116            excluded_paths: self.excluded_paths,
117            enable_caching: self.enable_caching,
118            cache_ttl: self.cache_ttl,
119            template_dirs: self.template_dirs,
120            allowed_operations: self.allowed_operations,
121        };
122        config.validate()?;
123        Ok(config)
124    }
125}
126
127/// Global server configuration
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ServerConfig {
130    /// List of configured vaults
131    pub vaults: Vec<VaultConfig>,
132    /// Configuration profile name
133    pub profile: String,
134
135    // Core settings
136    pub watch_for_changes: bool,
137    pub max_file_size: u64,
138    pub allowed_extensions: HashSet<String>,
139    pub excluded_paths: HashSet<String>,
140    pub enable_caching: bool,
141    pub cache_ttl: u64,
142    pub log_level: String,
143
144    // Advanced settings
145    pub template_dirs: Vec<PathBuf>,
146    pub default_template_variables: serde_json::Value,
147    pub editor_backup_enabled: bool,
148    pub editor_atomic_writes: bool,
149    pub max_backup_files: usize,
150    pub max_edit_history: usize,
151    pub backup_retention_days: u32,
152
153    // Link graph settings
154    pub link_graph_enabled: bool,
155    pub link_suggestions_enabled: bool,
156    pub max_link_suggestions: usize,
157    pub link_similarity_threshold: f32,
158
159    // Search settings
160    pub full_text_search_enabled: bool,
161    pub index_rebuild_interval: u64,
162
163    // Multi-vault
164    pub multi_vault_enabled: bool,
165
166    // Admin
167    pub metrics_enabled: bool,
168    pub debug_mode: bool,
169}
170
171impl Default for ServerConfig {
172    fn default() -> Self {
173        Self {
174            vaults: vec![],
175            profile: "default".to_string(),
176            watch_for_changes: true,
177            max_file_size: 10 * 1024 * 1024, // 10MB
178            allowed_extensions: [".md", ".txt", ".canvas"]
179                .iter()
180                .map(|s| s.to_string())
181                .collect(),
182            excluded_paths: [".obsidian", ".git", ".DS_Store", "node_modules"]
183                .iter()
184                .map(|s| s.to_string())
185                .collect(),
186            enable_caching: true,
187            cache_ttl: 3600,
188            log_level: "INFO".to_string(),
189            template_dirs: vec![],
190            default_template_variables: serde_json::json!({}),
191            editor_backup_enabled: true,
192            editor_atomic_writes: true,
193            max_backup_files: 100,
194            max_edit_history: 100,
195            backup_retention_days: 7,
196            link_graph_enabled: true,
197            link_suggestions_enabled: true,
198            max_link_suggestions: 10,
199            link_similarity_threshold: 0.3,
200            full_text_search_enabled: true,
201            index_rebuild_interval: 3600,
202            multi_vault_enabled: false,
203            metrics_enabled: false,
204            debug_mode: false,
205        }
206    }
207}
208
209impl ServerConfig {
210    /// Create new configuration
211    pub fn new() -> Self {
212        Self::default()
213    }
214
215    /// Validate configuration
216    pub fn validate(&self) -> Result<()> {
217        if self.vaults.is_empty() {
218            return Err(Error::config_error("At least one vault must be configured"));
219        }
220
221        // Check unique vault names
222        let names: HashSet<_> = self.vaults.iter().map(|v| &v.name).collect();
223        if names.len() != self.vaults.len() {
224            return Err(Error::config_error("Vault names must be unique"));
225        }
226
227        // Check unique default vaults
228        let defaults: Vec<_> = self.vaults.iter().filter(|v| v.is_default).collect();
229        if defaults.len() > 1 {
230            return Err(Error::config_error("Only one vault can be default"));
231        }
232
233        // Validate each vault
234        for vault in &self.vaults {
235            vault.validate()?;
236        }
237
238        Ok(())
239    }
240
241    /// Get default vault config
242    pub fn default_vault(&self) -> Result<&VaultConfig> {
243        self.vaults
244            .iter()
245            .find(|v| v.is_default)
246            .or_else(|| self.vaults.first())
247            .ok_or_else(|| Error::config_error("No default vault configured"))
248    }
249
250    /// Save vault configuration to file (for persistence)
251    pub async fn save_vaults(&self, path: &Path) -> Result<()> {
252        let yaml = serde_yaml::to_string(&self.vaults)
253            .map_err(|e| Error::config_error(format!("Failed to serialize vaults: {}", e)))?;
254
255        tokio::fs::write(path, yaml).await.map_err(|e| {
256            Error::config_error(format!(
257                "Failed to save vaults to {}: {}",
258                path.display(),
259                e
260            ))
261        })
262    }
263
264    /// Load vault configuration from file
265    pub async fn load_vaults(path: &Path) -> Result<Vec<VaultConfig>> {
266        if !path.exists() {
267            return Ok(Vec::new()); // Return empty if file doesn't exist
268        }
269
270        let content = tokio::fs::read_to_string(path).await.map_err(|e| {
271            Error::config_error(format!(
272                "Failed to load vaults from {}: {}",
273                path.display(),
274                e
275            ))
276        })?;
277
278        let vaults = serde_yaml::from_str(&content)
279            .map_err(|e| Error::config_error(format!("Invalid vault configuration: {}", e)))?;
280
281        Ok(vaults)
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use tempfile::TempDir;
289
290    #[test]
291    fn test_vault_config_builder() {
292        let temp = TempDir::new().unwrap();
293        let vault = VaultConfig::builder("main", temp.path())
294            .as_default()
295            .watch_for_changes(true)
296            .build();
297
298        assert!(vault.is_ok());
299        let v = vault.unwrap();
300        assert_eq!(v.name, "main");
301        assert!(v.is_default);
302    }
303
304    #[test]
305    fn test_server_config_validation() {
306        let mut config = ServerConfig::new();
307        config.vaults.clear();
308        assert!(config.validate().is_err());
309    }
310}