Skip to main content

obsidian_cli_inspector/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct Config {
6    pub vault_path: PathBuf,
7    #[serde(default)]
8    pub database_path: Option<PathBuf>,
9    #[serde(default)]
10    pub log_path: Option<PathBuf>,
11    #[serde(default)]
12    pub exclude: ExcludeConfig,
13    #[serde(default)]
14    pub search: SearchConfig,
15    #[serde(default)]
16    pub graph: GraphConfig,
17    #[serde(default)]
18    pub llm: Option<LlmConfig>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize, Default)]
22pub struct ExcludeConfig {
23    #[serde(default = "default_exclude_patterns")]
24    pub patterns: Vec<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct SearchConfig {
29    #[serde(default = "default_search_limit")]
30    pub default_limit: usize,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct GraphConfig {
35    #[serde(default = "default_max_depth")]
36    pub max_depth: usize,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct LlmConfig {
41    pub api_url: String,
42    pub model: String,
43    #[serde(default = "default_timeout")]
44    pub timeout_seconds: u64,
45}
46
47fn default_exclude_patterns() -> Vec<String> {
48    vec![
49        ".obsidian/".to_string(),
50        ".git/".to_string(),
51        ".trash/".to_string(),
52    ]
53}
54
55fn default_search_limit() -> usize {
56    20
57}
58
59fn default_max_depth() -> usize {
60    3
61}
62
63fn default_timeout() -> u64 {
64    30
65}
66
67impl Default for SearchConfig {
68    fn default() -> Self {
69        Self {
70            default_limit: default_search_limit(),
71        }
72    }
73}
74
75impl Default for GraphConfig {
76    fn default() -> Self {
77        Self {
78            max_depth: default_max_depth(),
79        }
80    }
81}
82
83impl Config {
84    pub fn from_file(path: &PathBuf) -> anyhow::Result<Self> {
85        let content = std::fs::read_to_string(path)?;
86        let config: Config = toml::from_str(&content)?;
87        Ok(config)
88    }
89
90    pub fn database_path(&self) -> PathBuf {
91        self.database_path.clone().unwrap_or_else(|| {
92            // default to the XDG config directory so the DB lives next to config/logs
93            let mut path = self.config_dir();
94            path.push("vault.db");
95            path
96        })
97    }
98
99    pub fn config_dir(&self) -> PathBuf {
100        dirs::config_dir()
101            .unwrap_or_else(|| PathBuf::from("."))
102            .join("obsidian-cli-inspector")
103    }
104
105    pub fn log_dir(&self) -> PathBuf {
106        self.log_path
107            .clone()
108            .unwrap_or_else(|| self.config_dir().join("logs"))
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_config_database_path_with_custom_path() {
118        let config = Config {
119            vault_path: PathBuf::from("/test/vault"),
120            database_path: Some(PathBuf::from("/custom/path/db.db")),
121            log_path: None,
122            exclude: Default::default(),
123            search: Default::default(),
124            graph: Default::default(),
125            llm: None,
126        };
127
128        assert_eq!(config.database_path(), PathBuf::from("/custom/path/db.db"));
129    }
130
131    #[test]
132    fn test_config_database_path_default() {
133        let config = Config {
134            vault_path: PathBuf::from("/test/vault"),
135            database_path: None,
136            log_path: None,
137            exclude: Default::default(),
138            search: Default::default(),
139            graph: Default::default(),
140            llm: None,
141        };
142
143        let db_path = config.database_path();
144        assert!(db_path.to_string_lossy().ends_with("vault.db"));
145    }
146
147    #[test]
148    fn test_config_log_dir_with_custom_path() {
149        let config = Config {
150            vault_path: PathBuf::from("/test/vault"),
151            database_path: None,
152            log_path: Some(PathBuf::from("/custom/logs")),
153            exclude: Default::default(),
154            search: Default::default(),
155            graph: Default::default(),
156            llm: None,
157        };
158
159        assert_eq!(config.log_dir(), PathBuf::from("/custom/logs"));
160    }
161
162    #[test]
163    fn test_config_log_dir_default() {
164        let config = Config {
165            vault_path: PathBuf::from("/test/vault"),
166            database_path: None,
167            log_path: None,
168            exclude: Default::default(),
169            search: Default::default(),
170            graph: Default::default(),
171            llm: None,
172        };
173
174        let log_path = config.log_dir();
175        assert!(log_path.to_string_lossy().contains("logs"));
176    }
177
178    #[test]
179    fn test_llm_config_creation() {
180        let llm = LlmConfig {
181            api_url: "http://api.example.com".to_string(),
182            model: "gpt-4".to_string(),
183            timeout_seconds: 60,
184        };
185
186        assert_eq!(llm.api_url, "http://api.example.com");
187        assert_eq!(llm.timeout_seconds, 60);
188    }
189
190    #[test]
191    fn test_config_default_search_limit() {
192        assert_eq!(super::default_search_limit(), 20);
193    }
194
195    #[test]
196    fn test_config_default_max_depth() {
197        assert_eq!(super::default_max_depth(), 3);
198    }
199
200    #[test]
201    fn test_config_default_timeout() {
202        assert_eq!(super::default_timeout(), 30);
203    }
204
205    #[test]
206    fn test_config_default_exclude_patterns() {
207        let patterns = super::default_exclude_patterns();
208        assert!(patterns.contains(&".obsidian/".to_string()));
209        assert!(patterns.contains(&".git/".to_string()));
210        assert!(patterns.contains(&".trash/".to_string()));
211    }
212
213    #[test]
214    fn test_config_struct_creation() {
215        let config = Config {
216            vault_path: PathBuf::from("/test/vault"),
217            database_path: Some(PathBuf::from("/test/db.db")),
218            log_path: Some(PathBuf::from("/test/logs")),
219            exclude: ExcludeConfig::default(),
220            search: SearchConfig::default(),
221            graph: GraphConfig::default(),
222            llm: None,
223        };
224
225        assert_eq!(config.vault_path, PathBuf::from("/test/vault"));
226        assert!(config.database_path.is_some());
227        assert!(config.log_path.is_some());
228    }
229
230    #[test]
231    fn test_search_config_default_implementation() {
232        let search = SearchConfig::default();
233        assert_eq!(search.default_limit, 20);
234    }
235
236    #[test]
237    fn test_graph_config_default_implementation() {
238        let graph = GraphConfig::default();
239        assert_eq!(graph.max_depth, 3);
240    }
241}