1use crate::{CodememError, GraphConfig, ScoringWeights, VectorConfig};
6use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11#[serde(default)]
12pub struct CodememConfig {
13 pub scoring: ScoringWeights,
14 pub vector: VectorConfig,
15 pub graph: GraphConfig,
16 pub embedding: EmbeddingConfig,
17 pub storage: StorageConfig,
18}
19
20impl CodememConfig {
21 pub fn load(path: &Path) -> Result<Self, CodememError> {
23 let content = std::fs::read_to_string(path)?;
24 toml::from_str(&content).map_err(|e| CodememError::Config(e.to_string()))
25 }
26
27 pub fn save(&self, path: &Path) -> Result<(), CodememError> {
29 let content =
30 toml::to_string_pretty(self).map_err(|e| CodememError::Config(e.to_string()))?;
31 if let Some(parent) = path.parent() {
32 std::fs::create_dir_all(parent)?;
33 }
34 std::fs::write(path, content)?;
35 Ok(())
36 }
37
38 pub fn load_or_default() -> Self {
40 let path = Self::default_path();
41 if path.exists() {
42 Self::load(&path).unwrap_or_default()
43 } else {
44 Self::default()
45 }
46 }
47
48 pub fn default_path() -> PathBuf {
50 dirs::home_dir()
51 .unwrap_or_else(|| PathBuf::from("."))
52 .join(".codemem")
53 .join("config.toml")
54 }
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59#[serde(default)]
60pub struct EmbeddingConfig {
61 pub provider: String,
63 pub model: String,
65 pub url: String,
67 pub dimensions: usize,
69 pub cache_capacity: usize,
71}
72
73impl Default for EmbeddingConfig {
74 fn default() -> Self {
75 Self {
76 provider: "candle".to_string(),
77 model: "BAAI/bge-base-en-v1.5".to_string(),
78 url: String::new(),
79 dimensions: 768,
80 cache_capacity: 10_000,
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(default)]
88pub struct StorageConfig {
89 pub db_path: String,
91 pub cache_size_mb: u32,
93 pub busy_timeout_secs: u64,
95}
96
97impl Default for StorageConfig {
98 fn default() -> Self {
99 Self {
100 db_path: dirs::home_dir()
101 .unwrap_or_else(|| PathBuf::from("."))
102 .join(".codemem")
103 .join("codemem.db")
104 .to_string_lossy()
105 .into_owned(),
106 cache_size_mb: 64,
107 busy_timeout_secs: 5,
108 }
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn default_config_roundtrips_through_toml() {
118 let config = CodememConfig::default();
119 let toml_str =
120 toml::to_string_pretty(&config).expect("default config should serialize to TOML");
121 let parsed: CodememConfig =
122 toml::from_str(&toml_str).expect("serialized TOML should parse back");
123 assert!((parsed.scoring.vector_similarity - 0.25).abs() < f64::EPSILON);
124 assert_eq!(parsed.vector.dimensions, 768);
125 assert_eq!(parsed.embedding.provider, "candle");
126 }
127
128 #[test]
129 fn load_nonexistent_returns_error() {
130 let result = CodememConfig::load(Path::new("/tmp/nonexistent_codemem_config.toml"));
131 assert!(result.is_err());
132 }
133
134 #[test]
135 fn save_and_load_roundtrip() {
136 let dir = std::env::temp_dir().join("codemem_config_test");
137 let _ = std::fs::remove_dir_all(&dir);
138 let path = dir.join("config.toml");
139
140 let mut config = CodememConfig::default();
141 config.scoring.vector_similarity = 0.5;
142 config.storage.cache_size_mb = 128;
143
144 config.save(&path).expect("save should succeed");
145 let loaded = CodememConfig::load(&path).expect("load should succeed");
146
147 assert!((loaded.scoring.vector_similarity - 0.5).abs() < f64::EPSILON);
148 assert_eq!(loaded.storage.cache_size_mb, 128);
149
150 let _ = std::fs::remove_dir_all(&dir);
151 }
152
153 #[test]
154 fn load_or_default_returns_default_when_no_file() {
155 let nonexistent = std::path::PathBuf::from("/tmp/codemem_test_no_exist/config.toml");
157 let _ = std::fs::remove_file(&nonexistent); let config = CodememConfig::load(&nonexistent).unwrap_or_default();
159 assert!((config.scoring.vector_similarity - 0.25).abs() < f64::EPSILON);
160 }
161
162 #[test]
163 fn default_path_ends_with_config_toml() {
164 let path = CodememConfig::default_path();
165 assert!(path.ends_with("config.toml"));
166 }
167
168 #[test]
169 fn partial_toml_uses_defaults_for_missing_fields() {
170 let partial = r#"
171[scoring]
172vector_similarity = 0.4
173"#;
174 let config: CodememConfig = toml::from_str(partial).expect("partial TOML should parse");
175 assert!((config.scoring.vector_similarity - 0.4).abs() < f64::EPSILON);
176 assert_eq!(config.vector.dimensions, 768);
178 assert_eq!(config.embedding.provider, "candle");
179 }
180}