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 }