Skip to main content

codesynapse_core/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::Path;
3
4#[derive(Debug, Clone, Serialize, Deserialize, Default)]
5pub struct CodeSynapseConfig {
6    pub output: Option<String>,
7    #[serde(default)]
8    pub no_llm: bool,
9    #[serde(default)]
10    pub code_only: bool,
11    pub formats: Option<Vec<String>>,
12    pub llm: Option<LlmConfig>,
13    pub embeddings: Option<EmbeddingsConfig>,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, Default)]
17pub struct EmbeddingsConfig {
18    /// Path to a directory containing `tokenizer.json` + `model.safetensors`.
19    /// Example: `./potion-code-16M`
20    pub model_path: Option<String>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct LlmConfig {
25    pub provider: Option<String>,
26    pub model: Option<String>,
27    pub api_key: Option<String>,
28    pub base_url: Option<String>,
29}
30
31impl CodeSynapseConfig {
32    pub fn from_file(path: &Path) -> Result<Self, String> {
33        let content =
34            std::fs::read_to_string(path).map_err(|e| format!("Failed to read config: {}", e))?;
35        toml::from_str(&content).map_err(|e| format!("Failed to parse config: {}", e))
36    }
37
38    pub fn discover(start: &Path) -> Result<Self, String> {
39        let candidates = [
40            start.join("codesynapse.toml"),
41            start.join("codesynapse.toml"),
42            start.join(".codesynapse.toml"),
43            start.join(".codesynapse.toml"),
44        ];
45        for candidate in &candidates {
46            if candidate.exists() {
47                return Self::from_file(candidate);
48            }
49        }
50        Err("No codesynapse.toml found".to_string())
51    }
52
53    pub fn write_default(path: &Path) -> Result<(), String> {
54        let config = CodeSynapseConfig::default();
55        let content = toml::to_string_pretty(&config)
56            .map_err(|e| format!("Failed to serialize config: {}", e))?;
57        std::fs::write(path, content).map_err(|e| format!("Failed to write config: {}", e))?;
58        Ok(())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_config_default() {
68        let config = CodeSynapseConfig::default();
69        assert!(!config.no_llm);
70        assert!(!config.code_only);
71        assert!(config.output.is_none());
72        assert!(config.formats.is_none());
73        assert!(config.llm.is_none());
74    }
75
76    #[test]
77    fn test_config_parse_full() {
78        let toml_str = r#"
79output = "codesynapse-out"
80no_llm = true
81code_only = true
82formats = ["json", "html"]
83
84[llm]
85provider = "anthropic"
86model = "claude-sonnet-4-20250514"
87"#;
88        let config: CodeSynapseConfig = toml::from_str(toml_str).unwrap();
89        assert_eq!(config.output, Some("codesynapse-out".to_string()));
90        assert!(config.no_llm);
91        assert!(config.code_only);
92        assert_eq!(
93            config.formats,
94            Some(vec!["json".to_string(), "html".to_string()])
95        );
96        let llm = config.llm.unwrap();
97        assert_eq!(llm.provider, Some("anthropic".to_string()));
98        assert_eq!(llm.model, Some("claude-sonnet-4-20250514".to_string()));
99    }
100
101    #[test]
102    fn test_config_parse_minimal() {
103        let toml_str = r#"no_llm = true"#;
104        let config: CodeSynapseConfig = toml::from_str(toml_str).unwrap();
105        assert!(!config.code_only);
106        assert!(config.no_llm);
107        assert!(config.output.is_none());
108        assert!(config.formats.is_none());
109    }
110
111    #[test]
112    fn test_config_write_default() {
113        let dir = std::env::temp_dir().join("codesynapse_test_config");
114        let _ = std::fs::remove_dir_all(&dir);
115        std::fs::create_dir_all(&dir).unwrap();
116
117        let path = dir.join("codesynapse.toml");
118        CodeSynapseConfig::write_default(&path).unwrap();
119        assert!(path.exists());
120
121        let loaded = CodeSynapseConfig::from_file(&path).unwrap();
122        assert!(!loaded.no_llm);
123        assert!(!loaded.code_only);
124
125        let _ = std::fs::remove_dir_all(&dir);
126    }
127
128    #[test]
129    fn test_config_discover() {
130        let dir = std::env::temp_dir().join("codesynapse_test_discover");
131        let _ = std::fs::remove_dir_all(&dir);
132        std::fs::create_dir_all(&dir).unwrap();
133
134        let path = dir.join("codesynapse.toml");
135        std::fs::write(
136            &path,
137            r#"no_llm = true
138code_only = true
139"#,
140        )
141        .unwrap();
142
143        let config = CodeSynapseConfig::discover(&dir).unwrap();
144        assert!(config.no_llm);
145        assert!(config.code_only);
146
147        let _ = std::fs::remove_dir_all(&dir);
148    }
149
150    #[test]
151    fn test_config_discover_not_found() {
152        let dir = std::env::temp_dir().join("codesynapse_test_no_config");
153        let _ = std::fs::remove_dir_all(&dir);
154        std::fs::create_dir_all(&dir).unwrap();
155
156        let result = CodeSynapseConfig::discover(&dir);
157        assert!(result.is_err());
158
159        let _ = std::fs::remove_dir_all(&dir);
160    }
161
162    #[test]
163    fn test_config_parse_empty() {
164        let config: CodeSynapseConfig = toml::from_str("").unwrap();
165        assert!(!config.no_llm);
166        assert!(!config.code_only);
167    }
168}