ricecoder_external_lsp/registry/
config.rs1use crate::error::{ExternalLspError, Result};
4use crate::types::LspServerRegistry;
5use std::path::Path;
6use tracing::{debug, info};
7
8pub struct ConfigLoader;
10
11impl ConfigLoader {
12 pub fn load_from_file(path: &Path) -> Result<LspServerRegistry> {
14 debug!("Loading LSP configuration from: {:?}", path);
15
16 let content = std::fs::read_to_string(path).map_err(|e| {
17 ExternalLspError::ConfigError(format!("Failed to read config file: {}", e))
18 })?;
19
20 Self::load_from_string(&content)
21 }
22
23 pub fn load_from_string(content: &str) -> Result<LspServerRegistry> {
25 let registry: LspServerRegistry = serde_yaml::from_str(content).map_err(|e| {
26 ExternalLspError::ConfigError(format!("Failed to parse YAML: {}", e))
27 })?;
28
29 Self::validate(®istry)?;
31
32 info!("Successfully loaded LSP configuration with {} languages", registry.servers.len());
33
34 Ok(registry)
35 }
36
37 fn validate(registry: &LspServerRegistry) -> Result<()> {
39 for (language, configs) in ®istry.servers {
40 if configs.is_empty() {
41 return Err(ExternalLspError::InvalidConfiguration(format!(
42 "Language '{}' has no server configurations",
43 language
44 )));
45 }
46
47 for (idx, config) in configs.iter().enumerate() {
48 if config.language != *language {
49 return Err(ExternalLspError::InvalidConfiguration(format!(
50 "Server {} for language '{}' has mismatched language field: '{}'",
51 idx, language, config.language
52 )));
53 }
54
55 if config.executable.is_empty() {
56 return Err(ExternalLspError::InvalidConfiguration(format!(
57 "Server {} for language '{}' has empty executable",
58 idx, language
59 )));
60 }
61
62 if config.extensions.is_empty() {
63 return Err(ExternalLspError::InvalidConfiguration(format!(
64 "Server {} for language '{}' has no file extensions",
65 idx, language
66 )));
67 }
68
69 if config.timeout_ms == 0 {
70 return Err(ExternalLspError::InvalidConfiguration(format!(
71 "Server {} for language '{}' has invalid timeout_ms: 0",
72 idx, language
73 )));
74 }
75 }
76 }
77
78 Ok(())
79 }
80
81 pub fn merge_configs(
83 runtime: Option<LspServerRegistry>,
84 project: Option<LspServerRegistry>,
85 user: Option<LspServerRegistry>,
86 builtin: LspServerRegistry,
87 ) -> Result<LspServerRegistry> {
88 let mut result = builtin;
89
90 if let Some(user_config) = user {
92 Self::merge_into(&mut result, user_config)?;
93 }
94
95 if let Some(project_config) = project {
97 Self::merge_into(&mut result, project_config)?;
98 }
99
100 if let Some(runtime_config) = runtime {
102 Self::merge_into(&mut result, runtime_config)?;
103 }
104
105 Ok(result)
106 }
107
108 fn merge_into(target: &mut LspServerRegistry, source: LspServerRegistry) -> Result<()> {
110 for (language, configs) in source.servers {
112 target.servers.insert(language, configs);
113 }
114
115 target.global = source.global;
117
118 Ok(())
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_load_valid_config() {
128 let yaml = r#"
129global:
130 max_processes: 5
131 default_timeout_ms: 5000
132 enable_fallback: true
133 health_check_interval_ms: 30000
134
135servers:
136 rust:
137 - language: rust
138 extensions: [".rs"]
139 executable: rust-analyzer
140 args: []
141 env: {}
142 enabled: true
143 timeout_ms: 10000
144 max_restarts: 3
145 idle_timeout_ms: 300000
146"#;
147
148 let result = ConfigLoader::load_from_string(yaml);
149 assert!(result.is_ok());
150
151 let registry = result.unwrap();
152 assert_eq!(registry.servers.len(), 1);
153 assert!(registry.servers.contains_key("rust"));
154 }
155
156 #[test]
157 fn test_load_invalid_yaml() {
158 let yaml = "invalid: [yaml";
159 let result = ConfigLoader::load_from_string(yaml);
160 assert!(result.is_err());
161 }
162
163 #[test]
164 fn test_validate_empty_executable() {
165 let yaml = r#"
166global:
167 max_processes: 5
168 default_timeout_ms: 5000
169 enable_fallback: true
170 health_check_interval_ms: 30000
171
172servers:
173 rust:
174 - language: rust
175 extensions: [".rs"]
176 executable: ""
177 args: []
178 env: {}
179 enabled: true
180 timeout_ms: 10000
181 max_restarts: 3
182 idle_timeout_ms: 300000
183"#;
184
185 let result = ConfigLoader::load_from_string(yaml);
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn test_validate_no_extensions() {
191 let yaml = r#"
192global:
193 max_processes: 5
194 default_timeout_ms: 5000
195 enable_fallback: true
196 health_check_interval_ms: 30000
197
198servers:
199 rust:
200 - language: rust
201 extensions: []
202 executable: rust-analyzer
203 args: []
204 env: {}
205 enabled: true
206 timeout_ms: 10000
207 max_restarts: 3
208 idle_timeout_ms: 300000
209"#;
210
211 let result = ConfigLoader::load_from_string(yaml);
212 assert!(result.is_err());
213 }
214
215 #[test]
216 fn test_merge_configs() {
217 let builtin_yaml = r#"
218global:
219 max_processes: 5
220 default_timeout_ms: 5000
221 enable_fallback: true
222 health_check_interval_ms: 30000
223
224servers:
225 rust:
226 - language: rust
227 extensions: [".rs"]
228 executable: rust-analyzer
229 args: []
230 env: {}
231 enabled: true
232 timeout_ms: 10000
233 max_restarts: 3
234 idle_timeout_ms: 300000
235"#;
236
237 let user_yaml = r#"
238global:
239 max_processes: 10
240 default_timeout_ms: 10000
241 enable_fallback: true
242 health_check_interval_ms: 30000
243
244servers:
245 python:
246 - language: python
247 extensions: [".py"]
248 executable: pylsp
249 args: []
250 env: {}
251 enabled: true
252 timeout_ms: 5000
253 max_restarts: 3
254 idle_timeout_ms: 300000
255"#;
256
257 let builtin = ConfigLoader::load_from_string(builtin_yaml).unwrap();
258 let user = ConfigLoader::load_from_string(user_yaml).ok();
259
260 let result = ConfigLoader::merge_configs(None, None, user, builtin).unwrap();
261
262 assert_eq!(result.servers.len(), 2);
263 assert!(result.servers.contains_key("rust"));
264 assert!(result.servers.contains_key("python"));
265 assert_eq!(result.global.max_processes, 10);
266 }
267}