use crate::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VaultConfig {
pub name: String,
pub path: PathBuf,
pub is_default: bool,
pub watch_for_changes: Option<bool>,
pub max_file_size: Option<u64>,
pub allowed_extensions: Option<HashSet<String>>,
pub excluded_paths: Option<HashSet<String>>,
pub enable_caching: Option<bool>,
pub cache_ttl: Option<u64>,
pub template_dirs: Option<Vec<PathBuf>>,
pub allowed_operations: Option<HashSet<String>>,
}
impl VaultConfig {
pub fn builder(name: impl Into<String>, path: impl Into<PathBuf>) -> VaultConfigBuilder {
VaultConfigBuilder::new(name, path)
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(Error::config_error("Vault name cannot be empty"));
}
if !self.path.exists() {
std::fs::create_dir_all(&self.path).map_err(|e| {
Error::config_error(format!(
"Vault path does not exist and could not be created: {} ({})",
self.path.display(),
e
))
})?;
}
if !self.path.is_dir() {
return Err(Error::config_error(format!(
"Vault path is not a directory: {}",
self.path.display()
)));
}
Ok(())
}
}
pub struct VaultConfigBuilder {
name: String,
path: PathBuf,
is_default: bool,
watch_for_changes: Option<bool>,
max_file_size: Option<u64>,
allowed_extensions: Option<HashSet<String>>,
excluded_paths: Option<HashSet<String>>,
enable_caching: Option<bool>,
cache_ttl: Option<u64>,
template_dirs: Option<Vec<PathBuf>>,
allowed_operations: Option<HashSet<String>>,
}
impl VaultConfigBuilder {
pub fn new(name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self {
name: name.into(),
path: path.into(),
is_default: false,
watch_for_changes: None,
max_file_size: None,
allowed_extensions: None,
excluded_paths: None,
enable_caching: None,
cache_ttl: None,
template_dirs: None,
allowed_operations: None,
}
}
pub fn as_default(mut self) -> Self {
self.is_default = true;
self
}
pub fn watch_for_changes(mut self, watch: bool) -> Self {
self.watch_for_changes = Some(watch);
self
}
pub fn build(self) -> Result<VaultConfig> {
let expanded_path = shellexpand::full(&self.path.to_string_lossy())
.map(|p| PathBuf::from(p.into_owned()))
.unwrap_or(self.path);
let config = VaultConfig {
name: self.name,
path: expanded_path,
is_default: self.is_default,
watch_for_changes: self.watch_for_changes,
max_file_size: self.max_file_size,
allowed_extensions: self.allowed_extensions,
excluded_paths: self.excluded_paths,
enable_caching: self.enable_caching,
cache_ttl: self.cache_ttl,
template_dirs: self.template_dirs,
allowed_operations: self.allowed_operations,
};
config.validate()?;
Ok(config)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
pub vaults: Vec<VaultConfig>,
pub profile: String,
pub watch_for_changes: bool,
pub max_file_size: u64,
pub allowed_extensions: HashSet<String>,
pub excluded_paths: HashSet<String>,
pub enable_caching: bool,
pub cache_ttl: u64,
pub log_level: String,
pub template_dirs: Vec<PathBuf>,
pub default_template_variables: serde_json::Value,
pub editor_backup_enabled: bool,
pub editor_atomic_writes: bool,
pub max_backup_files: usize,
pub max_edit_history: usize,
pub backup_retention_days: u32,
pub link_graph_enabled: bool,
pub link_suggestions_enabled: bool,
pub max_link_suggestions: usize,
pub link_similarity_threshold: f32,
pub full_text_search_enabled: bool,
pub index_rebuild_interval: u64,
pub multi_vault_enabled: bool,
pub metrics_enabled: bool,
pub debug_mode: bool,
}
impl Default for ServerConfig {
fn default() -> Self {
Self {
vaults: vec![],
profile: "default".to_string(),
watch_for_changes: true,
max_file_size: 10 * 1024 * 1024, allowed_extensions: [".md", ".txt", ".canvas"]
.iter()
.map(|s| s.to_string())
.collect(),
excluded_paths: [".obsidian", ".git", ".DS_Store", "node_modules"]
.iter()
.map(|s| s.to_string())
.collect(),
enable_caching: true,
cache_ttl: 3600,
log_level: "INFO".to_string(),
template_dirs: vec![],
default_template_variables: serde_json::json!({}),
editor_backup_enabled: true,
editor_atomic_writes: true,
max_backup_files: 100,
max_edit_history: 100,
backup_retention_days: 7,
link_graph_enabled: true,
link_suggestions_enabled: true,
max_link_suggestions: 10,
link_similarity_threshold: 0.3,
full_text_search_enabled: true,
index_rebuild_interval: 3600,
multi_vault_enabled: false,
metrics_enabled: false,
debug_mode: false,
}
}
}
impl ServerConfig {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> Result<()> {
if self.vaults.is_empty() {
return Err(Error::config_error("At least one vault must be configured"));
}
let names: HashSet<_> = self.vaults.iter().map(|v| &v.name).collect();
if names.len() != self.vaults.len() {
return Err(Error::config_error("Vault names must be unique"));
}
let defaults: Vec<_> = self.vaults.iter().filter(|v| v.is_default).collect();
if defaults.len() > 1 {
return Err(Error::config_error("Only one vault can be default"));
}
for vault in &self.vaults {
vault.validate()?;
}
Ok(())
}
pub fn default_vault(&self) -> Result<&VaultConfig> {
self.vaults
.iter()
.find(|v| v.is_default)
.or_else(|| self.vaults.first())
.ok_or_else(|| Error::config_error("No default vault configured"))
}
pub async fn save_vaults(&self, path: &Path) -> Result<()> {
let yaml = serde_yaml::to_string(&self.vaults)
.map_err(|e| Error::config_error(format!("Failed to serialize vaults: {}", e)))?;
tokio::fs::write(path, yaml).await.map_err(|e| {
Error::config_error(format!(
"Failed to save vaults to {}: {}",
path.display(),
e
))
})
}
pub async fn load_vaults(path: &Path) -> Result<Vec<VaultConfig>> {
if !path.exists() {
return Ok(Vec::new()); }
let content = tokio::fs::read_to_string(path).await.map_err(|e| {
Error::config_error(format!(
"Failed to load vaults from {}: {}",
path.display(),
e
))
})?;
let vaults = serde_yaml::from_str(&content)
.map_err(|e| Error::config_error(format!("Invalid vault configuration: {}", e)))?;
Ok(vaults)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_vault_config_builder() {
let temp = TempDir::new().unwrap();
let vault = VaultConfig::builder("main", temp.path())
.as_default()
.watch_for_changes(true)
.build();
assert!(vault.is_ok());
let v = vault.unwrap();
assert_eq!(v.name, "main");
assert!(v.is_default);
}
#[test]
fn test_server_config_validation() {
let mut config = ServerConfig::new();
config.vaults.clear();
assert!(config.validate().is_err());
}
}