1use std::path::PathBuf;
22
23use serde::{Deserialize, Serialize};
24
25use crate::error::{ClawError, ClawResult};
26
27#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
29pub enum JournalMode {
30 #[default]
32 WAL,
33 Delete,
35 Truncate,
37}
38
39impl std::fmt::Display for JournalMode {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 JournalMode::WAL => write!(f, "WAL"),
43 JournalMode::Delete => write!(f, "DELETE"),
44 JournalMode::Truncate => write!(f, "TRUNCATE"),
45 }
46 }
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ClawConfig {
55 pub db_path: PathBuf,
57
58 pub max_connections: u32,
60
61 pub wal_enabled: bool,
63
64 pub cache_size_mb: usize,
66
67 pub auto_migrate: bool,
69
70 pub snapshot_dir: Option<PathBuf>,
72
73 pub journal_mode: JournalMode,
75
76 #[cfg(feature = "encryption")]
82 pub encryption_key: Option<[u8; 32]>,
83}
84
85impl Default for ClawConfig {
86 fn default() -> Self {
87 let db_path = dirs::data_local_dir()
88 .unwrap_or_else(|| PathBuf::from("."))
89 .join("clawdb")
90 .join("claw.db");
91
92 ClawConfig {
93 db_path,
94 max_connections: 10,
95 wal_enabled: true,
96 cache_size_mb: 64,
97 auto_migrate: true,
98 snapshot_dir: None,
99 journal_mode: JournalMode::WAL,
100 #[cfg(feature = "encryption")]
101 encryption_key: None,
102 }
103 }
104}
105
106impl ClawConfig {
107 pub fn builder() -> ClawConfigBuilder {
109 ClawConfigBuilder::default()
110 }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ClawConfigBuilder {
120 db_path: Option<PathBuf>,
121 max_connections: u32,
122 wal_enabled: bool,
123 cache_size_mb: usize,
124 auto_migrate: bool,
125 snapshot_dir: Option<PathBuf>,
126 journal_mode: JournalMode,
127 #[cfg(feature = "encryption")]
128 encryption_key: Option<[u8; 32]>,
129}
130
131impl Default for ClawConfigBuilder {
132 fn default() -> Self {
133 let defaults = ClawConfig::default();
134 ClawConfigBuilder {
135 db_path: Some(defaults.db_path),
136 max_connections: defaults.max_connections,
137 wal_enabled: defaults.wal_enabled,
138 cache_size_mb: defaults.cache_size_mb,
139 auto_migrate: defaults.auto_migrate,
140 snapshot_dir: defaults.snapshot_dir,
141 journal_mode: defaults.journal_mode,
142 #[cfg(feature = "encryption")]
143 encryption_key: None,
144 }
145 }
146}
147
148impl ClawConfigBuilder {
149 pub fn db_path(mut self, path: impl Into<PathBuf>) -> Self {
151 self.db_path = Some(path.into());
152 self
153 }
154
155 pub fn max_connections(mut self, n: u32) -> Self {
157 self.max_connections = n;
158 self
159 }
160
161 pub fn wal_enabled(mut self, enabled: bool) -> Self {
163 self.wal_enabled = enabled;
164 self
165 }
166
167 pub fn cache_size_mb(mut self, mb: usize) -> Self {
169 self.cache_size_mb = mb;
170 self
171 }
172
173 pub fn auto_migrate(mut self, enabled: bool) -> Self {
175 self.auto_migrate = enabled;
176 self
177 }
178
179 pub fn snapshot_dir(mut self, dir: impl Into<PathBuf>) -> Self {
181 self.snapshot_dir = Some(dir.into());
182 self
183 }
184
185 pub fn journal_mode(mut self, mode: JournalMode) -> Self {
187 self.journal_mode = mode;
188 self
189 }
190
191 #[cfg(feature = "encryption")]
196 pub fn encryption_key(mut self, key: [u8; 32]) -> Self {
197 self.encryption_key = Some(key);
198 self
199 }
200
201 pub fn build(self) -> ClawResult<ClawConfig> {
211 let db_path = self
213 .db_path
214 .ok_or_else(|| ClawError::Config("db_path must be set".to_string()))?;
215
216 if let Some(parent) = db_path.parent() {
217 if !parent.as_os_str().is_empty() && !parent.exists() {
218 std::fs::create_dir_all(parent).map_err(|e| {
219 ClawError::Config(format!(
220 "cannot create db_path parent directory '{}': {e}",
221 parent.display()
222 ))
223 })?;
224 }
225 }
226
227 if self.max_connections < 1 {
229 return Err(ClawError::Config(
230 "max_connections must be >= 1".to_string(),
231 ));
232 }
233
234 if self.cache_size_mb < 1 {
236 return Err(ClawError::Config("cache_size_mb must be >= 1".to_string()));
237 }
238
239 Ok(ClawConfig {
240 db_path,
241 max_connections: self.max_connections,
242 wal_enabled: self.wal_enabled,
243 cache_size_mb: self.cache_size_mb,
244 auto_migrate: self.auto_migrate,
245 snapshot_dir: self.snapshot_dir,
246 journal_mode: self.journal_mode,
247 #[cfg(feature = "encryption")]
248 encryption_key: self.encryption_key,
249 })
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn default_config_is_valid() {
259 let config = ClawConfig::default();
260 assert_eq!(config.max_connections, 10);
261 assert!(config.wal_enabled);
262 assert_eq!(config.cache_size_mb, 64);
263 assert!(config.auto_migrate);
264 assert_eq!(config.journal_mode, JournalMode::WAL);
265 assert!(config.snapshot_dir.is_none());
266 }
267
268 #[test]
269 fn builder_with_temp_path_succeeds() {
270 let dir = tempfile::tempdir().unwrap();
271 let db_path = dir.path().join("test.db");
272
273 let config = ClawConfig::builder()
274 .db_path(db_path.clone())
275 .max_connections(5)
276 .cache_size_mb(32)
277 .build()
278 .expect("should succeed");
279
280 assert_eq!(config.db_path, db_path);
281 assert_eq!(config.max_connections, 5);
282 assert_eq!(config.cache_size_mb, 32);
283 }
284
285 #[test]
286 fn builder_creates_missing_parent_dir() {
287 let dir = tempfile::tempdir().unwrap();
288 let db_path = dir.path().join("nested").join("deep").join("claw.db");
289
290 let config = ClawConfig::builder()
291 .db_path(db_path.clone())
292 .build()
293 .expect("should create parent dirs");
294
295 assert!(db_path.parent().unwrap().exists());
296 assert_eq!(config.db_path, db_path);
297 }
298
299 #[test]
300 fn builder_rejects_zero_max_connections() {
301 let dir = tempfile::tempdir().unwrap();
302 let db_path = dir.path().join("claw.db");
303
304 let err = ClawConfig::builder()
305 .db_path(db_path)
306 .max_connections(0)
307 .build()
308 .unwrap_err();
309
310 assert!(matches!(err, ClawError::Config(_)));
311 assert!(err.to_string().contains("max_connections"));
312 }
313
314 #[test]
315 fn builder_rejects_zero_cache_size() {
316 let dir = tempfile::tempdir().unwrap();
317 let db_path = dir.path().join("claw.db");
318
319 let err = ClawConfig::builder()
320 .db_path(db_path)
321 .cache_size_mb(0)
322 .build()
323 .unwrap_err();
324
325 assert!(matches!(err, ClawError::Config(_)));
326 assert!(err.to_string().contains("cache_size_mb"));
327 }
328
329 #[test]
330 fn journal_mode_display() {
331 assert_eq!(JournalMode::WAL.to_string(), "WAL");
332 assert_eq!(JournalMode::Delete.to_string(), "DELETE");
333 assert_eq!(JournalMode::Truncate.to_string(), "TRUNCATE");
334 }
335
336 #[test]
337 fn builder_chainable_setters() {
338 let dir = tempfile::tempdir().unwrap();
339 let snap_dir = dir.path().join("snapshots");
340 let db_path = dir.path().join("claw.db");
341
342 let config = ClawConfig::builder()
343 .db_path(db_path)
344 .max_connections(20)
345 .wal_enabled(false)
346 .cache_size_mb(256)
347 .auto_migrate(false)
348 .snapshot_dir(snap_dir.clone())
349 .journal_mode(JournalMode::Truncate)
350 .build()
351 .unwrap();
352
353 assert_eq!(config.max_connections, 20);
354 assert!(!config.wal_enabled);
355 assert_eq!(config.cache_size_mb, 256);
356 assert!(!config.auto_migrate);
357 assert_eq!(config.snapshot_dir, Some(snap_dir));
358 assert_eq!(config.journal_mode, JournalMode::Truncate);
359 }
360
361 #[test]
362 fn config_serde_roundtrip() {
363 let dir = tempfile::tempdir().unwrap();
364 let db_path = dir.path().join("claw.db");
365 let config = ClawConfig::builder().db_path(db_path).build().unwrap();
366
367 let json = serde_json::to_string(&config).unwrap();
368 let restored: ClawConfig = serde_json::from_str(&json).unwrap();
369 assert_eq!(config.db_path, restored.db_path);
370 assert_eq!(config.max_connections, restored.max_connections);
371 assert_eq!(config.journal_mode, restored.journal_mode);
372 }
373}