1use crate::error::{Error, Result};
6use serde::{Deserialize, Serialize};
7use std::collections::HashSet;
8use std::path::Path;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct VaultConfig {
14 pub name: String,
16 pub path: PathBuf,
18 pub is_default: bool,
20
21 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 pub fn builder(name: impl Into<String>, path: impl Into<PathBuf>) -> VaultConfigBuilder {
35 VaultConfigBuilder::new(name, path)
36 }
37
38 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
62pub 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 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 pub fn as_default(mut self) -> Self {
97 self.is_default = true;
98 self
99 }
100
101 pub fn watch_for_changes(mut self, watch: bool) -> Self {
103 self.watch_for_changes = Some(watch);
104 self
105 }
106
107 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#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct ServerConfig {
130 pub vaults: Vec<VaultConfig>,
132 pub profile: String,
134
135 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 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 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 pub full_text_search_enabled: bool,
161 pub index_rebuild_interval: u64,
162
163 pub multi_vault_enabled: bool,
165
166 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, 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 pub fn new() -> Self {
212 Self::default()
213 }
214
215 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 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 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 for vault in &self.vaults {
235 vault.validate()?;
236 }
237
238 Ok(())
239 }
240
241 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 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 pub async fn load_vaults(path: &Path) -> Result<Vec<VaultConfig>> {
266 if !path.exists() {
267 return Ok(Vec::new()); }
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}