/ src / server / config.rs
config.rs
  1  use serde::{Deserialize, Serialize};
  2  use std::fs;
  3  use std::path::Path;
  4  use std::sync::atomic::{AtomicBool, Ordering};
  5  use std::sync::Arc;
  6  use tokio::sync::RwLock;
  7  
  8  /// Configuration for a single external provider
  9  #[derive(Debug, Clone, Default, Deserialize, Serialize)]
 10  #[serde(deny_unknown_fields)]
 11  pub struct ProviderConfig {
 12      /// Whether this provider is enabled
 13      #[serde(default)]
 14      pub enabled: bool,
 15      /// Override binary path for this provider
 16      #[serde(default)]
 17      pub binary: Option<String>,
 18  }
 19  
 20  /// Configuration for external providers
 21  #[derive(Debug, Clone, Deserialize, Serialize)]
 22  pub struct ProvidersConfig {
 23      /// Directory containing provider binaries
 24      #[serde(default = "default_providers_path")]
 25      pub path: String,
 26      /// Individual provider configurations (keyed by provider name)
 27      #[serde(flatten)]
 28      pub providers: std::collections::HashMap<String, ProviderConfig>,
 29  }
 30  
 31  fn default_providers_path() -> String {
 32      // Use XDG_DATA_HOME or fall back to ~/.local/share
 33      std::env::var("XDG_DATA_HOME")
 34          .map(|xdg| format!("{}/ecolog/providers", xdg))
 35          .unwrap_or_else(|_| {
 36              std::env::var("HOME")
 37                  .map(|home| format!("{}/.local/share/ecolog/providers", home))
 38                  .unwrap_or_else(|_| "~/.local/share/ecolog/providers".to_string())
 39          })
 40  }
 41  
 42  impl Default for ProvidersConfig {
 43      fn default() -> Self {
 44          Self {
 45              path: default_providers_path(),
 46              providers: std::collections::HashMap::new(),
 47          }
 48      }
 49  }
 50  
 51  #[derive(Debug, Clone, Deserialize, Serialize, Default)]
 52  #[serde(deny_unknown_fields)]
 53  pub struct EcologConfig {
 54      #[serde(default)]
 55      pub features: FeatureConfig,
 56      #[serde(default)]
 57      pub strict: StrictConfig,
 58      #[serde(default)]
 59      pub inlay_hints: InlayHintConfig,
 60      #[serde(default)]
 61      pub workspace: abundantis::config::WorkspaceConfig,
 62      #[serde(default)]
 63      pub resolution: abundantis::config::ResolutionConfig,
 64      #[serde(default)]
 65      pub interpolation: abundantis::config::InterpolationConfig,
 66      #[serde(default)]
 67      pub cache: abundantis::config::CacheConfig,
 68      #[serde(default)]
 69      pub sources: abundantis::config::SourcesConfig,
 70      #[serde(default)]
 71      pub providers: ProvidersConfig,
 72  }
 73  
 74  #[derive(Debug, Clone, Deserialize, Serialize)]
 75  #[serde(deny_unknown_fields)]
 76  pub struct FeatureConfig {
 77      #[serde(default = "true_bool")]
 78      pub hover: bool,
 79      #[serde(default = "true_bool")]
 80      pub completion: bool,
 81      #[serde(default = "true_bool")]
 82      pub diagnostics: bool,
 83      #[serde(default = "true_bool")]
 84      pub definition: bool,
 85      #[serde(default)]
 86      pub inlay_hints: bool,
 87  }
 88  
 89  #[derive(Debug, Clone, Deserialize, Serialize)]
 90  #[serde(deny_unknown_fields)]
 91  pub struct StrictConfig {
 92      #[serde(default = "true_bool")]
 93      pub hover: bool,
 94      #[serde(default = "true_bool")]
 95      pub completion: bool,
 96  }
 97  
 98  impl Default for FeatureConfig {
 99      fn default() -> Self {
100          Self {
101              hover: true,
102              completion: true,
103              diagnostics: true,
104              definition: true,
105              inlay_hints: false,
106          }
107      }
108  }
109  
110  #[derive(Debug, Clone, Deserialize, Serialize)]
111  #[serde(deny_unknown_fields)]
112  pub struct InlayHintConfig {
113      #[serde(default = "true_bool")]
114      pub direct_references: bool,
115  
116      #[serde(default = "true_bool")]
117      pub binding_declarations: bool,
118  
119      #[serde(default)]
120      pub binding_usages: bool,
121  
122      #[serde(default = "true_bool")]
123      pub property_accesses: bool,
124  
125      #[serde(default = "default_max_hint_length")]
126      pub max_value_length: usize,
127  
128      #[serde(default)]
129      pub max_hints_per_line: usize,
130  }
131  
132  fn default_max_hint_length() -> usize {
133      30
134  }
135  
136  impl Default for InlayHintConfig {
137      fn default() -> Self {
138          Self {
139              direct_references: true,
140              binding_declarations: true,
141              binding_usages: false,
142              property_accesses: true,
143              max_value_length: default_max_hint_length(),
144              max_hints_per_line: 0,
145          }
146      }
147  }
148  
149  impl Default for StrictConfig {
150      fn default() -> Self {
151          Self {
152              hover: true,
153              completion: true,
154          }
155      }
156  }
157  
158  impl EcologConfig {
159      pub fn to_abundantis_config(&self) -> abundantis::config::AbundantisConfig {
160          abundantis::config::AbundantisConfig {
161              workspace: self.workspace.clone(),
162              resolution: self.resolution.clone(),
163              interpolation: self.interpolation.clone(),
164              cache: self.cache.clone(),
165              sources: self.sources.clone(),
166          }
167      }
168  }
169  
170  /// Cached feature flags for lock-free access in hot paths.
171  /// These atomics are updated when config changes.
172  #[derive(Default)]
173  pub struct CachedFeatureFlags {
174      pub hover: AtomicBool,
175      pub completion: AtomicBool,
176      pub diagnostics: AtomicBool,
177      pub definition: AtomicBool,
178      pub inlay_hints: AtomicBool,
179  }
180  
181  impl CachedFeatureFlags {
182      fn new() -> Self {
183          Self {
184              hover: AtomicBool::new(true),
185              completion: AtomicBool::new(true),
186              diagnostics: AtomicBool::new(true),
187              definition: AtomicBool::new(true),
188              inlay_hints: AtomicBool::new(false),
189          }
190      }
191  
192      fn update_from(&self, features: &FeatureConfig) {
193          self.hover.store(features.hover, Ordering::Relaxed);
194          self.completion
195              .store(features.completion, Ordering::Relaxed);
196          self.diagnostics
197              .store(features.diagnostics, Ordering::Relaxed);
198          self.definition
199              .store(features.definition, Ordering::Relaxed);
200          self.inlay_hints
201              .store(features.inlay_hints, Ordering::Relaxed);
202      }
203  }
204  
205  pub struct ConfigManager {
206      config: Arc<RwLock<EcologConfig>>,
207      init_settings: Arc<RwLock<Option<serde_json::Value>>>,
208      /// Cached feature flags for lock-free access.
209      /// Updated whenever config is loaded or updated.
210      pub cached_features: CachedFeatureFlags,
211  }
212  
213  impl Default for ConfigManager {
214      fn default() -> Self {
215          Self::new()
216      }
217  }
218  
219  impl ConfigManager {
220      pub fn new() -> Self {
221          Self {
222              config: Arc::new(RwLock::new(EcologConfig::default())),
223              init_settings: Arc::new(RwLock::new(None)),
224              cached_features: CachedFeatureFlags::new(),
225          }
226      }
227  
228      /// Check if hover feature is enabled (lock-free).
229      #[inline]
230      pub fn is_hover_enabled(&self) -> bool {
231          self.cached_features.hover.load(Ordering::Relaxed)
232      }
233  
234      /// Check if completion feature is enabled (lock-free).
235      #[inline]
236      pub fn is_completion_enabled(&self) -> bool {
237          self.cached_features.completion.load(Ordering::Relaxed)
238      }
239  
240      /// Check if diagnostics feature is enabled (lock-free).
241      #[inline]
242      pub fn is_diagnostics_enabled(&self) -> bool {
243          self.cached_features.diagnostics.load(Ordering::Relaxed)
244      }
245  
246      /// Check if definition feature is enabled (lock-free).
247      #[inline]
248      pub fn is_definition_enabled(&self) -> bool {
249          self.cached_features.definition.load(Ordering::Relaxed)
250      }
251  
252      /// Check if inlay hints feature is enabled (lock-free).
253      #[inline]
254      pub fn is_inlay_hints_enabled(&self) -> bool {
255          self.cached_features.inlay_hints.load(Ordering::Relaxed)
256      }
257  
258      pub fn get_config(&self) -> Arc<RwLock<EcologConfig>> {
259          self.config.clone()
260      }
261  
262      pub async fn set_init_settings(&self, settings: Option<serde_json::Value>) {
263          let mut lock = self.init_settings.write().await;
264          *lock = settings;
265      }
266  
267      pub async fn load_from_workspace(&self, root: &Path) -> Result<EcologConfig, String> {
268          let mut config_json = serde_json::to_value(EcologConfig::default())
269              .map_err(|e| format!("Failed to serialize defaults: {}", e))?;
270  
271          {
272              let init_settings = self.init_settings.read().await;
273              if let Some(settings) = init_settings.as_ref() {
274                  merge_json(&mut config_json, settings);
275              }
276          }
277  
278          let config_path = root.join("ecolog.toml");
279          if config_path.exists() {
280              let toml_content = fs::read_to_string(&config_path)
281                  .map_err(|e| format!("Failed to read config: {}", e))?;
282  
283              let toml_value: toml::Value = toml::from_str(&toml_content)
284                  .map_err(|e| format!("Failed to parse config: {}", e))?;
285              let toml_json = toml_to_json(&toml_value);
286  
287              merge_json(&mut config_json, &toml_json);
288          }
289  
290          let config: EcologConfig = serde_json::from_value(config_json)
291              .map_err(|e| format!("Failed to deserialize merged config: {}", e))?;
292  
293          // Update cached feature flags for lock-free access
294          self.cached_features.update_from(&config.features);
295  
296          let mut lock = self.config.write().await;
297          *lock = config.clone();
298  
299          Ok(config)
300      }
301  
302      pub async fn update(&self, new_config: EcologConfig) {
303          // Update cached feature flags for lock-free access
304          self.cached_features.update_from(&new_config.features);
305  
306          let mut lock = self.config.write().await;
307          *lock = new_config;
308      }
309  
310      pub async fn set_precedence(&self, precedence: Vec<abundantis::config::SourcePrecedence>) {
311          let mut lock = self.config.write().await;
312          lock.resolution.precedence = precedence;
313      }
314  
315      pub async fn get_precedence(&self) -> Vec<abundantis::config::SourcePrecedence> {
316          let lock = self.config.read().await;
317          lock.resolution.precedence.clone()
318      }
319  
320      pub async fn set_interpolation_enabled(&self, enabled: bool) {
321          let mut lock = self.config.write().await;
322          lock.interpolation.enabled = enabled;
323      }
324  
325      pub async fn get_interpolation_enabled(&self) -> bool {
326          let lock = self.config.read().await;
327          lock.interpolation.enabled
328      }
329  
330      pub async fn get_providers_config(&self) -> ProvidersConfig {
331          let lock = self.config.read().await;
332          lock.providers.clone()
333      }
334  }
335  
336  fn toml_to_json(toml: &toml::Value) -> serde_json::Value {
337      match toml {
338          toml::Value::String(s) => serde_json::Value::String(s.clone()),
339          toml::Value::Integer(i) => serde_json::Value::Number((*i).into()),
340          toml::Value::Float(f) => serde_json::Number::from_f64(*f)
341              .map(serde_json::Value::Number)
342              .unwrap_or(serde_json::Value::Null),
343          toml::Value::Boolean(b) => serde_json::Value::Bool(*b),
344          toml::Value::Array(arr) => serde_json::Value::Array(arr.iter().map(toml_to_json).collect()),
345          toml::Value::Table(table) => {
346              let map: serde_json::Map<String, serde_json::Value> = table
347                  .iter()
348                  .map(|(k, v)| (k.clone(), toml_to_json(v)))
349                  .collect();
350              serde_json::Value::Object(map)
351          }
352          toml::Value::Datetime(dt) => serde_json::Value::String(dt.to_string()),
353      }
354  }
355  
356  fn merge_json(base: &mut serde_json::Value, overlay: &serde_json::Value) {
357      match (base, overlay) {
358          (serde_json::Value::Object(base_map), serde_json::Value::Object(overlay_map)) => {
359              for (key, overlay_val) in overlay_map {
360                  if overlay_val.is_null() {
361                      continue;
362                  }
363                  match base_map.get_mut(key) {
364                      Some(base_val) => merge_json(base_val, overlay_val),
365                      None => {
366                          base_map.insert(key.clone(), overlay_val.clone());
367                      }
368                  }
369              }
370          }
371          (base, overlay) => {
372              if !overlay.is_null() {
373                  *base = overlay.clone();
374              }
375          }
376      }
377  }
378  
379  fn true_bool() -> bool {
380      true
381  }
382  
383  #[cfg(test)]
384  mod tests {
385      use super::*;
386      use std::io::Write;
387      use tempfile::TempDir;
388  
389      #[test]
390      fn test_feature_config_default() {
391          let config = FeatureConfig::default();
392          assert!(config.hover);
393          assert!(config.completion);
394          assert!(config.diagnostics);
395          assert!(config.definition);
396      }
397  
398      #[test]
399      fn test_strict_config_default() {
400          let config = StrictConfig::default();
401          assert!(config.hover);
402          assert!(config.completion);
403      }
404  
405      #[test]
406      fn test_ecolog_config_default() {
407          let config = EcologConfig::default();
408          assert!(config.features.hover);
409          assert!(config.features.completion);
410          assert!(config.features.diagnostics);
411          assert!(config.features.definition);
412          assert!(config.strict.hover);
413          assert!(config.strict.completion);
414      }
415  
416      #[test]
417      fn test_ecolog_config_to_abundantis() {
418          let config = EcologConfig::default();
419          let abundantis_config = config.to_abundantis_config();
420  
421          assert!(abundantis_config.interpolation.enabled);
422      }
423  
424      #[test]
425      fn test_config_manager_new() {
426          let manager = ConfigManager::new();
427  
428          let _config = manager.get_config();
429      }
430  
431      #[tokio::test]
432      async fn test_config_manager_load_missing_file() {
433          let manager = ConfigManager::new();
434          let temp_dir = TempDir::new().unwrap();
435  
436          let result = manager.load_from_workspace(temp_dir.path()).await;
437          assert!(result.is_ok());
438  
439          let config = result.unwrap();
440          assert!(config.features.hover);
441      }
442  
443      #[tokio::test]
444      async fn test_config_manager_load_valid_file() {
445          let manager = ConfigManager::new();
446          let temp_dir = TempDir::new().unwrap();
447  
448          let config_content = r#"
449  [features]
450  hover = false
451  completion = true
452  diagnostics = true
453  definition = false
454  
455  [strict]
456  hover = false
457  completion = false
458  "#;
459  
460          let config_path = temp_dir.path().join("ecolog.toml");
461          let mut file = std::fs::File::create(&config_path).unwrap();
462          file.write_all(config_content.as_bytes()).unwrap();
463  
464          let result = manager.load_from_workspace(temp_dir.path()).await;
465          assert!(result.is_ok());
466  
467          let config = result.unwrap();
468          assert!(!config.features.hover);
469          assert!(config.features.completion);
470          assert!(config.features.diagnostics);
471          assert!(!config.features.definition);
472          assert!(!config.strict.hover);
473          assert!(!config.strict.completion);
474      }
475  
476      #[tokio::test]
477      async fn test_config_manager_load_invalid_file() {
478          let manager = ConfigManager::new();
479          let temp_dir = TempDir::new().unwrap();
480  
481          let config_path = temp_dir.path().join("ecolog.toml");
482          let mut file = std::fs::File::create(&config_path).unwrap();
483          file.write_all(b"invalid toml content {{{").unwrap();
484  
485          let result = manager.load_from_workspace(temp_dir.path()).await;
486          assert!(result.is_err());
487          assert!(result.unwrap_err().contains("Failed to parse config"));
488      }
489  
490      #[tokio::test]
491      async fn test_config_manager_update() {
492          let manager = ConfigManager::new();
493  
494          let new_config = EcologConfig {
495              features: FeatureConfig {
496                  hover: false,
497                  ..FeatureConfig::default()
498              },
499              ..EcologConfig::default()
500          };
501  
502          manager.update(new_config).await;
503  
504          let config = manager.get_config();
505          let lock = config.read().await;
506          assert!(!lock.features.hover);
507      }
508  
509      #[tokio::test]
510      async fn test_config_manager_init_settings_only() {
511          let manager = ConfigManager::new();
512          let temp_dir = TempDir::new().unwrap();
513  
514          let init_settings = serde_json::json!({
515              "features": {
516                  "hover": false,
517                  "diagnostics": false
518              }
519          });
520          manager.set_init_settings(Some(init_settings)).await;
521  
522          let result = manager.load_from_workspace(temp_dir.path()).await;
523          assert!(result.is_ok());
524  
525          let config = result.unwrap();
526  
527          assert!(!config.features.hover);
528          assert!(!config.features.diagnostics);
529  
530          assert!(config.features.completion);
531          assert!(config.features.definition);
532      }
533  
534      #[tokio::test]
535      async fn test_config_manager_toml_overrides_init_settings() {
536          let manager = ConfigManager::new();
537          let temp_dir = TempDir::new().unwrap();
538  
539          let init_settings = serde_json::json!({
540              "features": {
541                  "hover": false,
542                  "diagnostics": false,
543                  "completion": false
544              }
545          });
546          manager.set_init_settings(Some(init_settings)).await;
547  
548          let config_content = r#"
549  [features]
550  hover = true
551  diagnostics = true
552  "#;
553          let config_path = temp_dir.path().join("ecolog.toml");
554          let mut file = std::fs::File::create(&config_path).unwrap();
555          file.write_all(config_content.as_bytes()).unwrap();
556  
557          let result = manager.load_from_workspace(temp_dir.path()).await;
558          assert!(result.is_ok());
559  
560          let config = result.unwrap();
561  
562          assert!(config.features.hover);
563          assert!(config.features.diagnostics);
564  
565          assert!(!config.features.completion);
566  
567          assert!(config.features.definition);
568      }
569  
570      #[tokio::test]
571      async fn test_config_manager_workspace_root_from_init_settings() {
572          let manager = ConfigManager::new();
573          let temp_dir = TempDir::new().unwrap();
574  
575          let init_settings = serde_json::json!({
576              "workspace": {
577                  "root": "/custom/workspace/root"
578              }
579          });
580          manager.set_init_settings(Some(init_settings)).await;
581  
582          let result = manager.load_from_workspace(temp_dir.path()).await;
583          assert!(result.is_ok());
584  
585          let config = result.unwrap();
586          assert_eq!(
587              config.workspace.root,
588              Some(std::path::PathBuf::from("/custom/workspace/root"))
589          );
590      }
591  
592      #[tokio::test]
593      async fn test_config_manager_rejects_top_level_versioned_schema_key() {
594          let manager = ConfigManager::new();
595          let temp_dir = TempDir::new().unwrap();
596  
597          let config_content = r#"
598  schema_version = 1
599  
600  [features]
601  hover = true
602  "#;
603          let config_path = temp_dir.path().join("ecolog.toml");
604          let mut file = std::fs::File::create(&config_path).unwrap();
605          file.write_all(config_content.as_bytes()).unwrap();
606  
607          let result = manager.load_from_workspace(temp_dir.path()).await;
608          assert!(result.is_err());
609          assert!(result
610              .unwrap_err()
611              .contains("Failed to deserialize merged config"));
612      }
613  
614      #[tokio::test]
615      async fn test_config_manager_rejects_unknown_nested_field() {
616          let manager = ConfigManager::new();
617          let temp_dir = TempDir::new().unwrap();
618  
619          let config_content = r#"
620  [features]
621  hover = true
622  legacy_hover_mode = true
623  "#;
624          let config_path = temp_dir.path().join("ecolog.toml");
625          let mut file = std::fs::File::create(&config_path).unwrap();
626          file.write_all(config_content.as_bytes()).unwrap();
627  
628          let result = manager.load_from_workspace(temp_dir.path()).await;
629          assert!(result.is_err());
630          assert!(result
631              .unwrap_err()
632              .contains("Failed to deserialize merged config"));
633      }
634  
635      #[tokio::test]
636      async fn test_config_manager_rejects_unknown_init_settings_fields() {
637          let manager = ConfigManager::new();
638          let temp_dir = TempDir::new().unwrap();
639  
640          let init_settings = serde_json::json!({
641              "schema_version": 1,
642              "features": {
643                  "hover": true
644              }
645          });
646          manager.set_init_settings(Some(init_settings)).await;
647  
648          let result = manager.load_from_workspace(temp_dir.path()).await;
649          assert!(result.is_err());
650          assert!(result
651              .unwrap_err()
652              .contains("Failed to deserialize merged config"));
653      }
654  }